Merge remote-tracking branch 'upstream/main' into v2/ses_01M6EA9PJ1AQ4Q6N2S9TNXP8B7-w3-level-misc

# Conflicts:
#	crates/horizon-client/src/mod_api.rs
#	crates/horizon-client/src/mod_api/generated.rs
This commit is contained in:
arismoko committed 2026-11-02 17:40:17 +00:00
commit 0a5b17f53b
203 files changed
+28552 -513

No files matched your search

+9
View File
@@ -185,6 +185,15 @@ jobs:
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_imports.py "$file"
fi
done
- name: Baseline x86-64 CPUs only
# No POPCNT or LZCNT at all (the script says why only those): a C library's count intrinsic killed the
# game before main on a Core 2 (#7264). Not the Mac builds: every Intel Mac that runs macOS 11 has both.
if: runner.os != 'macOS'
run: |
set -euo pipefail
rustup component add llvm-tools
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_cpu_baseline.py \
$(for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do echo "$BIN/$binary${{ matrix.exe }}"; done)
- name: Every program starts
run: |
set -euo pipefail
+2 -1
View File
@@ -96,7 +96,8 @@ configuration and toolchain changes select the whole workspace; CI also keeps it
Work is tracked in GitHub issues, and an issue that is assigned is taken (so is one with the `in progress` label:
claimed on Discord with Kira's `/claim`): pick another, or ask its holder in the thread. Before you start, comment
**`/claim`** on the issue; you are assigned at once, no role needed. No issue for your work yet? Open one (a line is
enough), then claim it. `/unclaim` gives it back.
enough), then claim it. `/unclaim` gives it back. Issues labelled `tracker`, `proposal` or `not claimable` aren't
claimed whole: claim one of the issues a tracker lists, or open one for the part you take.
A claim with no open pull request linking the issue (`Fixes #123` in the description, or a draft) is released after
5 days, with a comment: open a draft early. You can hold 8 open issues at a time, not counting the ones your
Generated
-2
View File
@@ -2114,8 +2114,6 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libmimalloc-sys"
version = "0.1.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
dependencies = [
"cc",
"cty",
+5 -1
View File
@@ -1,5 +1,5 @@
[workspace]
exclude = ["launcher", "crates/horizon-telemetry-server", "crates/horizon-mod-registry", "third_party/wgpu-hal", "third_party/naga"]
exclude = ["launcher", "crates/horizon-telemetry-server", "crates/horizon-mod-registry", "third_party/wgpu-hal", "third_party/naga", "third_party/libmimalloc-sys"]
members = [
"crates/horizon-core",
"crates/horizon-dualsense",
@@ -67,6 +67,10 @@ large_enum_variant = "allow"
wgpu-hal = { path = "third_party/wgpu-hal" }
# naga 30.0.1, with the fixes to its GLSL frontend that shader packs need (THIRD_PARTY_NOTICES.md).
naga = { path = "third_party/naga" }
# libmimalloc-sys 0.1.49 whose mimalloc v3 counts bits without POPCNT when MSVC builds it: the
# game died before main on CPUs without it (Core 2, #7264; THIRD_PARTY_NOTICES.md). Drop this and
# the folder once a libmimalloc-sys release has microsoft/mimalloc#1291's fix.
libmimalloc-sys = { path = "third_party/libmimalloc-sys" }
# Line tables keep file:line in backtraces; full debug info made every
# example and test binary hundreds of megabytes and slow to link.
+9
View File
@@ -14,6 +14,15 @@ Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
fixes to its GLSL frontend that shader packs need, each applied in the commit that needed it. It is licensed under MIT or Apache-2.0; both licenses
are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
## libmimalloc-sys and mimalloc
`third_party/libmimalloc-sys` is [libmimalloc-sys](https://github.com/purpleprotocol/mimalloc_rust)
0.1.49 by Octavian Oncescu, with the [mimalloc](https://github.com/microsoft/mimalloc) v3 sources it
bundles (Copyright (c) 2018-2025 Microsoft Corporation, Daan Leijen), both under the MIT license
(`LICENSE.txt` and `c_src/mimalloc/v3/LICENSE` in that folder). It holds only the v3 sources. One
change: `mi_popcount` in `include/mimalloc/bits.h` uses mimalloc's portable bit count on MSVC
x86/x64 instead of the POPCNT instruction, which CPUs before Nehalem lack (#7264).
## Pixelify Sans
`crates/horizon-theme/fonts/PixelifySans-*.ttf` is [Pixelify Sans](https://github.com/eifetx/Pixelify-Sans)
+32 -3
View File
@@ -4817,11 +4817,13 @@ impl Renderer {
) -> impl Iterator<Item = (&'a ([i32; 3], u32), bool)> + 'a {
let fade = fade_millis(fade_seconds);
let now = Instant::now();
// No section uploaded within the fade: all are whole, none is looked up.
let all = !self.sections_fading_in(fade, now);
let min_y = self.layout.min_y();
self.block_entities
.iter()
.filter(move |(key, _)| {
chunk_visibility(self.section_elapsed_ms(**key, now), fade)
all || chunk_visibility(self.section_elapsed_ms(**key, now), fade)
>= CHUNK_VISIBILITY_THRESHOLD
})
.filter_map(move |(key, blocks)| {
@@ -5089,8 +5091,8 @@ impl Renderer {
if self.detail_columns.remove(&(x, z)) == Some(complete) {
self.detail_revision += 1;
}
self.occlusion.remove_column(x, z);
}
self.occlusion.remove_columns(&gone);
let kept = |key: &SectionKey| !gone.contains(&(key.0, key.2));
let before = self.block_entities.len();
self.block_entities.retain(|key, _| kept(key));
@@ -6232,8 +6234,9 @@ impl Renderer {
self.config.height.max(1) as f32,
frame.depth_far,
));
let hand_view_proj = hand_projection * frame.bob * view;
let hand_globals = Globals {
view_proj: (hand_projection * frame.bob * view).to_cols_array_2d(),
view_proj: hand_view_proj.to_cols_array_2d(),
..globals
};
self.uploads.buffer(
@@ -8755,6 +8758,8 @@ impl Renderer {
first_crosshair: debug_depth + debug_top,
world_depth: false,
real: false,
hand_drawn: !pack_draws_hand,
hand_view_proj,
};
self.draw_overlay(&mut encoder, frame, &generated_view, generated, &overlay);
}
@@ -8797,6 +8802,8 @@ impl Renderer {
first_crosshair: debug_depth + debug_top,
world_depth: scaled.is_none(),
real: true,
hand_drawn: !pack_draws_hand,
hand_view_proj,
};
let readback =
self.draw_overlay(&mut encoder, frame, &view_target, &target_texture, &overlay);
@@ -9084,6 +9091,23 @@ impl Renderer {
}
}
}
// A native mod's turn after the hand, on the real frame only.
if overlay.real && !frame.title {
native_render::native_hand_drawn(&mut native_render::HandDrawn {
device: &self.device,
queue: &self.queue,
encoder: &mut *encoder,
color: target_texture,
color_view: view_target,
color_format: self.config.format.remove_srgb_suffix(),
size: [self.config.width, self.config.height],
hand: if overlay.hand_drawn { frame.hand } else { &[] },
view_proj: overlay.hand_view_proj,
camera: frame.camera,
atlas: self.hud.atlas_texture.as_ref(),
quad_indices: &self.indices,
});
}
// GameRenderer.render: LevelRenderer.blitEntityOutline after the level and the hand.
self.entity_outline.draw(
encoder,
@@ -9621,6 +9645,11 @@ struct Overlay {
world_depth: bool,
/// The real frame, not frame generation's made one: the world icon grabs it.
real: bool,
/// The game drew the first-person hand, in its own pass or at the end of the world's
/// (`hand_in_world`), not a shader pack.
hand_drawn: bool,
/// Camera-relative world to clip space for the hand: its own field of view and the bob.
hand_view_proj: Mat4,
}
fn world_projection(fov_degrees: f32, aspect: f32, far: f32) -> Mat4 {
+46 -6
View File
@@ -506,12 +506,17 @@ impl Arena {
by_page[page].push((start, Self::span(vertices).min(self.page_limit)));
}
for (page, ranges) in by_page.into_iter().enumerate() {
if !ranges.is_empty() {
self.pages[page]
.as_mut()
.expect("live arena page")
.free
.give_all(ranges);
if ranges.is_empty() {
continue;
}
let page = self.pages[page].as_mut().expect("live arena page");
// Every range of the page given back (a far teleport empties the arena): one
// free range, without sorting tens of thousands into the list.
let given: u64 = ranges.iter().map(|r| r.1).sum();
if page.free_vertices() + given == page.capacity {
page.free = FreeList::new(page.capacity);
} else {
page.free.give_all(ranges);
}
}
}
@@ -746,6 +751,41 @@ mod tests {
assert_eq!(read(&device, &queue, arena.buffer(page), next * 4), 9);
}
/// Released together, every range of a page leaves it one free range (a far teleport
/// empties the arena at once), while a page keeping some ranges frees only the others.
#[test]
fn ranges_released_at_once_free_exactly_their_vertices() {
let Some((device, queue)) = device() else {
eprintln!("no GPU adapter: skipped");
return;
};
let mut arena = Arena::new(&device, 4, GRAIN, false);
arena.page_limit = 4 * GRAIN;
let ranges: Vec<(u32, u64)> = (0..8)
.map(|_| alloc(&mut arena, &device, &queue, GRAIN).unwrap())
.collect();
let (first, second) = (ranges[0].0, ranges[7].0);
assert_ne!(first, second);
// All four of the first page, the middle two of the second.
let released: Vec<(u32, u64, u64)> = ranges
.iter()
.enumerate()
.filter(|&(i, _)| i < 4 || i == 5 || i == 6)
.map(|(_, &(page, start))| (page, start, GRAIN))
.collect();
arena.release_all(released);
let free = |page: u32| -> Vec<(u64, u64)> {
let free = &arena.pages[page as usize].as_ref().unwrap().free.0;
free.iter().map(|(&start, &len)| (start, len)).collect()
};
assert_eq!(free(first), vec![(0, 4 * GRAIN)]);
assert_eq!(free(second), vec![(ranges[5].1, 2 * GRAIN)]);
// The emptied page takes a whole page's range again, without growing.
let pages = arena.pages.iter().flatten().count();
assert_eq!(arena.alloc_existing(4 * GRAIN), Some((first, 0)));
assert_eq!(arena.pages.iter().flatten().count(), pages);
}
#[test]
fn a_sparse_page_empties_into_the_others_and_is_reclaimed() {
let Some((device, queue)) = device() else {
@@ -12,7 +12,7 @@ use std::sync::{
};
mod generated;
pub use generated::{RenderHooks, RenderNeedsEvent, WorldDrawnEvent};
pub use generated::{HandDrawnEvent, RenderHooks, RenderNeedsEvent, WorldDrawnEvent};
pub use wgpu;
static RENDERERS: OnceLock<Box<[&'static dyn RenderHooks]>> = OnceLock::new();
@@ -40,8 +40,8 @@ pub fn world_drawn_hooked() -> bool {
}
/// What a mod can draw with once the world is drawn: after the sky, terrain, entities,
/// particles, weather and the world post pass, before the first-person hand, the HUD and the
/// screens. `color` holds the world's picture (copy it out before reading it: it is the target
/// particles, weather and the world post pass, before the first-person hand ([`HandDrawn`]),
/// the HUD and the screens. `color` holds the world's picture (copy it out before reading it: it is the target
/// drawn into), `depth` the world's depth, reversed (0 is the sky).
#[non_exhaustive]
pub struct WorldDrawn<'a> {
@@ -76,6 +76,35 @@ pub struct WorldDrawn<'a> {
pub quad_indices: &'a wgpu::Buffer,
}
/// What a mod can draw with once the first-person hand is drawn ([`native_hand_drawn`]): after
/// the world and the hand, before entity outlines, post effects, screen effects and the HUD.
/// `color` holds the frame's picture with the hand in it (copy it out before reading it).
#[non_exhaustive]
pub struct HandDrawn<'a> {
pub device: &'a wgpu::Device,
pub queue: &'a wgpu::Queue,
pub encoder: &'a mut wgpu::CommandEncoder,
/// The frame's target and a view of it in `color_format`.
pub color: &'a wgpu::Texture,
pub color_view: &'a wgpu::TextureView,
pub color_format: wgpu::TextureFormat,
/// The size of `color`.
pub size: [u32; 2],
/// The hand and held item just drawn, camera-relative quads of 4 vertices: empty when no
/// hand drew (third person, F1, a sleeping or spectating player) or a shader pack drew it.
pub hand: &'a [Vertex],
/// Camera-relative world to clip space for the hand: its own field of view
/// (`hand_fov_degrees`) and the view bob, without TAA's jitter (the hand has none). Its
/// depth is the hand's own: the hand's pass clears depth and keeps none.
pub view_proj: Mat4,
/// The camera's position in the world.
pub camera: DVec3,
/// The shared atlas the hand's UVs point into.
pub atlas: Option<&'a wgpu::Texture>,
/// Quad indices (0 1 2 0 2 3 for each 4 vertices).
pub quad_indices: &'a wgpu::Buffer,
}
/// What the mods need from a frame that the game doesn't make for itself. Each mod sets what it
/// needs and leaves the rest, so several can ask.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -94,6 +123,13 @@ pub fn native_world_drawn(world: &mut WorldDrawn<'_>) {
generated::dispatch_world_drawn(RENDERERS.get().map_or(&[], |renderers| renderers), world);
}
/// The first-person hand is drawn (see [`HandDrawn`]), on every frame [`native_world_drawn`]
/// runs, with no hand too. API @API:hand_drawn@.
#[inline(never)]
pub fn native_hand_drawn(hand: &mut HandDrawn<'_>) {
generated::dispatch_hand_drawn(RENDERERS.get().map_or(&[], |renderers| renderers), hand);
}
/// Asked as a frame is built and drawn: set what your mod needs.
#[inline(never)]
pub fn native_render_needs(needs: &mut RenderNeeds) {
@@ -11,6 +11,11 @@ pub trait RenderHooks: Send + Sync {
fn render_needs(&self, event: &mut RenderNeedsEvent<'_>) {
let _ = event;
}
/// The first-person hand is drawn (see [`HandDrawn`]), on every frame [`native_world_drawn`]
/// runs, with no hand too. API @API:hand_drawn@.
fn hand_drawn(&self, event: &mut HandDrawnEvent<'_, '_>) {
let _ = event;
}
}
/// What [`RenderHooks::world_drawn`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
@@ -34,3 +39,14 @@ pub fn dispatch_render_needs(mods: &[&dyn RenderHooks], needs: &mut RenderNeeds)
mod_hook.render_needs(&mut event);
}
}
/// What [`RenderHooks::hand_drawn`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct HandDrawnEvent<'a, 'b> {
pub hand: &'a mut HandDrawn<'b>,
}
pub fn dispatch_hand_drawn(mods: &[&dyn RenderHooks], hand: &mut HandDrawn<'_>) {
let mut event = HandDrawnEvent { hand };
for mod_hook in mods {
mod_hook.hand_drawn(&mut event);
}
}
@@ -13,6 +13,7 @@
//! frame has (vanilla's partial update), so what it opens shows at once; what
//! it closes goes at the next full search.
use super::SectionKey;
use crate::fast_hash::FxHashSet;
use crate::mesher::ALL_VISIBLE;
use glam::DVec3;
use std::collections::{HashMap, VecDeque};
@@ -589,6 +590,19 @@ impl Occlusion {
}
}
}
/// `remove_column` for each of `gone`. When they are every column it holds (a
/// teleport drops the whole view), the old graph says nothing about what arrives
/// next: it starts over as `clear` does instead of seeing through each removed
/// section one edit at a time (some 90 000 at render distance 32).
pub fn remove_columns(&mut self, gone: &FxHashSet<(i32, i32)>) {
if self.columns.map.keys().all(|column| gone.contains(column)) {
self.clear();
return;
}
for &(x, z) in gone {
self.remove_column(x, z);
}
}
pub fn clear(&mut self) {
self.columns.map.clear();
self.edits.clear();
@@ -1313,6 +1327,46 @@ mod tests {
println!("search_cost: best {best:.2} ms over 12, {reached} reached");
}
/// Removed columns see through every face: the graph is the one of a world that
/// never had them, whether some columns go or all of them (a teleport).
#[test]
fn removed_columns_leave_the_graph_of_a_world_without_them() {
fn without(o: &Occlusion, gone: &FxHashSet<(i32, i32)>) -> Occlusion {
let mut fresh = Occlusion::new(0, 6);
for (&(x, z), column) in &o.columns.map {
if !gone.contains(&(x, z)) {
for (y, &vis) in column.iter().enumerate() {
fresh.set((x, y as i32, z), vis);
}
}
}
fresh
}
let camera = at((0, 2, 0));
let mut o = Occlusion::new(0, 6);
random_world(&mut o, 0x51ed_2701, 900, 6);
o.settle(camera, 6);
let some: FxHashSet<(i32, i32)> = (-6..=6)
.flat_map(|x| (-1..=1).map(move |z| (x, z)))
.collect();
let mut expected = without(&o, &some);
o.remove_columns(&some);
o.settle(camera, 6);
expected.settle(camera, 6);
let graph = |o: &Occlusion| o.graph.as_ref().expect("searched").directions.clone();
assert_eq!(graph(&o), graph(&expected));
let all: FxHashSet<(i32, i32)> = o.columns.map.keys().copied().collect();
o.remove_columns(&all);
assert!(o.contains((40, 3, 40)), "no graph from the old view culls");
random_world(&mut o, 0x2545_f491, 900, 6);
let mut arrived = Occlusion::new(0, 6);
random_world(&mut arrived, 0x2545_f491, 900, 6);
o.settle(camera, 6);
arrived.settle(camera, 6);
assert_eq!(graph(&o), graph(&arrived));
}
/// The worker's graph is the one the render thread would compute.
#[test]
fn the_worker_matches_a_search_on_the_render_thread() {
@@ -147,6 +147,17 @@ impl<T> Resident<T> {
/// Removes every section of the `gone` columns, the others keeping their order: one
/// pass over the store, where `remove` moves a slot and looks its column up for each.
pub fn remove_columns(&mut self, gone: &FxHashSet<(i32, i32)>) -> Vec<T> {
// Every column (a far teleport): emptied, without a lookup per section.
if self.columns.iter().all(|c| gone.contains(&(c.x, c.z))) {
self.bounds.clear();
self.slots.clear();
self.columns.clear();
self.column_index.clear();
return std::mem::take(&mut self.items)
.into_iter()
.map(|(_, value)| value)
.collect();
}
let items = std::mem::take(&mut self.items);
let bounds = std::mem::take(&mut self.bounds);
self.slots.clear();
@@ -533,7 +544,18 @@ mod tests {
((seed >> 4) % 6) as i32 - 1,
((seed >> 8) % 5) as i32 - 2,
);
if (seed >> 12).is_multiple_of(97) {
if (seed >> 12).is_multiple_of(389) {
// Every column at once (a far teleport), and one that holds nothing.
let gone: FxHashSet<(i32, i32)> = (-2..=3)
.flat_map(|x| (-2..=2).map(move |z| (x, z)))
.collect();
let mut removed = r.remove_columns(&gone);
let mut expected: Vec<u32> = model.drain().map(|(_, v)| v).collect();
removed.sort_unstable();
expected.sort_unstable();
assert_eq!(removed, expected);
assert!(r.columns.is_empty() && r.column_index.is_empty());
} else if (seed >> 12).is_multiple_of(97) {
// Whole columns at once: the column of `key` and the next one along x.
let gone: FxHashSet<(i32, i32)> =
[(key.0, key.2), (key.0 + 1, key.2)].into_iter().collect();
+2 -5
View File
@@ -933,10 +933,7 @@ impl Graphics {
// and render threads are raised above four cores, where vanilla
// raises its server thread). Leaving two idled a core of a 4-thread
// laptop while a world loaded: a teleport there took ~15% longer.
let auto = std::thread::available_parallelism()
.map_or(2, |n| n.get())
.saturating_sub(1)
.max(1);
let auto = horizon_threads::worker_count().saturating_sub(1).max(1);
if self.battery { auto.div_ceil(2) } else { auto }
}
/// The frame cap battery mode adds: 60 in a world, 30 in menus without a
@@ -1294,7 +1291,7 @@ mod tests {
fn automatic_workers_leave_a_core_as_vanilla_does() {
let g = Graphics::default();
assert_eq!(g.workers, 0);
let cores = std::thread::available_parallelism().map_or(2, |n| n.get());
let cores = horizon_threads::worker_count();
assert_eq!(g.worker_threads(), cores.saturating_sub(1).max(1));
// A manual count is the player's own choice, kept as asked.
let manual = Graphics { workers: 3, ..g };
+1 -1
View File
@@ -1114,7 +1114,7 @@ impl Lod {
let keeps_observed = source
.as_ref()
.is_some_and(|source| source.keeps_observed());
let cores = thread::available_parallelism().map_or(1, |n| n.get());
let cores = horizon_threads::worker_count();
let shared = Arc::new(Shared {
jobs: Mutex::new(VecDeque::new()),
mesh_jobs: Mutex::new(std::collections::BinaryHeap::new()),
+1 -3
View File
@@ -7,9 +7,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
/// Threads take the next item as they finish one, so a run of slow items
/// doesn't hold back the others.
pub fn par_map<T: Sync, R: Send>(items: &[T], f: impl Fn(&T) -> R + Sync) -> Vec<R> {
let threads = std::thread::available_parallelism()
.map_or(1, |n| n.get())
.min(items.len());
let threads = horizon_threads::worker_count().min(items.len());
if threads <= 1 {
return items.iter().map(f).collect();
}
+58 -1
View File
@@ -1098,6 +1098,43 @@ pub struct ParticleEngine {
const MAX_PARTICLES: usize = 16384;
/// `ParticleLimit.SPORE_BLOSSOM`, the only limited particle.
const SPORE_BLOSSOM_LIMIT: usize = 1000;
const PARTICLE_LIGHT_CACHE_SIZE: usize = 64;
#[derive(Clone, Copy)]
struct CachedParticleLight {
block: [i32; 3],
packed: u8,
}
struct ParticleLightCache {
entries: [Option<CachedParticleLight>; PARTICLE_LIGHT_CACHE_SIZE],
}
impl Default for ParticleLightCache {
fn default() -> Self {
Self {
entries: [None; PARTICLE_LIGHT_CACHE_SIZE],
}
}
}
impl ParticleLightCache {
fn get(&mut self, block: [i32; 3], light_at: &dyn Fn([i32; 3]) -> u8) -> u8 {
let hash = (block[0] as u32).wrapping_mul(73_856_093)
^ (block[1] as u32).wrapping_mul(19_349_663)
^ (block[2] as u32).wrapping_mul(83_492_791);
let slot = hash as usize & (PARTICLE_LIGHT_CACHE_SIZE - 1);
if let Some(entry) = self.entries[slot]
&& entry.block == block
{
return entry.packed;
}
let packed = light_at(block);
self.entries[slot] = Some(CachedParticleLight { block, packed });
packed
}
}
impl ParticleEngine {
pub fn new(root: &Path, atlas: &HashMap<String, Rect>, translucent: &HashSet<String>) -> Self {
let mut engine = Self {
@@ -4156,6 +4193,7 @@ impl ParticleEngine {
// Camera.setRotation: rotationYXZ(PI - yRot, -xRot, 0).
let base = Quat::from_rotation_y(std::f32::consts::PI - yaw.to_radians())
* Quat::from_rotation_x(-pitch.to_radians());
let mut light_cache = ParticleLightCache::default();
for p in &self.particles {
if p.no_render {
continue;
@@ -4255,7 +4293,7 @@ impl ParticleEngine {
_ => {}
}
let block = ParticleLevel::block_at(p.position);
let packed = light_at(block);
let packed = light_cache.get(block, light_at);
let (sky_light, block_light) = p.light_coords(
partial,
u32::from(packed >> 4) * 16,
@@ -4366,6 +4404,25 @@ const CREEPER: [[f64; 2]; 12] = [
mod tests {
use super::*;
#[test]
fn particle_light_cache_reuses_exact_blocks_and_fails_open_on_collisions() {
let reads = std::cell::Cell::new(0);
let light_at = |block: [i32; 3]| {
reads.set(reads.get() + 1);
block[0] as u8
};
let mut cache = ParticleLightCache::default();
let first = [0, 0, 0];
let colliding = [64, 0, 0];
assert_eq!(cache.get(first, &light_at), 0);
assert_eq!(cache.get(first, &light_at), 0);
assert_eq!(reads.get(), 1);
assert_eq!(cache.get(colliding, &light_at), 64);
assert_eq!(cache.get(first, &light_at), 0);
assert_eq!(reads.get(), 3);
}
#[test]
fn a_mod_particles_json_becomes_its_sprite_set() {
let dir =
+11 -1
View File
@@ -61,6 +61,7 @@ fn cpu_if_enabled() -> Option<Duration> {
/// Run `f`, recording its duration under `name` when spans are enabled.
#[inline]
pub fn time<T>(name: &'static str, f: impl FnOnce() -> T) -> T {
let _heap = horizon_threads::heap_tag(name);
if !recording_here() {
return f();
}
@@ -291,9 +292,15 @@ pub mod startup {
pub struct Step {
name: &'static str,
started: Instant,
traced: bool,
/// A heap-census build's tag for the step's allocations.
_heap: Option<horizon_threads::heap_census::Tag>,
}
impl Drop for Step {
fn drop(&mut self) {
if !self.traced {
return;
}
let end = Instant::now();
write(format_args!(
r#""ph":"X","name":"{}","ts":{},"dur":{}"#,
@@ -307,9 +314,12 @@ pub mod startup {
/// Starts a step named `name` (a literal: no quotes or backslashes).
#[inline]
pub fn step(name: &'static str) -> Option<Step> {
on().then(|| Step {
let traced = on();
(traced || horizon_threads::HEAP_CENSUS).then(|| Step {
name,
started: Instant::now(),
traced,
_heap: horizon_threads::heap_tag(name),
})
}
+111 -17
View File
@@ -774,12 +774,23 @@ impl<G: ChunkGenerator> Source for Jobs<G> {
};
(kind, crate::spans::thread_cpu().unwrap_or_default())
});
let _tag = horizon_threads::heap_tag(match &job.job {
Job::Generate(..) => "stream.generate",
Job::Decorate(..) => "stream.decorate",
Job::Finish(..) => "stream.finish",
Job::Light(..) => "stream.light",
Job::Mesh(..) => "stream.mesh",
_ => "stream.other",
});
let done = self.locals.with(
worker,
|| WorkerLocal {
worker: self.generator.worker(),
faces: None,
mesh_scratch: MeshScratch::default(),
|| {
let _tag = horizon_threads::heap_tag("stream.worker_state");
WorkerLocal {
worker: self.generator.worker(),
faces: None,
mesh_scratch: MeshScratch::default(),
}
},
|local| self.run_job(job.job, local),
);
@@ -1125,20 +1136,39 @@ impl Streaming {
self.submit(None, 0, Job::Free(meshes));
}
}
/// A column the server unloaded (ClientboundForgetLevelChunkPacket).
pub fn forget(&mut self, world: &mut World, x: i32, z: i32) -> Option<Arc<Chunk>> {
self.lit.remove(&(x, z));
self.meshed.remove(&(x, z));
self.lighting.remove(&(x, z));
// By key, not by a pass over every pending section: a moved or
// shrunk storage forgets thousands of columns in one frame.
for sy in 0..world.layout().sections() as i32 {
self.meshing.remove(&(x, sy, z));
self.compile_started.remove(&(x, sy, z));
self.waiting.remove(&(x, sy, z));
/// The columns the server unloaded (ClientboundForgetLevelChunkPacket),
/// often thousands at once (a teleport or a moved view): their pending
/// sections go through [`forget_pending_sections`]. Returns the chunks
/// they held.
pub fn forget_columns(
&mut self,
world: &mut World,
gone: &FxHashSet<(i32, i32)>,
) -> Vec<Arc<Chunk>> {
// Every column held (a far teleport): no pending section is left to keep, and a
// pass over them all (80,000 at render distance 32) took 1.7 ms of the frame.
if world.chunks.keys().all(|column| gone.contains(column)) {
self.meshing.clear();
self.compile_started.clear();
self.waiting.clear();
} else {
forget_pending_sections(
&mut self.meshing,
&mut self.compile_started,
&mut self.waiting,
gone,
world.layout().sections() as i32,
);
}
world.dirty_sections.take_column(x, z);
world.chunks.remove(&(x, z))
let mut chunks = Vec::with_capacity(gone.len());
for &(x, z) in gone {
self.lit.remove(&(x, z));
self.meshed.remove(&(x, z));
self.lighting.remove(&(x, z));
world.dirty_sections.take_column(x, z);
chunks.extend(world.chunks.remove(&(x, z)));
}
chunks
}
/// Queue a job for the column `at`, nearest first: its distance (`distance`)
/// times four plus `bias`, which orders a column's jobs. A job without a
@@ -2750,6 +2780,34 @@ fn whole_relight_columns(
(columns, inline)
}
/// Drops the pending sections (`meshing`, `compile_started`, `waiting`) of
/// the `gone` columns: one pass over each store when that is cheaper than a
/// lookup per section of every column (a far teleport at render distance 32
/// forgets ~3,700 columns, 1.7 ms of lookups), else by key.
fn forget_pending_sections(
meshing: &mut FxHashMap<(i32, i32, i32), u64>,
compile_started: &mut FxHashMap<(i32, i32, i32), u64>,
waiting: &mut FxHashSet<(i32, i32, i32)>,
gone: &FxHashSet<(i32, i32)>,
sections: i32,
) {
let lookups = gone.len().saturating_mul(sections.max(0) as usize);
if lookups > meshing.len() + compile_started.len() + waiting.len() {
let kept = |&(x, _, z): &(i32, i32, i32)| !gone.contains(&(x, z));
meshing.retain(|k, _| kept(k));
compile_started.retain(|k, _| kept(k));
waiting.retain(kept);
} else {
for &(x, z) in gone {
for sy in 0..sections {
meshing.remove(&(x, sy, z));
compile_started.remove(&(x, sy, z));
waiting.remove(&(x, sy, z));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -2897,6 +2955,42 @@ mod tests {
assert!(waiting.elapsed() >= std::time::Duration::from_millis(10));
}
/// Whether the forgotten columns are few (looked up by key) or many
/// (one pass over each store), exactly their pending sections go.
#[test]
fn forgotten_columns_lose_their_pending_sections_either_way() {
let pending: Vec<(i32, i32, i32)> = (0..4)
.flat_map(|x| (0..3).map(move |sy| (x, sy, -x)))
.collect();
for gone in [
vec![(1, -1)],
(0..400)
.map(|x| (x, -x))
.filter(|&c| c != (2, -2))
.collect(),
] {
let gone: FxHashSet<(i32, i32)> = gone.into_iter().collect();
let mut meshing: FxHashMap<_, u64> = pending.iter().map(|&k| (k, 1)).collect();
let mut started = meshing.clone();
let mut waiting: FxHashSet<_> = pending.iter().copied().collect();
forget_pending_sections(&mut meshing, &mut started, &mut waiting, &gone, 3);
let mut left: Vec<_> = waiting.into_iter().collect();
left.sort();
let expected: Vec<_> = pending
.iter()
.copied()
.filter(|&(x, _, z)| !gone.contains(&(x, z)))
.collect();
assert_eq!(left, expected, "{} gone", gone.len());
let mut left: Vec<_> = meshing.into_keys().collect();
left.sort();
assert_eq!(left, expected);
let mut left: Vec<_> = started.into_keys().collect();
left.sort();
assert_eq!(left, expected);
}
}
/// Edits in two neighbouring columns light those and the ten around
/// them again; a column next to one that can't be lit whole keeps its
/// edits in the frame.
+3
View File
@@ -117,6 +117,9 @@ rng-trace = ["horizon-core/rng-trace", "horizon-client-lib/rng-trace"]
# Rust's allocations go to the system heap instead of mimalloc, where heap profilers that replace
# malloc (heaptrack, valgrind's massif) see them. For profiling only: mimalloc is faster.
system-allocator = []
# mimalloc with a tag on each block: `horizon-ctl memory` lists the live heap by startup step and
# thread (`heap_by_tag_mb`). For measuring only: each block costs 16 bytes more.
alloc-census = ["horizon-threads/alloc-census"]
# Compatibility with earlier experiment commands: main already uses mimalloc.
windows-mimalloc = []
# Opt-in Windows heap experiment; see docs/windows-allocators.md.
@@ -221,7 +221,7 @@ pub const COMMANDS: &[Cmd] = &[
"Replies protocol (MAJOR.MINOR.PATCH of the control protocol, docs/api/stability.md), release (the game's version, the string MCP serverInfo carries too) and features (the command groups the game answers, so a script can test for one before calling it).",
["horizon-ctl hello"], &[], mcp: true, local: false),
cmd!("memory", Session, "memory", "what holds the game's memory: resident set, allocator, client and server columns by part, renderer, GPU memory by resource",
"Columns count blocks, biomes, light, heightmaps and records, each shared section once; server_columns are the single-player server thread's own levels. GPU memory by label needs Vulkan or DirectX 12. What the census doesn't reach is the resident set less its parts.", ["horizon-ctl memory"], &[], mcp: true, local: false),
"Columns count blocks, biomes, light, heightmaps and records, each shared section once; server_columns are the single-player server thread's own levels. GPU memory by label needs Vulkan or DirectX 12. What the census doesn't reach is the resident set less its parts. A build with the alloc-census feature adds heap_by_tag_mb: the live heap by startup step, timed span, chunk job and thread.", ["horizon-ctl memory"], &[], mcp: true, local: false),
cmd!("worlds", Session, "worlds", "the saved worlds, newest first: folder, name, mode, last played, version",
"folder is what open, world-rename, world-backup and world-delete take.", ["horizon-ctl worlds"], &[], mcp: true, local: false),
cmd!("world-rename", Session, "world-rename FOLDER NAME", "rename a world, as Edit World does (its folder stays)",
+89 -3
View File
@@ -519,8 +519,10 @@ pub fn mob_to_nbt(
}
}
let species = m.species();
// MushroomCow saves its variant under `Type` (below), not `variant`.
if species != crate::mobs::Species::Mooshroom
// `VariantUtils.writeVariant`: only the species that store the name. A
// mooshroom saves `Type` (below), a fox `Type`, a horse and a llama a
// packed `Variant` int, so those report the component but save no name.
if crate::mobs::finalize::saves_variant_name(species)
&& let Some((_, variant)) =
crate::mobs::finalize::species_variant_component(species, m.extra.variant)
{
@@ -1643,7 +1645,7 @@ pub fn mob_from_nbt(
m.extra.vector[0] = 0.7;
}
}
if crate::mobs::finalize::species_variant_component(m.species(), 0).is_some()
if crate::mobs::finalize::saves_variant_name(m.species())
&& let Some(v) = text(tag.get("variant"))
{
m.extra.variant = crate::mobs::finalize::variant_index(m.species(), &v);
@@ -2701,6 +2703,7 @@ pub struct TridentSave<'a> {
pub shake: u32,
pub dealt_damage: bool,
pub creative_only: bool,
pub no_gravity: bool,
pub item: &'a crate::items::ItemStack,
}
pub fn trident_to_nbt(t: &TridentSave, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
@@ -2730,6 +2733,12 @@ pub fn trident_to_nbt(t: &TridentSave, uuid: [i32; 4], raw: Option<&Tag>) -> Tag
] {
put(&mut tag, key, value);
}
// Entity.saveWithoutId writes NoGravity only when set.
if t.no_gravity {
put(&mut tag, "NoGravity", Tag::Byte(1));
} else if let Tag::Compound(map) = &mut tag {
map.remove(&"NoGravity".into());
}
tag
}
/// (position, motion, yaw, pitch, in ground, life, shake, dealt damage,
@@ -2744,6 +2753,7 @@ pub struct TridentNbt {
pub shake: u32,
pub dealt_damage: bool,
pub creative_only: bool,
pub no_gravity: bool,
pub item: crate::items::ItemStack,
}
pub fn trident_from_nbt(tag: &Tag) -> Option<TridentNbt> {
@@ -2758,6 +2768,7 @@ pub fn trident_from_nbt(tag: &Tag) -> Option<TridentNbt> {
shake: number(tag.get("shake")).unwrap_or(0).max(0) as u32,
dealt_damage: flag(tag, "DealtDamage"),
creative_only: number(tag.get("pickup")) == Some(2),
no_gravity: flag(tag, "NoGravity"),
item: tag
.get("item")
.and_then(item_from_nbt)
@@ -3443,12 +3454,16 @@ mod tests {
shake: 3,
dealt_damage: true,
creative_only: true,
no_gravity: true,
item: &trident,
},
[1, 2, 3, 4],
None,
);
let t = trident_from_nbt(&tag).unwrap();
// `Entity.saveWithoutId` writes NoGravity only when set.
assert!(t.no_gravity);
assert!(matches!(tag.get("NoGravity"), Some(Tag::Byte(1))));
assert_eq!((t.in_ground, t.life, t.shake), (true, 9, 3));
assert_eq!((t.dealt_damage, t.creative_only), (true, true));
assert_eq!((t.yaw, t.pitch), (10.0, 20.0));
@@ -3956,6 +3971,14 @@ pub fn apply_item_components(m: &mut Mob, stack: &crate::items::ItemStack) {
// alone, so a spawn egg carrying one hatches that variant.
crate::mobs::Species::Fox => (Some("minecraft:fox/variant"), None),
crate::mobs::Species::Frog => (Some("minecraft:frog/variant"), None),
// Horse.applyImplicitComponent: `setVariant` sets the coat colour and
// keeps the markings the spawn picked (`getTypeVariant() & -256`),
// which Horizon keeps in `extra.variant2` and leaves alone here.
crate::mobs::Species::Horse => (Some("minecraft:horse/variant"), None),
// Llama.applyImplicitComponent, which a trader llama inherits.
crate::mobs::Species::Llama | crate::mobs::Species::TraderLlama => {
(Some("minecraft:llama/variant"), None)
}
_ => (None, None),
};
if let Some(name) = variant.and_then(|id| text(components.other(id))) {
@@ -4855,6 +4878,69 @@ mod item_component_tests {
assert_eq!(fox.extra.variant, 0, "unknown names read as red");
}
/// `Horse.applyImplicitComponent` and `Llama.applyImplicitComponent`: the
/// `.../variant` component sets the coat colour. A horse's markings are
/// the other half of the packed variant its spawn picked and stay.
#[test]
fn a_horse_or_llama_spawn_egg_picks_its_variant_component() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client")
.join(horizon_core::VANILLA_VERSION);
if !root.join("items.json").exists() {
return;
}
let data = crate::mobs::MobTypes::load(&root).unwrap();
let spawn = |kind: &str, id: u32| {
Mob::new(
id,
Arc::new(data.types[kind].clone()),
Vec3::new(0.0, 4.0, 0.0),
0.0,
)
};
// A black horse (Horse.Variant.BLACK, 4) with a white field (markings
// 2), as a spawn in a plains leaves it.
let mut horse = spawn("minecraft:horse", 1);
(horse.extra.variant, horse.extra.variant2) = (4, 2);
let mut egg = crate::items::ItemStack::new("minecraft:horse_spawn_egg", 1);
egg.data_mut().set_other(
"minecraft:horse/variant",
Some(string("minecraft:dark_brown")),
);
apply_item_components(&mut horse, &egg);
assert_eq!(horse.extra.variant, 6, "Horse.Variant.DARK_BROWN");
assert_eq!(
horse.extra.variant2, 2,
"the markings the spawn picked stay"
);
// `Horse.get(DataComponents.HORSE_VARIANT)` reports it back.
let (id, variant) = crate::mobs::finalize::variant_component(&horse).unwrap();
assert_eq!(
(id, variant.as_str()),
("minecraft:horse/variant", "minecraft:dark_brown")
);
// The save keeps the packed `Variant` int and gains no `variant` name,
// which only `VariantUtils.writeVariant` species store.
let tag = mob_to_nbt(&horse, [1, 2, 3, 4], None, None);
assert_eq!(tag.get("variant"), None);
assert!(
matches!(tag.get("Variant"), Some(Tag::Int(v)) if *v == 6 | 2 << 8),
"colour 6 and markings 2 packed"
);
// A llama, as a spawn in a plains leaves it: creamy.
let mut egg = crate::items::ItemStack::new("minecraft:llama_spawn_egg", 1);
egg.data_mut()
.set_other("minecraft:llama/variant", Some(string("minecraft:gray")));
let mut llama = spawn("minecraft:llama", 2);
assert_eq!(llama.extra.variant, 0);
apply_item_components(&mut llama, &egg);
assert_eq!(llama.extra.variant, 3, "Llama.Variant.GRAY");
// TraderLlama inherits the llama's arm.
let mut trader = spawn("minecraft:trader_llama", 3);
apply_item_components(&mut trader, &egg);
assert_eq!(trader.extra.variant, 3, "the trader llama reads it too");
}
/// `EntityType.updateCustomEntityTag`: the egg's `entity_data` on the mob it
/// spawned, when the type is its own — the mob's compound, the data merged
/// over it and loaded back, as the spawn egg site does it.
+81 -11
View File
@@ -2399,6 +2399,7 @@ struct Game<'a> {
/// `horizon-ctl gpu-passes`: time Horizon's own passes on the GPU with F3 shut.
gpu_passes: bool,
screen: Screen,
observed_screen: Option<native_hooks::ObservedScreen>,
window: Option<Arc<Window>>,
renderer: Option<Renderer>,
/// Avoid an endless rebuild loop if the new device is lost as well.
@@ -2708,6 +2709,7 @@ struct Game<'a> {
waypoints: horizon_client::locator_bar::ClientWaypoints,
/// The camera the last frame drew from (position, yaw, pitch, fov), for the locator bar.
locator_camera: Option<([f64; 3], f32, f32, f32)>,
native_camera_frame: Option<horizon_client::mod_api::NativeCamera>,
/// `LocalPlayer.postEffects`: the chains the server has the screen run
/// (`ClientboundPostEffectsPacket`), in order.
post_effects: Vec<String>,
@@ -6013,9 +6015,8 @@ impl Game<'_> {
if entity && hand == Hand::Off && self.with_hand(hand, |g| g.interact_entity(None))? {
return Ok(Some(hand));
}
// A client of a server never hits a fence knot with its crosshair:
// `LeadItem.useOn` and `LeashFenceKnotEntity.interact` reach the
// server as a click on the fence block.
// A click on the fence block: `LeadItem.useOn`. A client whose crosshair
// met the knot first sends Interact with the knot's id instead.
if on_block && self.with_hand(hand, Game::interact_knot) {
return Ok(Some(hand));
}
@@ -6061,8 +6062,7 @@ impl Game<'_> {
|| self.interact_boat(target)
|| self.interact_cart(target)
|| self.interact_mob(target)?
// Knots are reached only through the crosshair pick for now: a
// guest's Interact naming a knot's id is a known gap.
// A guest's Interact naming a knot's id goes to `use_knot` before this.
|| target.is_none() && self.interact_knot())
}
/// `ItemStack.applyAfterUseComponentSideEffects`: a used stack with a
@@ -10810,10 +10810,18 @@ impl Game<'_> {
);
}
// Mod block entities: their mods draw them (`native_block_entity_submit`),
// culled by the bounds they give.
let mods = r
.visible_block_entities(self.settings.chunk_fade)
.filter(|(_, state)| !self.res.block_entity_render().renders(*state));
// culled by the bounds they give. A world without any (`has_mods`) has none
// to find among its other unrendered block entities (furnaces, hoppers...).
let mods = self
.world
.block_entities
.has_mods()
.then(|| {
r.visible_block_entities(self.settings.chunk_fade)
.filter(|(_, state)| !self.res.block_entity_render().renders(*state))
})
.into_iter()
.flatten();
let untemplated = self.untemplated_mod_block_entities(camera);
self.mod_block_entity_geometry(
mods.chain(untemplated.iter()),
@@ -12891,7 +12899,17 @@ impl Game<'_> {
let target = self.bucket_target(content, &hit);
let mut canceled = false;
self.native_fill_bucket(stack, target, &mut canceled);
if canceled || self.bucket_empty_cancelled(stack, target) {
// `mayUseItemAt(blockPos2)`: a block that refuses the fluid passes
// it to the block beside, so a mod hears that block too (powder
// snow places like a block and never goes there).
let beside_too = content != BucketContent::Powder && hit.beside() != target;
if !canceled && beside_too {
self.native_fill_bucket(stack, hit.beside(), &mut canceled);
}
if canceled
|| self.bucket_empty_cancelled(stack, target)
|| beside_too && self.bucket_empty_cancelled(stack, hit.beside())
{
return Ok(UseResult::Fail);
}
let pos = BlockPos::new(target[0], target[1], target[2]);
@@ -13580,6 +13598,29 @@ impl Game<'_> {
if canceled {
continue;
}
let (mut x, mut y, mut z, mut pitch, mut yaw, mut roll, mut pose_scale) =
(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0);
self.native_render_hand_pose(
hand,
item.as_ref(),
a,
attack,
inverse_height,
&mut x,
&mut y,
&mut z,
&mut pitch,
&mut yaw,
&mut roll,
&mut pose_scale,
);
// No mod moved the hand (always so without one): no matrices to build.
let pose = [x, y, z, pitch, yaw, roll, pose_scale];
let to_world = if pose == [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0] {
to_world
} else {
to_world * native_hooks::hand_pose_matrix(pose)
};
// The main hand on its arm, the off hand on the other one.
let left = self.settings.main_hand_left != (hand == Hand::Off);
let Some(stack) = item else {
@@ -14307,7 +14348,7 @@ impl Game<'_> {
let lines = vec![(horizon_client::text::Text::literal(text), u32::MAX)];
let (x, y, w, h) =
Self::tooltip_geometry(atlas, &lines, (x, y), (batch.width, batch.height), [0.0; 2]);
Self::tooltip_box(batch, atlas, x, y, w, h);
self.tooltip_box(batch, None, x, y, w, h);
Self::tooltip_text(batch, atlas, &lines, x, y);
}
/// `CreativeModeInventoryScreen.getTooltipFromContainerItem`: the lines
@@ -15006,6 +15047,7 @@ impl Game<'_> {
if self.renderer.as_mut().is_some_and(|r| r.display_changed()) {
self.apply_ui_settings();
}
self.observe_screen();
// Minecraft.doWorldLoad: the LevelLoadingScreen while single player's
// server starts, with the server's chunk map and progress.
if let Some(starting) = &self.integrated_starting {
@@ -15348,6 +15390,32 @@ impl Game<'_> {
let distance = (terrain_distance * 16) as f32;
let depth_far =
horizon_client::camera::depth_far(terrain_distance, self.settings.cloud_range);
// Only native mods read it: no matrices or allocation per frame without one.
let mods_read_camera = !horizon_client::mod_api::mods().is_empty();
self.native_camera_frame = (mods_read_camera && !title).then(|| {
let partial = camera_alpha as f32;
horizon_client::mod_api::NativeCamera {
position: camera.to_array(),
eye_height: self.view.eye_height_old
+ (self.view.eye_height - self.view.eye_height_old) * partial,
partial_tick: partial,
yaw,
pitch,
fov_degrees,
near: 0.05,
far: depth_far,
view: (bob * spin * horizon_client::camera::view_matrix(yaw, pitch))
.to_cols_array(),
projection: glam::Mat4::perspective_rh(
fov_degrees.to_radians(),
size[0] as f32 / size[1].max(1) as f32,
depth_far,
0.05,
)
.to_cols_array(),
dimension: self.dim.id().to_string(),
}
});
self.view_frustum = self
.renderer
.as_ref()
@@ -18281,6 +18349,7 @@ impl<'a> Game<'a> {
debug: false,
gpu_passes: false,
screen: Screen::Playing,
observed_screen: None,
window: None,
renderer: None,
last_device_recovery: None,
@@ -18458,6 +18527,7 @@ impl<'a> Game<'a> {
waypoint_styles: prepared.waypoint_styles,
waypoints: Default::default(),
locator_camera: None,
native_camera_frame: None,
post_effects: Vec::new(),
spectated_post_effect: None,
experience_display: Default::default(),
@@ -69,6 +69,8 @@ pub(super) enum Target {
Falling(u32),
/// A firework rocket, by its entity id (`minecraft:firework_rocket`).
Rocket(u32),
/// A thrown trident, by its entity id (`minecraft:trident`).
Trident(u32),
/// An ominous item spawner, by its entity id (`minecraft:ominous_item_spawner`).
Spawner(u32),
}
@@ -152,6 +154,7 @@ fn non_player_entity_id(t: Target) -> Option<i32> {
| Target::Tnt(id)
| Target::Falling(id)
| Target::Rocket(id)
| Target::Trident(id)
| Target::Spawner(id)) = t
else {
return None;
@@ -549,6 +552,11 @@ impl Game<'_> {
.iter()
.find(|r| r.id == id)
.map(|r| [r.position.x, r.position.y, r.position.z]),
Target::Trident(id) => self
.tridents
.iter()
.find(|t| t.id == id)
.map(|t| [t.position.x, t.position.y, t.position.z]),
Target::Spawner(id) => self
.ominous_spawners
.iter()
@@ -603,6 +611,7 @@ impl Game<'_> {
Target::Tnt(_) => "minecraft:tnt".into(),
Target::Falling(_) => "minecraft:falling_block".into(),
Target::Rocket(_) => "minecraft:firework_rocket".into(),
Target::Trident(_) => "minecraft:trident".into(),
Target::Spawner(_) => "minecraft:ominous_item_spawner".into(),
}
}
@@ -905,6 +914,7 @@ impl Game<'_> {
Target::Tnt(id),
Target::Falling(id),
Target::Rocket(id),
Target::Trident(id),
Target::Spawner(id),
]
.into_iter()
@@ -930,6 +940,7 @@ impl Game<'_> {
Target::Tnt(id) => self.tnts.iter().any(|t| t.id == id),
Target::Falling(id) => self.falling.iter().any(|f| f.id == id),
Target::Rocket(id) => self.rockets.iter().any(|r| r.id == id),
Target::Trident(id) => self.tridents.iter().any(|t| t.id == id),
Target::Spawner(id) => self.ominous_spawners.iter().any(|s| s.id == id),
}
}
@@ -1600,6 +1611,12 @@ impl Game<'_> {
r.previous = v;
}
}
Target::Trident(id) => {
if let Some(t) = self.tridents.iter_mut().find(|t| t.id == id) {
t.position = v;
t.previous = v;
}
}
Target::Spawner(id) => {
if let Some(s) = self.ominous_spawners.iter_mut().find(|s| s.id == id) {
s.position = [v.x, v.y, v.z];
@@ -2102,6 +2119,7 @@ impl Game<'_> {
Target::Tnt(id) => self.tnts.retain(|t| t.id != id),
Target::Falling(id) => self.falling.retain(|f| f.id != id),
Target::Rocket(id) => self.rockets.retain(|r| r.id != id),
Target::Trident(id) => self.tridents.retain(|t| t.id != id),
Target::Spawner(id) => self.ominous_spawners.retain(|s| s.id != id),
}
// The orbs marked for removal are gone (Entity.kill removes them).
@@ -2242,6 +2260,10 @@ impl Game<'_> {
beam: None,
invulnerable: false,
});
// tryAddFreshEntityWithPassengers refused: ERROR_DUPLICATE_UUID.
if !self.native_joins_flush(false, self.next_entity) {
return Err(err("commands.summon.failed.uuid", &[]));
}
name = Text::translate("entity.minecraft.end_crystal", &[]);
}
k if super::minecarts::CartKind::from_id(&kind).is_some() => {
@@ -3841,6 +3863,7 @@ mod look_at_tests {
Target::Tnt(7),
Target::Falling(7),
Target::Rocket(7),
Target::Trident(7),
Target::Spawner(7),
] {
assert_eq!(non_player_entity_id(t), Some(7), "{t:?}");
@@ -224,6 +224,11 @@ impl Game<'_> {
let r = self.rockets.iter().find(|r| r.id == id)?;
r.to_nbt(uuid, player_uuid, self.persist.raw.get(&id))
}
Target::Trident(id) => {
let uuid = self.uuid_for(id);
let t = self.tridents.iter().find(|t| t.id == id)?;
t.to_nbt(uuid, self.persist.raw.get(&id))
}
Target::Spawner(id) => {
let s = self.ominous_spawners.iter().find(|s| s.id == id)?;
let [x, y, z] = s.position;
@@ -856,6 +861,8 @@ impl Game<'_> {
Some(Target::Falling(id))
} else if self.rockets.iter().any(|r| r.id == id) {
Some(Target::Rocket(id))
} else if self.tridents.iter().any(|t| t.id == id) {
Some(Target::Trident(id))
} else if self.ominous_spawners.iter().any(|s| s.id == id) {
Some(Target::Spawner(id))
} else {
+14 -1
View File
@@ -402,6 +402,19 @@ impl Game<'_> {
at: [f64; 3],
persistent: bool,
scripted: bool,
) -> Option<u32> {
self.spawn_mob_reason(kind, at, persistent, scripted, !persistent)
}
/// `spawn_mob_for` with the spawn reason's NATURAL flag explicit: only a
/// natural spawn rolls the jockey mounts of zombie horses, husks and
/// drowned (`reinforcement` and spawner spawns pass `false`).
pub(super) fn spawn_mob_reason(
&mut self,
kind: &str,
at: [f64; 3],
persistent: bool,
scripted: bool,
natural: bool,
) -> Option<u32> {
let kind = if kind.contains(':') {
kind.to_owned()
@@ -437,7 +450,7 @@ impl Game<'_> {
self.enchant_spawn_equipment(id);
self.halloween_spawn_head(id);
if !persistent && !scripted {
self.spawn_jockeys(id, true);
self.spawn_jockeys(id, natural);
}
Some(id)
}
+3
View File
@@ -513,6 +513,9 @@ impl Game<'_> {
// `/summon ender_dragon` outside the End: no fight, but the dragon flies.
self.tick_fight();
}
// The spikes' crystals (placed above) join before they light a fire:
// a mod that cancels one keeps its fire out too.
self.native_joins_flush(false, 0);
self.tick_crystals();
self.tick_eyes();
if self.dim == Dim::End {
+16 -2
View File
@@ -996,8 +996,7 @@ impl Entity for ThrownTrident {
!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
Some(Target::Trident(self.id))
}
/// Like any arrow (`AbstractArrow.push`) not pushed while in the ground.
fn pushed_by_explosion(&self) -> bool {
@@ -1078,3 +1077,18 @@ mod no_gravity_stand_tests {
assert!(!stand.triggers_blocks());
}
}
#[cfg(test)]
mod trident_selector_tests {
use super::Entity as _;
use crate::game::{commands::Target, trident::ThrownTrident};
use horizon_core::geometry::Vec3;
/// `@e`, `/kill` and `/data` reach a thrown trident like any other entity
/// (`EntityTypeTest.forClass(Entity.class)`: every entity in the level).
#[test]
fn a_thrown_trident_is_a_selector_target() {
let t = ThrownTrident::remote(9, Vec3::new(0.5, 100.0, 0.5), 0.0, 0.0);
assert_eq!(t.selector_target(), Some(Target::Trident(9)));
}
}
+26 -3
View File
@@ -17,6 +17,14 @@ use horizon_client::{
/// `Abilities.walkingSpeed`.
const WALKING_SPEED: f32 = 0.1;
fn valid_fov_modifier(modifier: f32, fallback: f32) -> f32 {
if modifier.is_finite() && (0.01..=10.0).contains(&modifier) {
modifier
} else {
fallback
}
}
impl Game<'_> {
/// `Player.isScoping`: using a spyglass.
pub(super) fn scoping(&self) -> bool {
@@ -75,9 +83,12 @@ impl Game<'_> {
pub(super) fn tick_fov(&mut self) {
let first_person = self.camera_type.first_person();
let effect = self.fov_effect_scale();
let target = self
.fov_player()
.map_or(1.0, |p| field_of_view_modifier(&p, first_person, effect));
let target = self.fov_player().map_or(1.0, |player| {
let base = field_of_view_modifier(&player, first_person, effect);
let mut modifier = base;
self.native_compute_fov_modifier(&player, base, &mut modifier);
valid_fov_modifier(modifier, base)
});
let target = self.zoomed_fov_target(target);
// A server's spectator camera on a mob: its eye height. Its modifier
// stays 1 (another player's own modifier is not modelled here); the
@@ -219,3 +230,15 @@ impl Game<'_> {
hud_layout::paint(batch, &self.gui.atlas, &draws);
}
}
#[cfg(test)]
mod tests {
use super::valid_fov_modifier;
#[test]
fn modifier_validation() {
assert_eq!(valid_fov_modifier(0.5, 1.0), 0.5);
for value in [f32::NAN, f32::INFINITY, 0.0, 10.1] {
assert_eq!(valid_fov_modifier(value, 1.0), 1.0);
}
}
}
+7
View File
@@ -593,6 +593,13 @@ impl Game<'_> {
}
}
self.cull_entity_vertices(out, entity_start);
if let Some(stack) = &f.item {
let mut canceled = false;
self.native_render_item_in_frame(f.id, f.pos, stack, &mut canceled);
if canceled {
continue;
}
}
if let Some(id) = map {
// ItemFrameRenderer: the map fills the face, in 90-degree steps.
let depth = if f.invisible { 0.5 } else { 0.4375 };
@@ -2174,6 +2174,17 @@ impl Game<'_> {
{
return;
}
// LeashFenceKnotEntity.interact: a knot has no entity here, only its fence.
if let Some(knot) = self.knot_at_id(p.id) {
let hand = if p.hand == 1 { Hand::Off } else { Hand::Main };
let swung = self.as_guest(host, conn, |g| {
let swings = g.swings_started;
g.with_hand(hand, |g| g.use_knot(knot, true));
g.swings_started != swings
});
self.lan_swing_back(host, conn, swung);
return;
}
let before = host.hub.guests[gi].vehicle;
if self
.look_at_entity(host, gi, p.id, Some(p.location))
@@ -2958,6 +2969,15 @@ impl Game<'_> {
if let Some(b) = self.tracked_special_box(id) {
return Some(b);
}
if let Some(knot) = self.knot_at_id(id) {
let knot = horizon_client::mobs::leash::knot_state(knot);
let p = knot.position;
let half = knot.width / 2.0;
return Some(Aabb::new(
Vec3::new(p.x - half, p.y, p.z - half),
Vec3::new(p.x + half, p.y + knot.height, p.z + half),
));
}
self.creatures
.projectiles
.iter()
@@ -2965,6 +2985,14 @@ impl Game<'_> {
.map(|p| p.bounds())
}
/// The fence of the knot whose entity id is `id` (`lan_assign_ids`).
fn knot_at_id(&self, id: i32) -> Option<[i32; 3]> {
self.knot_ids
.iter()
.find(|&(_, &knot)| knot as i32 == id)
.map(|(&fence, _)| fence)
}
fn guest_attack(&mut self, host: &mut LanHost, conn: u32, id: i32) {
let Some(gi) = host.hub.guests.iter().position(|g| g.conn == conn) else {
return;
@@ -596,6 +596,12 @@ impl Game<'_> {
Some(g) => g.entity_id,
None => owner(p.owner),
};
// Entity.DATA_SHARED_FLAGS_ID's FLAG_ONFIRE: Entity.baseTick's
// `setSharedFlagOnFire(remainingFireTicks > 0)`, which a client's
// isOnFire reads to draw the flames (a Flame bow's arrow).
if p.arrow.as_ref().is_some_and(|a| a.on_fire()) {
e.meta.push((0, serializer::BYTE, Value::Byte(1)));
}
match &p.kind {
K::Arrow { critical, .. } => {
let a = p.arrow.as_deref();
+10 -23
View File
@@ -337,7 +337,7 @@ impl Game<'_> {
}
false
}
/// `LeadItem.useOn` and `LeashFenceKnotEntity.interact` on a fence.
/// `LeadItem.useOn` on the fence the crosshair hits.
pub(super) fn interact_knot(&mut self) -> bool {
let Some(hit) = self.pick() else {
return false;
@@ -355,7 +355,15 @@ impl Game<'_> {
{
return false;
}
let knot_pos = hit.pos;
// `FenceBlock.useWithoutItem` / `LeadItem.useOn` only tie the player's own mobs
// (`LeadItem.bindPlayerMobs`). A client whose crosshair met the knot sends Interact with
// the knot's id instead. Taking a mob back here would undo a tie: the client sends the
// click once for each hand.
self.use_knot(hit.pos, false)
}
/// A click at the fence at `knot_pos`: `knot_hit` for `LeashFenceKnotEntity.interact`,
/// otherwise `LeadItem.useOn`. Returns whether it was used.
pub(super) fn use_knot(&mut self, knot_pos: [i32; 3], knot_hit: bool) -> bool {
let knot = leash::knot_state(knot_pos).center;
let center = glam::DVec3::new(knot.x, knot.y, knot.z);
let at = [center.x, center.y, center.z];
@@ -363,27 +371,6 @@ impl Game<'_> {
.filter(|&i| self.mobs[i].extra.leash == Some(Holder::Knot(knot_pos)))
.collect();
let existing_boats = self.boats_on(Holder::Knot(knot_pos));
// `LeashFenceKnotEntity.interact` runs only when the ray meets the knot
// before the post: the knot's box (`recalculateBoundingBox`, 0.375 wide,
// y + 0.375 to y + 0.875) is wider than the post, so a hit point on the
// post inside that box went through the knot first. Any other click on
// the fence is `FenceBlock.useWithoutItem` / `LeadItem.useOn`, which only
// ties the player's own mobs (`LeadItem.bindPlayerMobs`).
let knot_hit = !(existing.is_empty() && existing_boats.is_empty()) && {
let box_ = leash::knot_state(knot_pos);
let half = box_.width / 2.0 + 1.0e-6;
(hit.point[0] - box_.position.x).abs() <= half
&& (hit.point[2] - box_.position.z).abs() <= half
&& hit.point[1] >= box_.position.y - 1.0e-6
&& hit.point[1] <= box_.position.y + box_.height + 1.0e-6
// The knot is an entity: a hit on it counts only within the
// entity interaction range (`filterHitResult`), shorter than
// the block range the fence was hit at.
&& self.eye_position().is_some_and(|eye| {
let d = [0, 1, 2].map(|i| hit.point[i] - eye[i]);
d[0] * d[0] + d[1] * d[1] + d[2] * d[2] <= self.entity_reach * self.entity_reach
})
};
let held_item = self.held().map(|s| s.item.to_string());
let sneaking = self.keys.contains(&winit::keyboard::KeyCode::ShiftLeft);
// Shears snip every lead on the knot.
+9
View File
@@ -141,6 +141,15 @@ impl Game<'_> {
json!({ "active": active, "cached": cached })
}),
"renderer": renderer,
// A build with the `alloc-census` feature: the live heap by the
// startup step, span or pool job that allocated it, else its thread.
"heap_by_tag_mb": horizon_threads::HEAP_CENSUS.then(|| {
horizon_threads::heap_census::census()
.into_iter()
.take(60)
.map(|(tag, bytes)| json!([tag, mb(bytes.max(0) as u64)]))
.collect::<Vec<_>>()
}),
})
}
}
@@ -489,4 +489,10 @@ impl Host for super::Game<'_> {
fn instance_dir(&self) -> PathBuf {
self.native_instance_dir()
}
fn camera(&self) -> Option<NativeCamera> {
self.native_camera()
}
fn block_collision(&self, pos: [i32; 3]) -> Vec<[f64; 6]> {
self.native_block_collision(pos)
}
}
@@ -117,6 +117,7 @@ fn target_id(t: Target) -> Option<u32> {
| Target::Tnt(id)
| Target::Falling(id)
| Target::Rocket(id)
| Target::Trident(id)
| Target::Spawner(id) => Some(id),
Target::Player | Target::Guest(_) => None,
}
@@ -343,6 +344,9 @@ impl Game<'_> {
self.persist.live.push((id, kind.into_owned(), true));
}
}
// Ids taken and never announced (the dragon's parts, another level's
// entities) must not keep the next flush walking.
self.persist.joined_mark = self.persist.joined_mark.max(self.next_entity);
kept
}
/// The entity with the lowest id above the last one announced.
+270 -1
View File
@@ -881,7 +881,7 @@ use super::{
native_panel_recipes::RecipePage,
};
use horizon_client::mod_api::{
self, NativeCommand, NativeDispensed, NativeLifecycleContext, NativeServerPlayer,
self, NativeCamera, NativeCommand, NativeDispensed, NativeLifecycleContext, NativeServerPlayer,
ServerTickPhase, WorldPhase,
};
use horizon_client::mod_api::{AttachmentResult, Attachments, Holder, Suggestion};
@@ -936,6 +936,106 @@ use std::{
};
use winit::{event::MouseButton, keyboard::KeyCode};
pub(super) struct ObservedScreen {
id: &'static str,
menu: Option<String>,
title: Option<Text>,
rect: [f32; 4],
slots: usize,
mouse: [f32; 2],
partial_tick: f32,
}
impl ObservedScreen {
fn view(&self) -> NativeScreenView<'_> {
NativeScreenView {
id: self.id,
menu: self.menu.as_deref(),
title: self.title.as_ref(),
rect: self.rect,
slots: self.slots,
mouse: self.mouse,
partial_tick: self.partial_tick,
}
}
}
fn observed_overlay_id(overlays: &[(bool, &'static str)]) -> Option<&'static str> {
overlays.iter().find_map(|(open, id)| open.then_some(*id))
}
impl Game<'_> {
fn observed_screen_view(&self) -> Option<NativeScreenView<'_>> {
let book = match &self.book {
Some(super::books::BookScreen::Edit { signing: true, .. }) => Some("book_sign"),
Some(super::books::BookScreen::Edit { .. }) => Some("book_edit"),
Some(super::books::BookScreen::View {
lectern: Some(_), ..
}) => Some("lectern"),
Some(super::books::BookScreen::View { .. }) => Some("book_view"),
None => None,
};
let overlay = observed_overlay_id(&[
(self.command_prompt.is_some(), "command_prompt"),
(self.link_prompt.is_some(), "link_prompt"),
(self.code_of_conduct.is_some(), "code_of_conduct"),
(self.server_packs.prompt.is_some(), "server_pack_prompt"),
(self.dialog_open(), "dialog"),
(book.is_some(), book.unwrap_or("book_view")),
(self.sign_edit.is_some(), "sign_editor"),
]);
let Some(id) = overlay else {
return self.screen_view_now();
};
let renderer = self.renderer.as_ref()?;
let batch = HudBatch::new([renderer.config.width, renderer.config.height]);
Some(NativeScreenView {
id,
menu: None,
title: None,
rect: [0.0, 0.0, batch.width, batch.height],
slots: 0,
mouse: self.native_cursor(),
partial_tick: self.clock.fraction() as f32,
})
}
pub(super) fn observe_screen(&mut self) {
// Free with no native mod: the snapshot clones the title and menu.
if mod_api::mods().is_empty() {
return;
}
let current = self.observed_screen_view().map(|view| ObservedScreen {
id: view.id,
menu: view.menu.map(str::to_owned),
title: view.title.cloned(),
rect: view.rect,
slots: view.slots,
mouse: view.mouse,
partial_tick: view.partial_tick,
});
let changed = match (&self.observed_screen, &current) {
(Some(old), Some(new)) => old.id != new.id || old.menu != new.menu,
(None, None) => false,
_ => true,
};
if changed {
if let Some(old) = &self.observed_screen {
self.native_screen_closing(&old.view());
}
if let Some(new) = &current {
self.native_screen_opening(&new.view());
self.native_screen_init_post(&new.view());
}
} else if let (Some(old), Some(new)) = (&self.observed_screen, &current)
&& old.rect != new.rect
{
self.native_screen_init_post(&new.view());
}
self.observed_screen = current;
}
}
/// `Level.setBlock`'s flags, for the `native_set_block_property` helpers:
/// `flags::UPDATE_CLIENTS` as a vanilla crop grows, `flags::UPDATE_ALL` as a
/// furnace sets its `lit`. Horizon sends every write to players, so
@@ -1647,6 +1747,27 @@ const SHAKE_MAX_DEGREES: f32 = 10.0;
/// The largest offset a shake gives, in blocks, along each axis.
const SHAKE_MAX_OFFSET: f32 = 0.5;
/// The pose a native mod asks for the first-person hand, each out-of-range
/// or non-finite component replaced by its identity.
pub(super) fn hand_pose_matrix(values: [f32; 7]) -> glam::Mat4 {
let valid = |value: f32, min: f32, max: f32, default: f32| {
if value.is_finite() && (min..=max).contains(&value) {
value
} else {
default
}
};
let [x, y, z, pitch, yaw, roll, scale] = values;
glam::Mat4::from_translation(glam::Vec3::new(
valid(x, -16.0, 16.0, 0.0),
valid(y, -16.0, 16.0, 0.0),
valid(z, -16.0, 16.0, 0.0),
)) * glam::Mat4::from_rotation_x(valid(pitch, -3600.0, 3600.0, 0.0).to_radians())
* glam::Mat4::from_rotation_y(valid(yaw, -3600.0, 3600.0, 0.0).to_radians())
* glam::Mat4::from_rotation_z(valid(roll, -3600.0, 3600.0, 0.0).to_radians())
* glam::Mat4::from_scale(glam::Vec3::splat(valid(scale, 0.01, 100.0, 1.0)))
}
/// `shake` scaled by `scale` (Distortion Effects) and bounded on each axis;
/// a value that isn't finite counts as none.
fn bounded_shake(shake: NativeShake, scale: f32) -> NativeShake {
@@ -2309,6 +2430,16 @@ impl Game<'_> {
pub(super) fn native_commands(&mut self, commands: &mut Vec<&'static str>) {
mod_api::dispatch_commands(self, mod_api::mods(), commands);
}
/// Player FOV scale, after vanilla effects and before zoom and easing.
/// Invalid values or values outside 0.01..=10 revert to base. API @API:compute_fov_modifier@.
pub(super) fn native_compute_fov_modifier(
&self,
player: &horizon_client::camera::FovPlayer,
base: f32,
modifier: &mut f32,
) {
mod_api::dispatch_compute_fov_modifier(self, mod_api::mods(), player, base, modifier);
}
/// Local player movement input, after reading it and before applying it.
pub(super) fn native_movement_input(
&self,
@@ -2387,6 +2518,60 @@ impl Game<'_> {
);
}
/// Additional first-person hand and item pose, including empty arms and maps.
/// Add offsets in blocks and pitch, yaw and roll in degrees; multiply scale.
/// API @API:render_hand_pose@.
pub(super) fn native_render_hand_pose(
&self,
hand: horizon_client::first_person::Hand,
stack: Option<&ItemStack>,
partial: f32,
swing: f32,
equip: f32,
x: &mut f32,
y: &mut f32,
z: &mut f32,
pitch: &mut f32,
yaw: &mut f32,
roll: &mut f32,
scale: &mut f32,
) {
mod_api::dispatch_render_hand_pose(
self,
mod_api::mods(),
hand,
stack,
partial,
swing,
equip,
x,
y,
z,
pitch,
yaw,
roll,
scale,
);
}
/// Before a visible item frame's item or map is drawn. Cancel hides the item, not the frame.
pub(super) fn native_render_item_in_frame(
&self,
entity_id: u32,
pos: [i32; 3],
stack: &ItemStack,
canceled: &mut bool,
) {
mod_api::dispatch_render_item_in_frame(
self,
mod_api::mods(),
entity_id,
pos,
stack,
canceled,
);
}
/// Final scaled third-person camera distance, before visual-shape collision clipping.
/// `entity` includes its living scale/distance and optional mount scale/distance.
/// Only finite requests in 0..=1024 blocks replace the vanilla distance.
@@ -2840,6 +3025,20 @@ impl Game<'_> {
) {
mod_api::dispatch_screen_draw(self, mod_api::mods(), block, data, batch, origin);
}
/// A screen was observed opening, at most once per frame.
pub(super) fn native_screen_opening(&self, screen: &NativeScreenView<'_>) {
mod_api::dispatch_screen_opening(self, mod_api::mods(), screen);
}
/// A screen was observed opening or changing rectangle, at most once per frame.
pub(super) fn native_screen_init_post(&self, screen: &NativeScreenView<'_>) {
mod_api::dispatch_screen_init_post(self, mod_api::mods(), screen);
}
/// A screen was observed closing, at most once per frame.
pub(super) fn native_screen_closing(&self, screen: &NativeScreenView<'_>) {
mod_api::dispatch_screen_closing(self, mod_api::mods(), screen);
}
/// The open screen has drawn itself and its tooltips (`Screen.render`,
/// the point of `ScreenEvent.Render.Post`): draw over it with
/// `native_fill`, `native_sprite` and `native_text`, `screen.rect` being
@@ -5610,6 +5809,24 @@ impl Game<'_> {
pub(super) fn native_tooltip(&self, stack: &ItemStack, lines: &mut Vec<(Text, u32)>) {
mod_api::dispatch_tooltip(self, mod_api::mods(), stack, lines);
}
/// Change a tooltip box's background and border gradient colors, as ARGB.
/// Stack is None for tooltips not associated with an item.
pub(super) fn native_tooltip_color(
&self,
stack: Option<&ItemStack>,
background: &mut u32,
border_top: &mut u32,
border_bottom: &mut u32,
) {
mod_api::dispatch_tooltip_color(
self,
mod_api::mods(),
stack,
background,
border_top,
border_bottom,
);
}
/// Client-side image component for this stack's tooltip. Answer its width/height
/// in GUI pixels; finite positive dimensions reserve space after text and any
/// vanilla bundle component. None keeps the vanilla tooltip unchanged.
@@ -8027,6 +8244,22 @@ impl Game<'_> {
let state = self.world.block(pos[0], pos[1], pos[2]);
self.res.facts().get(state).name.to_string()
}
/// The most recently extracted world frame's camera, if one exists.
pub(super) fn native_camera(&self) -> Option<NativeCamera> {
self.native_camera_frame.clone()
}
/// Collision boxes in block-local coordinates with an empty collision context.
pub(super) fn native_block_collision(&self, pos: [i32; 3]) -> Vec<[f64; 6]> {
self.block_collision_shape(pos)
.boxes()
.iter()
.map(|bounds| {
let min = bounds.min_position();
let max = bounds.max_position();
[min.x, min.y, min.z, max.x, max.y, max.z]
})
.collect()
}
/// Property `key` of the block at `pos` (a machine's `facing`), if it
/// has one. API 20.
pub(super) fn native_block_property(&self, pos: [i32; 3], key: &str) -> Option<String> {
@@ -9286,6 +9519,42 @@ fn mod_screen_opens(
#[cfg(test)]
mod tests {
#[test]
fn hand_pose_rejects_invalid_components() {
let identity = super::hand_pose_matrix([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]);
assert_eq!(identity, glam::Mat4::IDENTITY);
assert_eq!(
super::hand_pose_matrix([
f32::NAN,
17.0,
f32::INFINITY,
3601.0,
-3601.0,
f32::NAN,
0.0
]),
identity
);
let point = super::hand_pose_matrix([1.0, 2.0, 3.0, 0.0, 0.0, 0.0, 2.0])
.transform_point3(glam::Vec3::X);
assert_eq!(point, glam::Vec3::new(3.0, 2.0, 3.0));
}
#[test]
fn observed_overlay_prefers_first_open_screen() {
assert_eq!(
super::observed_overlay_id(&[(false, "sign"), (false, "book")]),
None
);
assert_eq!(
super::observed_overlay_id(&[(false, "sign"), (true, "book")]),
Some("book")
);
assert_eq!(
super::observed_overlay_id(&[(true, "sign"), (true, "book")]),
Some("sign")
);
}
/// Exercise the gameplay constructor, where name-based entities compete
/// with a mod block's sign template.
#[test]
@@ -365,8 +365,7 @@ impl Game<'_> {
if self.carried.is_some() {
self.draw_carried(batch);
} else if let Some(stack) = self.browser_pointed_stack(&layout) {
let lines = self.stack_tooltip(&stack.1);
self.draw_tooltip_lines(batch, &lines, cursor.0, cursor.1);
self.draw_stack_tooltip(batch, &stack.1, cursor.0, cursor.1);
} else {
self.draw_slot_tooltip(batch);
}
+55 -3
View File
@@ -24,6 +24,26 @@ fn zombie_kind(s: Species) -> bool {
)
}
/// Which `finalizeSpawn` mounts a mob may roll for its spawn reason.
#[derive(Debug, PartialEq, Eq)]
struct MountRolls {
zombie_horse_rider: bool,
camel_husk: bool,
nautilus: bool,
}
/// `ZombieHorse.finalizeSpawn` (NATURAL only), `Husk.finalizeSpawn`
/// (`triedToSpawnCamelHusk = reason != NATURAL`) and `Drowned.finalizeSpawn`
/// (never a baby; vanilla also allows STRUCTURE, which comes from worldgen and
/// never reaches `spawn_jockeys`, so only `natural` is modelled here).
fn mount_rolls(species: Species, baby: bool, natural: bool) -> MountRolls {
MountRolls {
zombie_horse_rider: natural && species == Species::ZombieHorse,
camel_husk: natural && species == Species::Husk,
nautilus: natural && species == Species::Drowned && !baby,
}
}
impl Game<'_> {
/// `Entity.startRiding` for two mobs.
pub(super) fn mount_mob(&mut self, rider: u32, vehicle: u32) {
@@ -333,6 +353,7 @@ impl Game<'_> {
};
let (species, baby) = (m.species(), m.is_baby());
let at = [m.position.x, m.position.y, m.position.z];
let mounts = mount_rolls(species, baby, natural);
match species {
s if zombie_kind(s) && baby => {
if self.world.random.next_float() < CHICKEN_JOCKEY_ODDS {
@@ -358,7 +379,7 @@ impl Game<'_> {
// comes from EntityType.create, which makes no zombie in peaceful
// (canSpawn), so a peaceful zombie horse spawns without one.
Species::ZombieHorse
if natural
if mounts.zombie_horse_rider
&& (self.ui.difficulty != 0
|| horizon_client::mobs::spawn_rules::allowed_in_peaceful(
Species::Zombie,
@@ -375,8 +396,8 @@ impl Game<'_> {
}
// After Zombie.finalizeSpawn's chickens: Husk and Drowned mounts.
match species {
Species::Husk if natural => self.spawn_camel_husk_jockey(id, at),
Species::Drowned if natural && !baby => self.spawn_zombie_nautilus_jockey(id, at),
Species::Husk if mounts.camel_husk => self.spawn_camel_husk_jockey(id, at),
Species::Drowned if mounts.nautilus => self.spawn_zombie_nautilus_jockey(id, at),
_ => {}
}
}
@@ -572,3 +593,34 @@ impl Game<'_> {
Some(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spawner_and_reinforcement_spawns_get_no_mounts() {
for (species, baby) in [
(Species::ZombieHorse, false),
(Species::Husk, false),
(Species::Drowned, false),
] {
let none = MountRolls {
zombie_horse_rider: false,
camel_husk: false,
nautilus: false,
};
assert_eq!(mount_rolls(species, baby, false), none, "{species:?}");
}
}
#[test]
fn natural_spawns_roll_their_own_mount() {
let r = |s, b| mount_rolls(s, b, true);
assert!(r(Species::ZombieHorse, false).zombie_horse_rider);
assert!(r(Species::Husk, false).camel_husk);
assert!(r(Species::Drowned, false).nautilus);
assert!(!r(Species::Drowned, true).nautilus);
assert!(!r(Species::Zombie, false).camel_husk);
}
}
+7 -5
View File
@@ -504,8 +504,9 @@ impl Game<'_> {
let mut t = super::trident::ThrownTrident::from_nbt(n);
t.id = self.take_entity_id();
// One that came through a portal keeps its cooldown.
self.restore_portal_cooldown(t.id, &tag);
let id = t.id;
self.tridents.push(t);
self.remember(id, uuid, tag);
}
}
"minecraft:firework_rocket" => {
@@ -1581,10 +1582,11 @@ impl Game<'_> {
i: usize,
keys: &HashSet<ChunkKey>,
) -> Option<(ChunkKey, Tag)> {
let t = &self.tridents[i];
keys.contains(&chunk_of(t.position)).then(|| {
let (uuid, raw) = anon_uuid_raw(&self.persist, "minecraft:trident", t.position);
(chunk_of(t.position), t.to_nbt(uuid, raw.as_ref()))
let (id, at) = (self.tridents[i].id, self.tridents[i].position);
keys.contains(&chunk_of(at)).then(|| {
let uuid = self.uuid_for(id);
let tag = self.tridents[i].to_nbt(uuid, self.persist.raw.get(&id));
(chunk_of(at), tag)
})
}
pub(super) fn save_tnt(
+20 -21
View File
@@ -2189,19 +2189,6 @@ impl Game<'_> {
(x - r.chunk_center.0).abs() <= range && (z - r.chunk_center.1).abs() <= range
})
}
/// A column the client no longer holds; far terrain keeps it as it was
/// last (`server_terrain`).
fn forget_remote_chunk(
&mut self,
x: i32,
z: i32,
) -> Option<std::sync::Arc<horizon_client::chunk::Chunk>> {
let chunk = self.streaming.forget(&mut self.world, x, z)?;
if let Some(lod) = &mut self.lod {
lod.observe(chunk.clone(), self.res.facts());
}
Some(chunk)
}
/// The columns a moved or resized storage no longer holds.
fn drop_remote_chunks_out_of_range(&mut self) {
let mut gone: horizon_client::fast_hash::FxHashSet<(i32, i32)> = self
@@ -2215,7 +2202,8 @@ impl Game<'_> {
}
/// Forgets the `gone` columns (and empties the set): their chunks (freed on a thread
/// of their own when there are many), their sections in the renderer and their block
/// entities, each store once for all of them.
/// entities, each store once for all of them. Far terrain keeps them as they were last
/// (`server_terrain`).
fn forget_remote_columns(
&mut self,
gone: &mut horizon_client::fast_hash::FxHashSet<(i32, i32)>,
@@ -2223,9 +2211,11 @@ impl Game<'_> {
if gone.is_empty() {
return;
}
let mut freed = Vec::with_capacity(gone.len());
for &(x, z) in gone.iter() {
freed.extend(self.forget_remote_chunk(x, z));
let freed = self.streaming.forget_columns(&mut self.world, gone);
if let Some(lod) = &mut self.lod {
for chunk in &freed {
lod.observe(chunk.clone(), self.res.facts());
}
}
if let Some(renderer) = self.renderer.as_mut() {
renderer.remove_columns(gone.iter().copied());
@@ -4411,9 +4401,18 @@ impl Game<'_> {
C::RecipeBookSettings(p) => self.remote_recipe_book_settings(p),
C::PlaceGhostRecipe(p) => self.remote_ghost_recipe(p),
C::MerchantOffers(p) => self.remote_merchant_offers(p),
C::ContainerClose(_) if self.remote_lectern_closed() => {}
C::ContainerClose(_) => {
if self.screen == Screen::Inventory {
// `LocalPlayer.clientSideCloseContainer` sets the screen to
// null, including dialogs and WaitingForResponseScreen. This
// differs from clearDialog, which restores the parent screen.
let dialog = self.dialog_open();
if dialog {
self.close_dialogs();
self.ui.chat_screen = None;
self.book = None;
self.sign_edit = None;
}
if !self.remote_lectern_closed() && (self.screen == Screen::Inventory || dialog) {
self.remote_close_local();
self.screen = Screen::Playing;
self.capture(true);
@@ -7154,7 +7153,7 @@ impl Game<'_> {
}
// Resolve the crosshair once. A passing entity interaction keeps
// that target and may only fall back to using the item itself.
let entity = self.entity_targeted();
let entity = self.remote_use_targets_entity();
horizon_client::interaction_input::use_on_target(&mut RemoteUseWorld(self), entity);
}
@@ -8192,7 +8191,7 @@ impl Game<'_> {
}
return;
}
let off_target = if self.entity_targeted() {
let off_target = if self.remote_use_targets_entity() {
self.remote_use_entity(Hand::Off)
} else {
self.remote_use_block(Hand::Off)
@@ -514,6 +514,14 @@ impl Game<'_> {
}
} else if let Some(p) = self.creatures.projectiles.iter_mut().find(|p| p.id == id) {
match (&mut p.kind, index, value) {
// Entity.DATA_SHARED_FLAGS_ID's FLAG_ONFIRE: Entity.isOnFire on
// a client, which draws the flames. A client doesn't count the
// fire down: the flag goes off when the server's fire is out.
(_, 0, Value::Byte(flags)) => {
if let Some(a) = p.arrow.as_mut() {
a.fire_ticks = i32::from(flags & 1);
}
}
// AbstractArrow.ID_FLAGS (critical) and IN_GROUND.
(ProjectileKind::Arrow { critical, .. }, 8, Value::Byte(flags)) => {
*critical = flags & 1 != 0;
@@ -806,7 +814,8 @@ impl Game<'_> {
}
for p in &self.creatures.projectiles {
out.push(json!({"id": p.id, "kind": format!("{:?}", p.kind).split([' ', '{']).next().unwrap_or(""), "position": at(p.position), "velocity": at(p.velocity),
"player_owned": p.owner == Some(horizon_client::mobs::Who::Player)}));
"player_owned": p.owner == Some(horizon_client::mobs::Who::Player),
"on_fire": p.arrow.as_ref().is_some_and(|a| a.on_fire())}));
}
for (kind, p, _) in self.potions.thrown_entities() {
out.push(json!({"kind": kind, "position": at(p)}));
@@ -26,8 +26,39 @@ pub(super) struct ObjectHit {
}
impl Game<'_> {
/// BlockAttachedEntity.isPickable: the nearest fence knot's box the view meets, with its id,
/// distance and position. Its box is wider than the post inside it, so the crosshair meets it
/// first and LeashFenceKnotEntity.interact takes the click. The fence block behind only ties
/// the player's own mobs (LeadItem.useOn), it never takes one back.
fn remote_knot_hit(&self) -> Option<(i32, f64, [f64; 3])> {
let (eye, dir) = self.aim()?;
self.remote_knots
.iter()
.filter_map(|(&id, &block)| {
let knot = horizon_client::mobs::leash::knot_state(block);
let p = knot.position;
let half = knot.width / 2.0;
let bounds = Aabb::new(
Vec3::new(p.x - half, p.y, p.z - half),
Vec3::new(p.x + half, p.y + knot.height, p.z + half),
);
ray_box(eye, dir, self.entity_reach, bounds)
.map(|d| (id as i32, d, [p.x, p.y, p.z]))
})
.min_by(|a, b| a.1.total_cmp(&b.1))
}
/// Whether a use starts on an entity: the crosshair's `EntityHitResult`, or a fence knot.
/// Only a use: Horizon's attacks still go to the fence behind a knot.
pub(super) fn remote_use_targets_entity(&self) -> bool {
self.entity_targeted()
|| self
.remote_knot_hit()
.is_some_and(|(_, d, _)| self.before_block(d))
}
/// The nearest boat, minecart, item frame, armor stand or painting the
/// view hits within reach; also End crystals when attacking.
/// view hits within reach; also End crystals when attacking, and fence knots when using.
pub(super) fn remote_object_hit(&self, attacking: bool) -> Option<ObjectHit> {
let (eye, dir) = self.aim()?;
let at = |d: f64| {
@@ -80,6 +111,9 @@ impl Game<'_> {
hits.push((id, d, [p.x, p.y, p.z]));
}
}
if !attacking && let Some((id, d, p)) = self.remote_knot_hit() {
hits.push((id, d, p));
}
if attacking {
// EndCrystal.isPickable: its 2 x 2 box, with the server's entity
// id, not special_targets' local projectile target id.
@@ -182,6 +182,7 @@ impl Game<'_> {
| Target::Tnt(id)
| Target::Falling(id)
| Target::Rocket(id)
| Target::Trident(id)
| Target::Other(id) => Some(uuid_text(self.uuid_for(id))),
// It keeps its UUID with itself.
Target::Spawner(id) => self
@@ -226,6 +227,7 @@ impl Game<'_> {
| Target::Crystal(id)
| Target::Dragon(id)
| Target::Rocket(id)
| Target::Trident(id)
| Target::Other(id) => self.persist.raw.get(&id),
Target::Player => self.level.player_extra.as_ref(),
// A server's player: the saved compound the guest's file is written from.
+1 -1
View File
@@ -148,7 +148,7 @@ impl Game<'_> {
self.native_mobs_joined(self.mobs.len() - 1, false);
self.enchant_spawn_equipment(id);
self.halloween_spawn_head(id);
self.spawn_jockeys(id, true);
self.spawn_jockeys(id, false);
Some((id, equipment))
}
Planned::Loaded(tag, equipment) => {
+30 -34
View File
@@ -2565,7 +2565,7 @@ impl Game<'_> {
let atlas = &self.gui.atlas;
let (x, y, w, h) =
Self::tooltip_geometry(atlas, &lines, (x, y), (batch.width, batch.height), [0.0; 2]);
Self::tooltip_box(batch, atlas, x, y, w, h);
self.tooltip_box(batch, None, x, y, w, h);
Self::tooltip_text(batch, atlas, &lines, x, y);
}
/// A tooltip's lines with their legacy codes resolved and their colours.
@@ -2605,47 +2605,43 @@ impl Game<'_> {
}
/// The box and its frame sprites.
pub(super) fn tooltip_box(
&self,
batch: &mut HudBatch,
atlas: &horizon_client::hud::HudAtlas,
stack: Option<&ItemStack>,
x: f32,
y: f32,
w: f32,
h: f32,
) {
batch.fill(atlas, x - 3.0, y - 4.0, w + 6.0, 1.0, rgba(16, 0, 16, 240));
batch.fill(
atlas,
x - 3.0,
y + h + 3.0,
w + 6.0,
1.0,
rgba(16, 0, 16, 240),
let (mut background, mut border_top, mut border_bottom) =
(0xf0100010, 0x505000ff, 0x5028007f);
self.native_tooltip_color(stack, &mut background, &mut border_top, &mut border_bottom);
let convert = |color: u32| {
let [a, r, g, b] = color.to_be_bytes();
rgba(r, g, b, a)
};
let (background, border_top, border_bottom) = (
convert(background),
convert(border_top),
convert(border_bottom),
);
batch.fill(
let atlas = &self.gui.atlas;
batch.fill(atlas, x - 3.0, y - 4.0, w + 6.0, 1.0, background);
batch.fill(atlas, x - 3.0, y + h + 3.0, w + 6.0, 1.0, background);
batch.fill(atlas, x - 4.0, y - 3.0, w + 8.0, h + 6.0, background);
batch.fill(atlas, x - 3.0, y - 3.0, w + 6.0, 1.0, border_top);
batch.fill(atlas, x - 3.0, y + h + 2.0, w + 6.0, 1.0, border_bottom);
batch.fill_gradient(
atlas,
x - 4.0,
y - 3.0,
w + 8.0,
h + 6.0,
rgba(16, 0, 16, 240),
(x - 3.0, y - 2.0, 1.0, h + 4.0),
border_top,
border_bottom,
);
batch.fill(atlas, x - 3.0, y - 3.0, w + 6.0, 1.0, rgba(80, 0, 255, 80));
batch.fill(
batch.fill_gradient(
atlas,
x - 3.0,
y + h + 2.0,
w + 6.0,
1.0,
rgba(40, 0, 127, 80),
);
batch.fill(atlas, x - 3.0, y - 2.0, 1.0, h + 4.0, rgba(60, 0, 190, 80));
batch.fill(
atlas,
x + w + 2.0,
y - 2.0,
1.0,
h + 4.0,
rgba(60, 0, 190, 80),
(x + w + 2.0, y - 2.0, 1.0, h + 4.0),
border_top,
border_bottom,
);
}
pub(super) fn tooltip_text(
@@ -2709,7 +2705,7 @@ impl Game<'_> {
(batch.width, batch.height),
[min_width, extra],
);
Self::tooltip_box(batch, atlas, x, y, w, h);
self.tooltip_box(batch, Some(stack), x, y, w, h);
let text_h = h - extra;
Self::tooltip_text(batch, atlas, &lines, x, y);
if let Some(layout) =
@@ -2756,7 +2752,7 @@ impl Game<'_> {
if py + 8.0 + 3.0 > batch.height {
py = batch.height - 8.0 - 3.0;
}
Self::tooltip_box(batch, atlas, px, py, tw, 8.0);
self.tooltip_box(batch, Some(stack), px, py, tw, 8.0);
Self::tooltip_text(batch, atlas, &lines, px, py);
}
/// `ClientBundleTooltip.extractImage`: the slots, the count past them and
+9 -2
View File
@@ -37,6 +37,8 @@ pub(super) struct ThrownTrident {
pub(super) no_physics: bool,
returning: u32,
creative_only: bool,
/// `Entity.isNoGravity`: `AbstractArrow.applyGravity` then skips the pull.
pub(super) no_gravity: bool,
/// The entity id a server sent as its owner (the one Loyalty returns it to).
pub(super) owner: u32,
/// The guest that threw it (`Projectile.getOwner`); `None` for the host.
@@ -66,6 +68,7 @@ impl ThrownTrident {
t.shake = n.shake;
t.dealt_damage = n.dealt_damage;
t.creative_only = n.creative_only;
t.no_gravity = n.no_gravity;
t
}
/// `AbstractArrow.addAdditionalSaveData` + `DealtDamage`.
@@ -85,6 +88,7 @@ impl ThrownTrident {
shake: self.shake,
dealt_damage: self.dealt_damage,
creative_only: self.creative_only,
no_gravity: self.no_gravity,
item: &self.stack,
},
uuid,
@@ -109,6 +113,7 @@ impl ThrownTrident {
no_physics: false,
returning: 0,
creative_only: false,
no_gravity: false,
owner: 0,
guest: None,
}
@@ -175,7 +180,7 @@ impl ThrownTrident {
self.position = to;
// A trident's water inertia (0.99) equals the air drag.
self.velocity = self.velocity.scale(0.99);
if !self.no_physics {
if !self.no_physics && !self.no_gravity {
self.velocity.y -= 0.05;
}
}
@@ -426,6 +431,7 @@ impl Game<'_> {
no_physics: false,
returning: 0,
creative_only: self.creative,
no_gravity: false,
owner: 0,
guest: self.lan_acting,
});
@@ -733,9 +739,10 @@ impl Game<'_> {
if !t.in_ground {
// A trident's water inertia (0.99) equals the air drag; gravity 0.05.
let drag = 0.99;
let gravity = if t.no_gravity { 0.0 } else { 0.05 };
t.velocity = Vec3::new(
t.velocity.x * drag,
t.velocity.y * drag - 0.05,
t.velocity.y * drag - gravity,
t.velocity.z * drag,
);
let h = (t.velocity.x * t.velocity.x + t.velocity.z * t.velocity.z).sqrt();
@@ -58,7 +58,8 @@ impl Game<'_> {
let Some(spot) = spot else {
return;
};
let Some(id) = self.spawn_mob(&kind.id, spot.map(f64::from), false) else {
let Some(id) = self.spawn_mob_reason(&kind.id, spot.map(f64::from), false, false, false)
else {
return;
};
if let Some(reinforcement) = self.mobs.iter_mut().find(|m| m.id == id) {
+1 -1
View File
@@ -374,7 +374,7 @@ struct WarmPool {
fn warm_pool() -> Option<&'static WarmPool> {
static POOL: OnceLock<Option<Arc<WarmPool>>> = OnceLock::new();
POOL.get_or_init(|| {
let threads = std::thread::available_parallelism().map_or(1, |n| n.get()) - 1;
let threads = horizon_threads::worker_count() - 1;
let pool = Arc::new(WarmPool {
job: Default::default(),
ready: Default::default(),
+4 -1
View File
@@ -1,9 +1,12 @@
//! Horizon: native Rust client for Minecraft Java Edition 26.3 gameplay.
#![forbid(unsafe_code)]
#[cfg(not(feature = "system-allocator"))]
#[cfg(not(any(feature = "system-allocator", feature = "alloc-census")))]
#[global_allocator]
static ALLOCATOR: horizon_threads::Allocator = horizon_threads::Allocator;
#[cfg(all(feature = "alloc-census", not(feature = "system-allocator")))]
#[global_allocator]
static ALLOCATOR: horizon_threads::CensusAllocator = horizon_threads::CensusAllocator;
/// The mod API level this game serves, kept in the program for the launcher
/// to read without running it (`horizon_mods::api_level::MARK`). `main`
+13
View File
@@ -24,6 +24,19 @@ pub const COLORS: [&str; 7] = [
"gray",
"darkbrown",
];
/// `Horse.Variant.getSerializedName` in id order (WHITE 0 to DARK_BROWN 6):
/// the names a `minecraft:horse/variant` component and the save's `Variant`
/// byte carry. [`COLORS`] is the same order, but names the texture
/// (`horse_darkbrown`), which drops the underscore.
pub const VARIANT_NAMES: [&str; 7] = [
"white",
"creamy",
"chestnut",
"brown",
"black",
"gray",
"dark_brown",
];
pub const MARKINGS: [&str; 5] = ["", "white", "whitefield", "whitedots", "blackdots"];
// AnimState slots.
@@ -2171,6 +2171,12 @@ pub fn variant_index(s: Species, name: &str) -> u8 {
// Fox.Variant, read from a `minecraft:fox/variant` component; the save
// keeps the same two names under `Type`.
Species::Fox => ["red", "snow"].iter().position(|v| *v == name),
// Horse.Variant and Llama.Variant, the coat colours their
// `.../variant` components carry; a trader llama inherits the llama's.
Species::Horse => super::equine::VARIANT_NAMES.iter().position(|v| *v == name),
Species::Llama | Species::TraderLlama => {
super::llama::VARIANTS.iter().position(|v| *v == name)
}
_ => ["temperate", "warm", "cold"]
.iter()
.position(|v| *v == name),
@@ -2193,10 +2199,29 @@ pub fn species_variant_component(s: Species, variant: u8) -> Option<(&'static st
Species::Mooshroom => "minecraft:mooshroom/variant",
Species::Chicken => "minecraft:chicken/variant",
Species::ZombieNautilus => "minecraft:zombie_nautilus/variant",
Species::Horse => "minecraft:horse/variant",
Species::Llama | Species::TraderLlama => "minecraft:llama/variant",
_ => return None,
};
Some((component, format!("minecraft:{}", variant_name(s, variant))))
}
/// Whether the mob's save keeps its variant under `variant` — the registry
/// id `VariantUtils.writeVariant` stores — rather than under its own key (a
/// fox's and a mooshroom's `Type`) or a packed int (a horse's and a llama's
/// `Variant`). Only these seven classes call `writeVariant`, so reporting the
/// component does not mean the save carries the name.
pub fn saves_variant_name(s: Species) -> bool {
matches!(
s,
Species::Wolf
| Species::Cat
| Species::Frog
| Species::Pig
| Species::Cow
| Species::Chicken
| Species::ZombieNautilus
)
}
/// The `*_sound_variant` registry of a species, in registry order.
pub fn sound_variants(s: Species) -> &'static [&'static str] {
match s {
@@ -2259,6 +2284,13 @@ pub fn variant_name(s: Species, index: u8) -> &'static str {
.copied()
.unwrap_or("temperate"),
Species::Mooshroom => ["red", "brown"].get(i).copied().unwrap_or("red"),
Species::Horse => super::equine::VARIANT_NAMES
.get(i)
.copied()
.unwrap_or("white"),
Species::Llama | Species::TraderLlama => {
super::llama::VARIANTS.get(i).copied().unwrap_or("creamy")
}
_ => ["temperate", "warm", "cold"]
.get(i)
.copied()
@@ -2320,6 +2352,60 @@ mod tests {
assert_eq!(variant_index(Species::Mooshroom, "minecraft:temperate"), 0);
assert_eq!(variant_name(Species::Mooshroom, 2), "red");
}
/// `Horse.get(DataComponents.HORSE_VARIANT)` and
/// `Llama.get(DataComponents.LLAMA_VARIANT)`: the coat colour a horse or a
/// llama reports, and the names a `.../variant` component carries.
#[test]
fn a_horse_and_a_llama_report_their_variant_component() {
for (species, component, names) in [
(
Species::Horse,
"minecraft:horse/variant",
&crate::mobs::equine::VARIANT_NAMES[..],
),
(
Species::Llama,
"minecraft:llama/variant",
&crate::mobs::llama::VARIANTS[..],
),
(
Species::TraderLlama,
"minecraft:llama/variant",
&crate::mobs::llama::VARIANTS[..],
),
] {
for (index, name) in names.iter().enumerate() {
let index = index as u8;
let name = format!("minecraft:{name}");
assert_eq!(
species_variant_component(species, index),
Some((component, name.clone())),
"{species:?} {index}"
);
assert_eq!(variant_index(species, &name), index, "{name}");
assert_eq!(format!("minecraft:{}", variant_name(species, index)), name);
}
}
// Horse.Variant.WHITE and Llama.Variant.CREAMY are the defaults.
assert_eq!(variant_index(Species::Horse, "minecraft:temperate"), 0);
assert_eq!(variant_index(Species::Llama, "minecraft:temperate"), 0);
assert_eq!(variant_name(Species::Horse, 7), "white");
assert_eq!(variant_name(Species::TraderLlama, 4), "creamy");
// Only `VariantUtils.writeVariant` species save the name; a horse and
// a llama save a packed `Variant` int, a fox and a mooshroom `Type`.
for species in [Species::Wolf, Species::Cat, Species::Chicken, Species::Pig] {
assert!(saves_variant_name(species), "{species:?}");
}
for species in [
Species::Horse,
Species::Llama,
Species::TraderLlama,
Species::Fox,
Species::Mooshroom,
] {
assert!(!saves_variant_name(species), "{species:?}");
}
}
/// #2269: the offspring of a spawn egg used on a mob is a baby of that
/// kind (`SpawnEggItem.spawnOffspringFromSpawnEgg`), and only the mob's
/// own kind takes the egg.
+52
View File
@@ -492,6 +492,23 @@ pub fn leash_offset(m: &Mob) -> [f64; 3] {
f64::from(0.55_f32 * m.eye_height() as f32),
f64::from(m.width() * 0.4),
]
} else if let Some(factor) = match m.species() {
// Pig.getLeashOffset, Rabbit.getLeashOffset and
// Strider.getLeashOffset: 0.6 of the eye height; Parrot.getLeashOffset
// and Ocelot.getLeashOffset: half of it.
super::Species::Pig | super::Species::Rabbit | super::Species::Strider => Some(0.6_f32),
super::Species::Parrot | super::Species::Ocelot => Some(0.5_f32),
_ => None,
} {
[
0.0,
f64::from(factor * m.eye_height() as f32),
f64::from(m.width() * 0.4),
]
} else if m.species() == super::Species::HappyGhast {
// HappyGhast.getLeashOffset: the origin, so the rope hangs at the
// ghast's feet rather than an eye height up.
[0.0, 0.0, 0.0]
} else {
[0.0, m.eye_height(), f64::from(m.width() * 0.4)]
}
@@ -631,6 +648,41 @@ mod tests {
}
});
}
/// `Pig`, `Rabbit`, `Strider`, `Parrot`, `Ocelot` and
/// `HappyGhast.getLeashOffset`: 0.6 or half of the eye height, and the
/// happy ghast's rope at the origin, against the default eye-height
/// height the fallthrough gave them.
#[test]
fn a_pig_a_rabbit_a_parrot_an_ocelot_and_a_ghast_hang_their_lead_where_vanilla_does() {
let _ = crate::mobs::world::with_test_world(&[], |_, types| {
for (kind, factor) in [
("minecraft:pig", 0.6),
("minecraft:rabbit", 0.6),
("minecraft:strider", 0.6),
("minecraft:parrot", 0.5),
("minecraft:ocelot", 0.5),
// `Vec3.ZERO`: no eye-height part and no forward shift.
("minecraft:happy_ghast", 0.0),
] {
let m = Mob::new(
1,
std::sync::Arc::new(types.types[kind].clone()),
Vec3::ZERO,
0.0,
);
let o = leash_offset(&m);
let h = f64::from(factor * m.eye_height() as f32);
let z = f64::from(m.width() * 0.4);
assert!((o[1] - h).abs() < 1e-9, "{kind}: height {} vs {h}", o[1]);
assert!(
(o[2] - if factor == 0.0 { 0.0 } else { z }).abs() < 1e-9,
"{kind}: forward {} vs {z}",
o[2]
);
assert_eq!(o[0], 0.0, "{kind}: centred");
}
});
}
/// `Nautilus.canBeLeashed` and `ZombieNautilus.canBeLeashed`: an
/// aggravated nautilus, one holding an `ANGRY_AT` or an `ATTACK_TARGET`
/// memory, cannot be put on a lead.
+19 -1
View File
@@ -6,7 +6,7 @@
use std::sync::{Arc, OnceLock};
pub use crate::biome::{BiomeColors, SpawnEntry};
pub use crate::camera::CameraEntity;
pub use crate::camera::{CameraEntity, FovPlayer};
pub use crate::command::Suggestion;
pub use crate::containers::{ModBlockEntity, ModContainer, ModEnergyBuffer, ModFluidTanks};
pub use crate::crafting::{CustomRecipe, Ingredient};
@@ -54,6 +54,24 @@ use std::time::Duration;
/// The window events `input_key` hands over (`winit::event::KeyEvent`) and their key types.
pub use winit;
/// Camera values from the most recently extracted world frame.
#[derive(Clone, Debug)]
pub struct NativeCamera {
pub position: [f64; 3],
pub eye_height: f32,
pub partial_tick: f32,
pub yaw: f32,
pub pitch: f32,
pub fov_degrees: f32,
pub near: f32,
pub far: f32,
/// Camera-relative view matrix in column-major order.
pub view: [f32; 16],
/// Reversed-Z projection matrix in column-major order.
pub projection: [f32; 16],
pub dimension: String,
}
/// A dispenser hook's answer: the slot's remainder (None empties it) and the
/// level event to play, `CLICK` (1000), `FAIL` (1001) or 0 for none.
pub type NativeDispensed = (Option<ItemStack>, i32);
+234
View File
@@ -4512,6 +4512,12 @@ pub trait Hooks: Send + Sync {
fn living_jump(&self, event: &mut LivingJumpEvent<'_>) {
let _ = event;
}
/// Player FOV scale, after vanilla effects and before zoom and easing.
/// Invalid values or values outside 0.01..=10 revert to base. API @API:compute_fov_modifier@.
fn compute_fov_modifier(&self, host: &dyn Host, event: &mut ComputeFovModifierEvent<'_>) {
let _ = host;
let _ = event;
}
/// Local player movement input, after reading it and before applying it.
fn movement_input(&self, host: &dyn Host, event: &mut MovementInputEvent) {
let _ = host;
@@ -4538,6 +4544,46 @@ pub trait Hooks: Send + Sync {
let _ = event;
Flow::Continue
}
/// Additional first-person hand and item pose, including empty arms and maps.
/// Add offsets in blocks and pitch, yaw and roll in degrees; multiply scale.
/// API @API:render_hand_pose@.
fn render_hand_pose(&self, host: &dyn Host, event: &mut RenderHandPoseEvent<'_>) {
let _ = host;
let _ = event;
}
/// Before a visible item frame's item or map is drawn. Cancel hides the item, not the frame.
///
/// Return `Flow::Cancel` to take it: this sets `canceled`, and later mods are skipped.
fn render_item_in_frame(
&self,
host: &dyn Host,
event: &mut RenderItemInFrameEvent<'_>,
) -> Flow {
let _ = host;
let _ = event;
Flow::Continue
}
/// A screen was observed closing, at most once per frame.
fn screen_closing(&self, host: &dyn Host, event: &mut ScreenClosingEvent<'_>) {
let _ = host;
let _ = event;
}
/// A screen was observed opening or changing rectangle, at most once per frame.
fn screen_init_post(&self, host: &dyn Host, event: &mut ScreenInitPostEvent<'_>) {
let _ = host;
let _ = event;
}
/// A screen was observed opening, at most once per frame.
fn screen_opening(&self, host: &dyn Host, event: &mut ScreenOpeningEvent<'_>) {
let _ = host;
let _ = event;
}
/// Change a tooltip box's background and border gradient colors, as ARGB.
/// Stack is None for tooltips not associated with an item.
fn tooltip_color(&self, host: &dyn Host, event: &mut TooltipColorEvent<'_>) {
let _ = host;
let _ = event;
}
}
/// What [`Hooks::frame`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
@@ -13825,6 +13871,30 @@ pub fn dispatch_living_jump(mods: &[Box<dyn Hooks>], entity: u32, kind: &str) {
mod_hook.living_jump(&mut event);
}
}
/// What [`Hooks::compute_fov_modifier`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct ComputeFovModifierEvent<'a> {
pub player: &'a horizon_client::camera::FovPlayer,
pub base: f32,
pub modifier: f32,
}
pub fn dispatch_compute_fov_modifier(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
player: &horizon_client::camera::FovPlayer,
base: f32,
modifier: &mut f32,
) {
let mut event = ComputeFovModifierEvent {
player,
base,
modifier: *modifier,
};
for mod_hook in super::entered(mods, "compute_fov_modifier") {
mod_hook.compute_fov_modifier(host, &mut event);
}
*modifier = event.modifier;
}
/// What [`Hooks::movement_input`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct MovementInputEvent {
@@ -13918,3 +13988,167 @@ pub fn dispatch_village_siege(
}
}
}
/// What [`Hooks::render_hand_pose`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct RenderHandPoseEvent<'a> {
pub hand: horizon_client::first_person::Hand,
pub stack: Option<&'a ItemStack>,
pub partial: f32,
pub swing: f32,
pub equip: f32,
pub x: f32,
pub y: f32,
pub z: f32,
pub pitch: f32,
pub yaw: f32,
pub roll: f32,
pub scale: f32,
}
pub fn dispatch_render_hand_pose(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
hand: horizon_client::first_person::Hand,
stack: Option<&ItemStack>,
partial: f32,
swing: f32,
equip: f32,
x: &mut f32,
y: &mut f32,
z: &mut f32,
pitch: &mut f32,
yaw: &mut f32,
roll: &mut f32,
scale: &mut f32,
) {
let mut event = RenderHandPoseEvent {
hand,
stack,
partial,
swing,
equip,
x: *x,
y: *y,
z: *z,
pitch: *pitch,
yaw: *yaw,
roll: *roll,
scale: *scale,
};
for mod_hook in super::entered(mods, "render_hand_pose") {
mod_hook.render_hand_pose(host, &mut event);
}
*x = event.x;
*y = event.y;
*z = event.z;
*pitch = event.pitch;
*yaw = event.yaw;
*roll = event.roll;
*scale = event.scale;
}
/// What [`Hooks::render_item_in_frame`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct RenderItemInFrameEvent<'a> {
pub entity_id: u32,
pub pos: [i32; 3],
pub stack: &'a ItemStack,
}
pub fn dispatch_render_item_in_frame(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
entity_id: u32,
pos: [i32; 3],
stack: &ItemStack,
canceled: &mut bool,
) {
if *canceled {
return;
}
let mut event = RenderItemInFrameEvent {
entity_id,
pos,
stack,
};
for mod_hook in super::entered(mods, "render_item_in_frame") {
if matches!(
mod_hook.render_item_in_frame(host, &mut event),
Flow::Cancel
) {
*canceled = true;
break;
}
}
}
/// What [`Hooks::screen_closing`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct ScreenClosingEvent<'a> {
pub screen: &'a NativeScreenView<'a>,
}
pub fn dispatch_screen_closing(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
screen: &NativeScreenView<'_>,
) {
let mut event = ScreenClosingEvent { screen };
for mod_hook in super::entered(mods, "screen_closing") {
mod_hook.screen_closing(host, &mut event);
}
}
/// What [`Hooks::screen_init_post`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct ScreenInitPostEvent<'a> {
pub screen: &'a NativeScreenView<'a>,
}
pub fn dispatch_screen_init_post(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
screen: &NativeScreenView<'_>,
) {
let mut event = ScreenInitPostEvent { screen };
for mod_hook in super::entered(mods, "screen_init_post") {
mod_hook.screen_init_post(host, &mut event);
}
}
/// What [`Hooks::screen_opening`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct ScreenOpeningEvent<'a> {
pub screen: &'a NativeScreenView<'a>,
}
pub fn dispatch_screen_opening(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
screen: &NativeScreenView<'_>,
) {
let mut event = ScreenOpeningEvent { screen };
for mod_hook in super::entered(mods, "screen_opening") {
mod_hook.screen_opening(host, &mut event);
}
}
/// What [`Hooks::tooltip_color`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct TooltipColorEvent<'a> {
pub stack: Option<&'a ItemStack>,
pub background: u32,
pub border_top: u32,
pub border_bottom: u32,
}
pub fn dispatch_tooltip_color(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
stack: Option<&ItemStack>,
background: &mut u32,
border_top: &mut u32,
border_bottom: &mut u32,
) {
let mut event = TooltipColorEvent {
stack,
background: *background,
border_top: *border_top,
border_bottom: *border_bottom,
};
for mod_hook in super::entered(mods, "tooltip_color") {
mod_hook.tooltip_color(host, &mut event);
}
*background = event.background;
*border_top = event.border_top;
*border_bottom = event.border_bottom;
}
+9
View File
@@ -1203,4 +1203,13 @@ pub trait Host {
fn instance_dir(&self) -> PathBuf {
unimplemented!("Host::instance_dir")
}
/// The most recently extracted world frame's camera, if one exists.
fn camera(&self) -> Option<NativeCamera> {
unimplemented!("Host::camera")
}
/// Collision boxes in block-local coordinates with an empty collision context.
fn block_collision(&self, pos: [i32; 3]) -> Vec<[f64; 6]> {
let _ = pos;
unimplemented!("Host::block_collision")
}
}
+1 -1
View File
@@ -2247,7 +2247,7 @@ fn tables(base: &Path, plan: &Plan, overlay: &Overlay<'_>) -> Result<()> {
overlay.write(name, &bytes)
};
let next = std::sync::atomic::AtomicUsize::new(0);
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
let threads = horizon_threads::worker_count();
std::thread::scope(|scope| {
let workers: Vec<_> = (0..threads.min(TABLE_THREADS))
.map(|_| {
+2 -2
View File
@@ -2100,8 +2100,8 @@ fn compress_records(
.map(|tag| ChunkRecord::from_nbt(&tag, Compression::Zlib, timestamp, Limits::saved()))
.transpose()
};
let helpers = std::thread::available_parallelism()
.map_or(1, |n| n.get().div_ceil(2))
let helpers = horizon_threads::worker_count()
.div_ceil(2)
.min(records.len() / CHUNKS_PER_HELPER);
if helpers <= 1 {
return records.iter().map(|(_, r)| compress(r)).collect();
+1 -3
View File
@@ -72,9 +72,7 @@ impl Builder {
// ticks and the frame leave, so more cores can share it: a third of
// the threads, rounded up (a 4-thread machine's one thread left idle
// time while the far trees and structures came in).
let count = thread::available_parallelism()
.map_or(1, |n| n.get().div_ceil(3))
.clamp(1, 12);
let count = horizon_threads::worker_count().div_ceil(3).clamp(1, 12);
let threads = (0..count)
.map(|_| Self::spawn(terrain.clone(), shared.clone(), tx.clone()))
.collect();
+31 -1
View File
@@ -275,7 +275,18 @@ fn strip_comments(text: &str) -> (String, Vec<(usize, String)>) {
out.push_str(before);
line += before.matches('\n').count();
if let Some(body) = from.strip_prefix("//") {
let end = body.find('\n').unwrap_or(body.len());
// A line comment ending in a backslash includes the next line (GLSL 4.60, 3.4; Iris
// does so too): such a comment runs on. Its line breaks stay.
let mut end = body.len();
for (at, _) in body.match_indices('\n') {
if !body[..at].trim_end_matches('\r').ends_with('\\') {
end = at;
break;
}
}
let joined = body[..end].matches('\n').count();
line += joined;
out.extend(std::iter::repeat_n('\n', joined));
rest = &body[end..];
} else if let Some(body) = from.strip_prefix("/*") {
let end = body.find("*/").unwrap_or(body.len());
@@ -422,6 +433,25 @@ mod tests {
assert_eq!(messages, ["#error live", "unknown directive `#bogus`"]);
}
/// A line comment ending in a backslash runs on to the next line, as in Iris: the line it
/// takes is not code and not a directive, and later lines keep their numbers.
#[test]
fn a_line_comment_runs_on_after_a_backslash() {
let p = run(&[(
"program/final.fsh",
"#version 330\n#if 1\nfloat a; // ends in a backslash \\\n#endif\n\
// alone \\\r\n#define HIDDEN\n#ifdef HIDDEN\nfloat b;\n#endif\nfloat c;\n#endif\n",
)]);
assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
assert_eq!(
code_lines(&p),
[
("float a;", "program/final.fsh:3".into()),
("float c;", "program/final.fsh:10".into()),
]
);
}
/// A stray `#endif` and an `#if` left open are warnings, as in Iris 1.11.7 (which draws such
/// a program): the `#endif` is ignored, the open `#if` reads on to the end of the file.
#[test]
+4
View File
@@ -16,5 +16,9 @@ libc = "0.2"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_ProcessStatus", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_WindowsAndMessaging"] }
[features]
# Counts live heap bytes by startup step and thread (`CensusAllocator`); for measuring only.
alloc-census = []
[lints]
workspace = true
+269
View File
@@ -0,0 +1,269 @@
//! Who holds the heap, for `horizon-ctl memory` (feature `alloc-census`, for
//! measuring only): mimalloc with a tag in front of each block, and the live
//! bytes of each tag. A block's tag is what ran on its thread when it was
//! allocated: the innermost [`tag`] (a startup step, a timed span or a pool
//! job: `startup::time`, `spans::time` and `jobs` set them), else the
//! thread's name. A block keeps its tag when another thread frees or grows
//! it, so a column generated by a pool job and kept by the server thread
//! counts for the job.
//!
//! Each block costs a header of 16 bytes (its alignment when larger) and an
//! atomic add: heaptrack's interpreter needed more than 7 GB for the game
//! (18 GB with line tables), this needs nothing beside it.
use std::{
alloc::{GlobalAlloc, Layout},
cell::Cell,
sync::atomic::{AtomicIsize, AtomicPtr, AtomicUsize, Ordering::Relaxed},
};
const TAGS: usize = 512;
/// Blocks allocated while this thread names itself, and past the table's end.
const UNTAGGED: u32 = 0;
const NONE: u32 = u32::MAX;
static LIVE: [AtomicIsize; TAGS] = [const { AtomicIsize::new(0) }; TAGS];
static NAME_PTR: [AtomicPtr<u8>; TAGS] = [const { AtomicPtr::new(std::ptr::null_mut()) }; TAGS];
static NAME_LEN: [AtomicUsize; TAGS] = [const { AtomicUsize::new(0) }; TAGS];
/// The next free slot; slot 0 is [`UNTAGGED`].
static NEXT: AtomicUsize = AtomicUsize::new(1);
thread_local! {
static STEP: Cell<u32> = const { Cell::new(NONE) };
static THREAD: Cell<u32> = const { Cell::new(NONE) };
/// Allocations this thread made before its tag settled: std names a
/// thread after its first few, so the name is read again until then.
static NAMING: Cell<u32> = const { Cell::new(0) };
static BUSY: Cell<bool> = const { Cell::new(false) };
}
/// The slot of `name`, registering it. A race may give one name two slots;
/// [`census`] adds them up.
fn slot(name: &'static str) -> u32 {
let n = NEXT.load(Relaxed).min(TAGS);
for i in 1..n {
let ptr = NAME_PTR[i].load(Relaxed);
if !ptr.is_null() && NAME_LEN[i].load(Relaxed) == name.len() {
// SAFETY: the slot holds a leaked or static string of that length.
let s = unsafe { std::slice::from_raw_parts(ptr, name.len()) };
if s == name.as_bytes() {
return i as u32;
}
}
}
let i = NEXT.fetch_add(1, Relaxed);
if i >= TAGS {
return UNTAGGED;
}
NAME_LEN[i].store(name.len(), Relaxed);
NAME_PTR[i].store(name.as_ptr().cast_mut(), Relaxed);
i as u32
}
/// The OS name of the calling thread, read without allocating.
#[cfg(any(target_vendor = "apple", target_os = "linux"))]
fn thread_name(buf: &mut [u8; 64]) -> &[u8] {
// SAFETY: writes at most `buf.len()` bytes, NUL included, into `buf`.
let ok = unsafe {
libc::pthread_getname_np(libc::pthread_self(), buf.as_mut_ptr().cast(), buf.len())
} == 0;
let len = buf.iter().position(|&b| b == 0).unwrap_or(0);
if ok { &buf[..len] } else { &[] }
}
#[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
fn thread_name(_: &mut [u8; 64]) -> &[u8] {
&[]
}
/// The calling thread's own tag: its name, looked up again for its first
/// allocations. Allocations made while it registers a name are untagged.
fn thread_tag() -> u32 {
let known = THREAD.get();
if known != NONE && NAMING.get() >= 256 {
return known;
}
if BUSY.replace(true) {
return UNTAGGED;
}
NAMING.set(NAMING.get().saturating_add(1));
let mut buf = [0u8; 64];
let name = match std::str::from_utf8(thread_name(&mut buf)) {
Ok("") | Err(_) => "unnamed thread",
Ok(name) => name,
};
let tag = if known != NONE && slot_name(known) == Some(name) {
known
} else {
// Thread names are few: each is leaked once.
let name: &'static str = match name {
"unnamed thread" => "unnamed thread",
name => Box::leak(name.to_owned().into_boxed_str()),
};
slot(name)
};
THREAD.set(tag);
BUSY.set(false);
tag
}
fn slot_name(i: u32) -> Option<&'static str> {
let i = i as usize;
let ptr = NAME_PTR.get(i)?.load(Relaxed);
if ptr.is_null() {
return None;
}
// SAFETY: registered slots hold leaked or static UTF-8 of that length.
Some(unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, NAME_LEN[i].load(Relaxed)))
})
}
fn current() -> u32 {
match STEP.get() {
NONE => thread_tag(),
step => step,
}
}
/// While it lives, the calling thread's blocks are tagged `name`.
pub struct Tag(u32);
impl Drop for Tag {
fn drop(&mut self) {
STEP.set(self.0);
}
}
/// Tag the calling thread's blocks `name` until the guard drops.
pub fn tag(name: &'static str) -> Tag {
let previous = STEP.get();
let was = BUSY.replace(true);
let tag = slot(name);
BUSY.set(was);
STEP.set(tag);
Tag(previous)
}
/// Live bytes by tag, largest first.
pub fn census() -> Vec<(&'static str, isize)> {
let mut by_name: Vec<(&'static str, isize)> = Vec::new();
for i in 0..NEXT.load(Relaxed).min(TAGS) {
let bytes = LIVE[i].load(Relaxed);
let name = slot_name(i as u32).unwrap_or("untagged");
match by_name.iter_mut().find(|(n, _)| *n == name) {
Some(entry) => entry.1 += bytes,
None => by_name.push((name, bytes)),
}
}
by_name.sort_by_key(|(_, bytes)| std::cmp::Reverse(*bytes));
by_name
}
/// mimalloc with a tag in front of each block (see the module).
pub struct CensusAllocator;
/// The header in front of a block of `layout`: 16 bytes, or its alignment.
fn header(layout: Layout) -> usize {
layout.align().max(16)
}
// SAFETY: forwards every call to mimalloc with the header added; the tag sits
// in the header's last four bytes, inside the block mimalloc returned.
unsafe impl GlobalAlloc for CensusAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let h = header(layout);
let Ok(outer) = Layout::from_size_align(layout.size() + h, layout.align().max(16)) else {
return std::ptr::null_mut();
};
// SAFETY: `outer` has a nonzero size.
let base = unsafe { mimalloc::MiMalloc.alloc(outer) };
if base.is_null() {
return base;
}
let tag = current();
LIVE[tag as usize].fetch_add(layout.size() as isize, Relaxed);
// SAFETY: `h` bytes precede the caller's block inside the allocation.
unsafe {
let block = base.add(h);
block.sub(4).cast::<u32>().write_unaligned(tag);
block
}
}
unsafe fn dealloc(&self, block: *mut u8, layout: Layout) {
let h = header(layout);
// SAFETY: `block` came from `alloc` with this layout.
unsafe {
let tag = block.sub(4).cast::<u32>().read_unaligned();
LIVE[tag as usize].fetch_sub(layout.size() as isize, Relaxed);
let outer =
Layout::from_size_align_unchecked(layout.size() + h, layout.align().max(16));
mimalloc::MiMalloc.dealloc(block.sub(h), outer);
}
}
unsafe fn realloc(&self, block: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let h = header(layout);
// SAFETY: `block` came from `alloc` with this layout; the header moves with it.
unsafe {
let outer =
Layout::from_size_align_unchecked(layout.size() + h, layout.align().max(16));
let tag = block.sub(4).cast::<u32>().read_unaligned();
let base = mimalloc::MiMalloc.realloc(block.sub(h), outer, new_size + h);
if base.is_null() {
return base;
}
LIVE[tag as usize].fetch_add(new_size as isize - layout.size() as isize, Relaxed);
base.add(h)
}
}
}
#[cfg(test)]
mod tests {
use super::{CensusAllocator, census, tag};
use std::alloc::{GlobalAlloc, Layout};
fn live(name: &str) -> isize {
census().iter().find(|(n, _)| *n == name).map_or(0, |e| e.1)
}
#[test]
fn a_block_counts_for_its_step_until_another_thread_frees_it() {
let layout = Layout::from_size_align(1 << 20, 64).unwrap();
let block = {
let _tag = tag("test.step");
// SAFETY: a nonzero layout.
unsafe { CensusAllocator.alloc(layout) }
};
assert_eq!(live("test.step"), 1 << 20);
// SAFETY: `block` came from this allocator with `layout`.
let block = unsafe { CensusAllocator.realloc(block, layout, 3 << 20) } as usize;
assert_eq!(live("test.step"), 3 << 20, "growing keeps the step's tag");
let grown = Layout::from_size_align(3 << 20, 64).unwrap();
std::thread::spawn(move || {
let _other = tag("test.other");
// SAFETY: `block` came from this allocator with `grown`.
unsafe { CensusAllocator.dealloc(block as *mut u8, grown) };
})
.join()
.unwrap();
assert_eq!(live("test.step"), 0);
assert_eq!(live("test.other"), 0);
}
#[test]
fn without_a_step_a_block_counts_for_its_thread() {
let layout = Layout::from_size_align(4096, 8).unwrap();
let (name, block) = std::thread::Builder::new()
.name("census-test".into())
.spawn(move || {
// SAFETY: a nonzero layout.
let block = unsafe { CensusAllocator.alloc(layout) } as usize;
(std::thread::current().name().unwrap().to_owned(), block)
})
.unwrap()
.join()
.unwrap();
// Linux keeps 15 bytes of a thread's name; the test's is shorter.
assert_eq!(live(&name), 4096);
// SAFETY: `block` came from this allocator with `layout`.
unsafe { CensusAllocator.dealloc(block as *mut u8, layout) };
assert_eq!(live(&name), 0);
}
}
+10 -5
View File
@@ -65,6 +65,10 @@ pub trait Source: Send + Sync {
fn idle(&self, _worker: usize) {}
/// Every worker is idle: free what the workers share.
fn all_idle(&self) {}
/// What a heap-census build tags its jobs' allocations (`crate::heap_tag`).
fn label(&self) -> &'static str {
std::any::type_name::<Self>()
}
}
/// How long a worker waits with no job before it frees its buffers.
@@ -161,10 +165,7 @@ static POOL: OnceLock<Pool> = OnceLock::new();
/// but one, which the render and server threads share with them (they run
/// ahead of the workers: [`crate::favour_current`], [`crate::behind_server`]).
pub fn default_threads() -> usize {
std::thread::available_parallelism()
.map_or(2, |n| n.get())
.saturating_sub(1)
.max(1)
crate::worker_count().saturating_sub(1).max(1)
}
/// Size the process's pool before its first use (later calls change
@@ -384,7 +385,11 @@ fn work(inner: &Inner, worker: usize) {
}
// A panicking job loses its own work, not the worker: the panic
// hook has already reported it.
let _ = catch_unwind(AssertUnwindSafe(|| entry.source.run(worker)));
let _ = catch_unwind(AssertUnwindSafe(|| {
let _tag =
crate::HEAP_CENSUS.then(|| crate::heap_census::tag(entry.source.label()));
entry.source.run(worker)
}));
if far {
inner.far.fetch_sub(1, Ordering::SeqCst);
}
+65 -1
View File
@@ -4,10 +4,36 @@
/// The game window's boot screen, before wgpu's first frame paints over it.
#[cfg(target_os = "windows")]
pub mod boot_paint;
pub mod heap_census;
pub mod jobs;
pub mod memory_limit;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{
OnceLock,
atomic::{AtomicBool, Ordering},
};
/// The worker threads the machine offers this process: `available_parallelism`'s
/// answer (its share of the cores — CPU affinity, a container's quota), resolved
/// once, since it doesn't change under a run and asking again on a later path
/// can only disagree with the pools already started. `HORIZON_WORKER_THREADS`
/// sets it instead, to cap every pool on a machine whose OS reports more cores
/// than a run should take; a value that isn't a positive number is ignored.
/// Each pool keeps its own share of the count (the streaming pool all but one,
/// far decoration a third of it).
pub fn worker_count() -> usize {
static COUNT: OnceLock<usize> = OnceLock::new();
*COUNT.get_or_init(|| {
parse_worker_count(std::env::var("HORIZON_WORKER_THREADS").ok())
.unwrap_or_else(|| std::thread::available_parallelism().map_or(1, |n| n.get()))
})
}
/// `Some(n)` when `value` names a positive count (`" 4 "` counts; `"0"`,
/// `"lots"`, a missing variable don't).
fn parse_worker_count(value: Option<String>) -> Option<usize> {
value?.trim().parse().ok().filter(|&n| n > 0)
}
/// Ask the OS to run the calling thread ahead of the process's other
/// threads, as vanilla's `MinecraftServer.spin` does for its server thread
@@ -207,6 +233,18 @@ pub fn catch_stop_signals() {}
/// 16% of a chunk's generation time), mimalloc keeps a heap per thread.
pub use mimalloc::MiMalloc as Allocator;
/// The global allocator of a build with the `alloc-census` feature: live
/// heap bytes by startup step and thread, for `horizon-ctl memory`.
pub use heap_census::CensusAllocator;
/// Whether this build counts the heap by tag (feature `alloc-census`).
pub const HEAP_CENSUS: bool = cfg!(feature = "alloc-census");
/// Tag the calling thread's allocations `name` while the guard lives, in a
/// build that counts the heap ([`HEAP_CENSUS`]); nothing otherwise.
#[inline]
pub fn heap_tag(name: &'static str) -> Option<heap_census::Tag> {
HEAP_CENSUS.then(|| heap_census::tag(name))
}
/// `mi_option_purge_delay` in mimalloc.h (index 15 in v2 and v3); the bindings
/// don't name it.
const PURGE_DELAY: libmimalloc_sys::mi_option_t = 15;
@@ -390,6 +428,32 @@ pub fn keep_stdio_from_children() {
#[cfg(test)]
mod tests {
/// Without `HORIZON_WORKER_THREADS`, the count is the machine's.
#[test]
fn worker_count_defaults_to_the_machine() {
if std::env::var_os("HORIZON_WORKER_THREADS").is_some() {
return;
}
let machine = std::thread::available_parallelism().map_or(1, |n| n.get());
assert_eq!(super::worker_count(), machine);
}
/// Only a positive number overrides the machine's count.
#[test]
fn worker_count_override_parses() {
for (value, parsed) in [
(None, None),
(Some("4"), Some(4)),
(Some(" 4 "), Some(4)),
(Some("0"), None),
(Some("-2"), None),
(Some("lots"), None),
(Some(""), None),
] {
assert_eq!(super::parse_worker_count(value.map(String::from)), parsed);
}
}
/// After it, this process's standard output is no longer handed to the processes it starts.
#[cfg(windows)]
#[test]
+28 -2
View File
@@ -1,5 +1,7 @@
# Native mods
API @API:compute_fov_modifier@ adds `native_compute_fov_modifier(player, base, modifier)` on the client each tick for the local player's FOV scale, after vanilla effects and before zoom and camera easing. `player` contains the calculation inputs and `base` is the original scale. Non-finite modifiers or values outside 0.01..=10 fall back to `base`. It does not affect a spectator camera on a mob. Bundles need "hooks": @API:compute_fov_modifier@.
Native mods are **mixin bundles** that change the game's own Rust source before it is compiled, with
[horizon-mixins](https://github.com/horizon-rust/horizon-mixins) (MIT, by levzzz5154, made for Horizon). A bundle
is a `horizon-mixin.toml` and a few `.rs` helpers that hook into named functions of the game (HEAD, BEFORE_CALL,
@@ -11,6 +13,10 @@ switch. The first install shows a trust notice, and every build waits behind a r
after a review (its later releases are checked by machine); and they play in a game of their own, built for the
instance they are installed in.
## Item frame rendering
`native_render_item_in_frame(entity_id, pos, stack, canceled)` runs before a visible item frame draws its item or map, including glow and invisible frames. Set `canceled` to hide the item without hiding the frame. Bundles need "hooks": @API:render_item_in_frame@.
## Villager farming
Farmers recognize mod blocks made from farmland and `CropBlock` templates, including declarations from
@@ -779,6 +785,10 @@ API 86 adds `native_block_highlight(pos, state, partial, canceled)` runs before
API 87 adds `native_render_hand(hand, stack, partial, swing, equip, canceled)` runs before each visible first-person hand: `Hand::Main` or `Hand::Off`, the displayed stack (None for an empty arm), partial tick, swing progress and inverse equip height including swap animation scale. Set `canceled` to skip the default geometry for that hand (arms and maps included). It runs after the existing spectator, hidden-HUD, sleep and camera gates. It only hides: the hook has no way to draw a replacement. Bundles need "hooks": 87.
`native_render_hand_pose` runs after cancellation for each visible first-person hand, including maps and the empty main-hand arm (the same path as `RenderArmEvent`). Add to `x`, `y`, `z` in blocks and `pitch`, `yaw`, `roll` in degrees, and multiply `scale` to compose with other mods. The pose is applied in hand view space before the ordinary hand and item poses, in translation, pitch, yaw, roll, scale order. Non-finite or out-of-range components are ignored individually: offsets must be within -16..=16 blocks, angles within -3600..=3600 degrees and scale within 0.01..=100. Bundles need "hooks": @API:render_hand_pose@.
`native_screen_opening(screen)`, `native_screen_init_post(screen)` and `native_screen_closing(screen)` observe screens once per frame with `NativeScreenView`, including sign editors, book edit/sign/view and lectern screens, dialogs and prompts. Overlay views use the full window as their rectangle and have no menu or title; anvil rename remains part of its inventory screen. Dialog states share the `dialog` id. Screens opened and closed within one frame are missed; cancelling an opening is not possible because there is no single transition point. Bundles need "hooks": @API:screen_opening@.
API 88 adds `native_client_resources_reloaded()` fires on the render thread after successful client resource-pack application: the atlas, models, renderer and client caches already use the new resources. It covers selection/F3+T and server-pack reloads. Failed staging or application and initial startup do not fire it. This is a completion notification, not a reload-listener registration or staging API. Bundles need "hooks": 88.
API 89 adds `native_item_decorations(stack, x, y, batch)` appends custom GUI drawing after the default bar, cooldown shade and count have been painted for an item icon. The stack is the displayed stack, x/y are its icon's top-left in GUI units, the units the batch takes (no item-local translation remains active). Existing GUI helpers can draw badges, text and overlays. The shared path covers the hotbar, carried stacks, menu slots, recipe book and item tooltip icons, and the item icons of the advancement, beacon, stats and trading screens. `native_item_bar` still controls the bar separately; this hook does not suppress vanilla decorations. Bundles need "hooks": 89.
@@ -1945,7 +1955,8 @@ For mods that run where the world is simulated: a dedicated server, a LAN host,
`minecraft:water`, `minecraft:lava` or `minecraft:powder_snow`; fish and axolotl buckets pour water) into `pos`,
right before the fluid is placed. Set `cancel` and nothing is poured. The player's state is swapped in, so
`native_tell` answers them. `native_fill_bucket` hears the same click without the player and the fluid; dispensers
and cauldrons are not heard.
and cauldrons are not heard. A fluid or mob bucket is also heard at the block beside the clicked face whenever that
block is not `pos`, since a block that refuses the fluid passes it there; the fluid may still go into `pos`.
- **`native_player_bed_enter(player, name, pos, cancel)`**: a server player passed the sleep checks and is about to
enter the bed at its head position. Set `cancel` to keep them out; helpers that look them up through `self.lan` may
find nothing while a guest packet is handled.
@@ -2217,7 +2228,9 @@ For mods that run where the world is simulated: a dedicated server, a LAN host,
before any is hit; take out the ones to spare. Entities are hurt as before.
- **`native_fill_bucket(stack, pos, canceled)`** and **`native_attack_entity(entity, kind, pos, canceled)`** (API 22):
a player's bucket at a block, and their blow on an entity, with the acting player swapped in. Canceled, the bucket
does nothing and the blow does no harm.
does nothing and the blow does no harm. A fluid or mob bucket also asks about the block beside the clicked face
whenever that block is not its target, since a block that refuses the fluid passes it there; the fluid may still
stay in the target.
### Mobs
@@ -2525,6 +2538,13 @@ render thread with the frame's wgpu objects; what a mod draws there shows in tha
colour target (copy it out before sampling it), the world's reversed depth and its size, the camera's position and
its matrices with and without TAA's jitter, the frame's camera-relative entity quads with their runs by entity, the
entity atlas and the shared quad index buffer. Not called on the title screen's panorama.
- `native_hand_drawn(hand)`: the first-person hand and held item are drawn, after `native_world_drawn` and before
entity outlines, post effects, screen effects and the HUD, on every frame `native_world_drawn` runs. `HandDrawn`
holds the device, queue and encoder, the frame's colour target with the hand in it, the hand's camera-relative
quads (empty when no hand drew: third person, F1, a shader pack's own hand), the hand's clip matrix (its own field
of view and the view bob, no jitter), the camera, the entity atlas and the quad index buffer. The hand's depth isn't
kept: draw its quads into a depth of your own. A motion blur can match the hand's quads with last frame's for the
swing, attack, equip and bob motion, and blur the picture with the hand in it.
- `native_render_needs(needs)`: set `entity_spans` to have the entity geometry split into runs by entity without a
shader pack (each run's id is stable from frame to frame: a hash of the entity type's name, a block entity's state),
and `world_depth` to keep the world pass's depth when nothing else needs it. Set what you need and leave the rest:
@@ -2942,3 +2962,9 @@ The following catalog hooks are not in the trait yet:
- Newer upstream hooks `biome_colors`, `camera_distance`, `compute_fog_color`, `compute_fov`,
`data_pack_files`, `mob_submit`, `mouse_button_pre`, `mouse_button_post`, `player_respawned`
and `server_data_loaded` will join a later slice.
The client `tooltip_color` hook changes the background and top and bottom border gradient colours as mutable ARGB `u32` values; it does not change text colours. It covers solid-fill item tooltips (including creative, recipe book, chat, trading and bundle-selected item names), screen text boxes, and native panel tooltips. The stack is `None` for text-only boxes, including recipe-book item lines where the stack is discarded before drawing. Sprite-based screen tooltips (including options and title screens) use a separate renderer and are not covered; no texture hook is offered.
# Camera and collision reads
The `native_camera()` helper returns the most recently extracted world frame's camera, or none before one exists. Its position, interpolated eye height, partial tick, angles, final FOV, near/far planes and dimension accompany column-major view and projection matrices. The view is camera-relative and includes bob and spin; projection uses reversed Z. Earlier callbacks in a frame can see the previous frame. `native_block_collision(pos)` returns block-local `[min_x, min_y, min_z, max_x, max_y, max_z]` boxes with an empty collision context; unloaded blocks and collision errors give no boxes.
+140 -93
View File
@@ -1960,9 +1960,9 @@ dependencies = [
[[package]]
name = "gpui-fast"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87b6d2f355531132e8f1ec357cb23901aba1d1b3e522260822ae199f02ec32da"
checksum = "7ee67914f779fdcfce4c7f59f6ba8fa68f680bd137648b25a801cd5d9cb1b8ab"
dependencies = [
"accesskit",
"anyhow",
@@ -1971,7 +1971,6 @@ dependencies = [
"backtrace",
"bitflags 2.13.2",
"chrono",
"ctor",
"derive_more",
"embed-resource",
"etagere",
@@ -1986,6 +1985,7 @@ dependencies = [
"gpui-pre-sum-tree",
"gpui-pre-util",
"heapless",
"icu_segmenter",
"image",
"inventory",
"itertools",
@@ -2015,6 +2015,7 @@ dependencies = [
"taffy",
"thiserror 2.0.21",
"ttf-parser",
"unicode-segmentation",
"url",
"usvg",
"uuid",
@@ -2025,9 +2026,9 @@ dependencies = [
[[package]]
name = "gpui-fast-apple"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f0aff0a75a63953782a609bf023c68cdb71c3dbd74478223db9bbd1b744795"
checksum = "6cdf402e4a795abb27cd9545a6b519f123f4f2b8050fc7f6e7e25e376f34867d"
dependencies = [
"anyhow",
"async-task",
@@ -2037,10 +2038,8 @@ dependencies = [
"core-foundation-sys",
"core-graphics 0.24.0",
"core-text",
"derive_more",
"dispatch2",
"etagere",
"foreign-types",
"futures",
"gpui-fast",
"gpui-pre-collections",
@@ -2049,9 +2048,9 @@ dependencies = [
"libc",
"log",
"mach2",
"metal",
"objc2 0.6.5",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-text",
"objc2-core-video",
"objc2-foundation 0.3.2",
@@ -2067,9 +2066,9 @@ dependencies = [
[[package]]
name = "gpui-fast-linux"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d5a4351392d339049584e06a58460a4907a060e3f46e49b7c130d80ffe36871"
checksum = "59c465cccb420a54d2abc14af42ce82ecb1ea473b531ddbd299eb4e15ae2dd83"
dependencies = [
"accesskit",
"accesskit_unix",
@@ -2112,9 +2111,9 @@ dependencies = [
[[package]]
name = "gpui-fast-macos"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8e8b377d6dcbc0007e68098cb3f40bc2591a1d29526301e9572a70bb674f171"
checksum = "d809f618f836aff7fc654c00c3f6e64d41abd83713140998dd83e8ad6906b1e9"
dependencies = [
"accesskit",
"accesskit_macos",
@@ -2140,6 +2139,8 @@ dependencies = [
"objc2-core-graphics",
"objc2-core-video",
"objc2-foundation 0.3.2",
"objc2-quartz-core 0.3.2",
"objc2-uniform-type-identifiers",
"objc2-user-notifications",
"parking_lot",
"raw-window-handle",
@@ -2151,9 +2152,9 @@ dependencies = [
[[package]]
name = "gpui-fast-macros"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e07e9e22196a84bb8cb06c19fc6e5d2218fa11d6b573f914f423970a1b38c97e"
checksum = "46724abd05d2c874cc31e1996bb862539c5735780dd363cb550469003a6e5140"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -2163,9 +2164,9 @@ dependencies = [
[[package]]
name = "gpui-fast-platform"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c4b2959631b713899ae6d58236a0ac7c7e404a6cd8c16f30822177eb90a5b59"
checksum = "eb75fcc655a819d87a75180f32528d26b869b421a53c5974046c6e2948c46ecf"
dependencies = [
"anyhow",
"console_error_panic_hook",
@@ -2179,9 +2180,9 @@ dependencies = [
[[package]]
name = "gpui-fast-web"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad903fddb78ae4c8f1c24b7fa07c79a0697da7614183d8c7dad67bd296802901"
checksum = "f5194d398f801160d6be5660055a14b175aed9b2dbca348d56065d742b11dbd9"
dependencies = [
"anyhow",
"async-channel",
@@ -2209,9 +2210,9 @@ dependencies = [
[[package]]
name = "gpui-fast-wgpu"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebf72d3c1fee16dde347707ef306940d39656f80e35d3aaace01075996f12199"
checksum = "7a3be892e9d079745739d0e52e397fd43b61d215de4fd1477bb554a9bcf0c7e1"
dependencies = [
"anyhow",
"bytemuck",
@@ -2236,9 +2237,9 @@ dependencies = [
[[package]]
name = "gpui-fast-windows"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1a193343ef7faaa18eb9ccc0aadf8ec9571f2791c4419c2ca23aa17198f970"
checksum = "088654835f1408e1aaa79afcc316530461a14fa1c4e543d47b1881b6ced63f19"
dependencies = [
"accesskit",
"accesskit_windows",
@@ -2266,9 +2267,9 @@ dependencies = [
[[package]]
name = "gpui-pre"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df4b5f8d0c278308df2f11e0bfe0212dd77d7848ce1bdc4b9d513334414396f6"
checksum = "9303c771abf2acff2a96d706f69d9a82088c4770173a07cce88f0002431d887d"
dependencies = [
"accesskit",
"anyhow",
@@ -2277,7 +2278,6 @@ dependencies = [
"backtrace",
"bitflags 2.13.2",
"chrono",
"ctor",
"derive_more",
"etagere",
"futures",
@@ -2291,6 +2291,7 @@ dependencies = [
"gpui-pre-sum-tree",
"gpui-pre-util",
"heapless",
"icu_segmenter",
"image",
"inventory",
"itertools",
@@ -2320,6 +2321,7 @@ dependencies = [
"taffy",
"thiserror 2.0.21",
"ttf-parser",
"unicode-segmentation",
"url",
"usvg",
"uuid",
@@ -2329,9 +2331,9 @@ dependencies = [
[[package]]
name = "gpui-pre-apple"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e97cd068d8134b4e073321b946491760aa1e4511e486084a92e73b9174bff78a"
checksum = "8c0bea8ffa563d3a1ada533e148bb55be7fb873c178979e889a3d1204f8e1b91"
dependencies = [
"anyhow",
"async-task",
@@ -2341,10 +2343,8 @@ dependencies = [
"core-foundation-sys",
"core-graphics 0.24.0",
"core-text",
"derive_more",
"dispatch2",
"etagere",
"foreign-types",
"futures",
"gpui-pre",
"gpui-pre-collections",
@@ -2353,9 +2353,9 @@ dependencies = [
"libc",
"log",
"mach2",
"metal",
"objc2 0.6.5",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-text",
"objc2-core-video",
"objc2-foundation 0.3.2",
@@ -2371,9 +2371,9 @@ dependencies = [
[[package]]
name = "gpui-pre-collections"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f89bf8941a7437dd761161c552543fb8268b9633fe74e29eafbe0014cd2164b"
checksum = "06882d54d40681ae3b145af9ef8a4021c2bf9967cc0a9fcf0cc50f003bf3cdf3"
dependencies = [
"gpui-pre-util",
"indexmap",
@@ -2382,9 +2382,9 @@ dependencies = [
[[package]]
name = "gpui-pre-derive-refineable"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2998b1cebbaa0fa004fe4939a513bbf4103c7fb69ae2e84ee73634554e3f7edc"
checksum = "3999591b50cfd74f1734baf97fe802cce5f282d60d85b908e3393d534fbd50fb"
dependencies = [
"proc-macro2",
"quote",
@@ -2393,16 +2393,15 @@ dependencies = [
[[package]]
name = "gpui-pre-http-client"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e516820ee56ac357a87ff97b1a2c5f22c8d5dcd2a50cdd15c16949f24464d98e"
checksum = "a00ff0e68341f52c5903182d7e2e2d7dc247d87b0d9947ce3564e453720347a0"
dependencies = [
"anyhow",
"bytes",
"futures",
"http",
"http-body",
"log",
"parking_lot",
"serde",
"serde_json",
@@ -2412,9 +2411,9 @@ dependencies = [
[[package]]
name = "gpui-pre-ios"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5773a1e3a399df64f3ecc5538727fd0575279bd190959cf97a9e2b698c0310b8"
checksum = "326382c3b3f004617ac0f61338594c00f053b08b7875f3486ea0f221e9b20a75"
dependencies = [
"anyhow",
"block2 0.6.2",
@@ -2434,9 +2433,9 @@ dependencies = [
[[package]]
name = "gpui-pre-macros"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c95d70a9fe10701533606c6135f9a1d4cd96e87e4e6062b3d426eb57b415daf"
checksum = "ee1c52959baa6834db3183e86affe70242ca90b4fc64ed2ae6dd689a799ce78e"
dependencies = [
"heck 0.5.0",
"proc-macro-crate",
@@ -2447,18 +2446,18 @@ dependencies = [
[[package]]
name = "gpui-pre-refineable"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ce7e83592bc6c1ead2affd91b333f11b60c3eb7d06cae1605dd56f149fe8674"
checksum = "097375d98fecafd6e00ec13c7711709cc6d827ccad385694ce9e2c2f570c02a5"
dependencies = [
"gpui-pre-derive-refineable",
]
[[package]]
name = "gpui-pre-scheduler"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87c2c6b0fabcd20e6773a0be30afa1f25637c53a06201bb80ab24a21fe198865"
checksum = "93c3c9f76341cdf03801699d68876915b42d5858e80c52a141ebfc350358147b"
dependencies = [
"async-task",
"backtrace",
@@ -2473,9 +2472,9 @@ dependencies = [
[[package]]
name = "gpui-pre-shared-string"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8068f94355f173558a51998a32985a434af54cc6c7262a060fffc369a505998f"
checksum = "781aaf9a9c19e3367b94100c7b1a17283721766bc890909ce8aa34e8c5b1a99a"
dependencies = [
"schemars",
"serde",
@@ -2484,9 +2483,9 @@ dependencies = [
[[package]]
name = "gpui-pre-sum-tree"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ce66c57e9b5fd6c7a90fa9f9de2f9bb342127d670ca3c8fe5885270079eefa"
checksum = "e0cb18765472fb2c64b7e6374f9a36f512ce25b1ccb3662fff51788564107eb3"
dependencies = [
"gpui-pre-ztracing",
"heapless",
@@ -2496,9 +2495,9 @@ dependencies = [
[[package]]
name = "gpui-pre-util"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162dc29b2d2c0223ee4bb4e9f5e913eab4f62cad0943eb0c1122a74ac82b0768"
checksum = "49036ad9887034f8d6652b65761ee1c1fdbd7d66be33a6088245fae54dc643e3"
dependencies = [
"anyhow",
"log",
@@ -2507,9 +2506,9 @@ dependencies = [
[[package]]
name = "gpui-pre-ztracing"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "814cc08c526961c88703806a68ba4cf216a1d988ffbbdc673304f4dd25858952"
checksum = "7ac9274f4fb36ce6bf128f9b48032196a3120cb858503af33ced6c21366cf2f7"
dependencies = [
"gpui-pre-ztracing-macro",
"tracing",
@@ -2517,9 +2516,9 @@ dependencies = [
[[package]]
name = "gpui-pre-ztracing-macro"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f9dae92b9bc15baa6e530c90bdd4c689d3710bd4d18c3566deb1d6ae182daf"
checksum = "99b18b06f5026b3d312488eacfdddbe78f82b4bf32bb2d2bc3186e31e552b797"
[[package]]
name = "half"
@@ -2882,9 +2881,9 @@ dependencies = [
[[package]]
name = "icu_collections"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd"
dependencies = [
"displaydoc",
"potential_utf",
@@ -2896,22 +2895,43 @@ dependencies = [
[[package]]
name = "icu_locale_core"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0"
dependencies = [
"displaydoc",
"litemap",
"serde",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.3.0"
name = "icu_locale_fallback"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
checksum = "406e9ae5b95d8aa37db6293405931b766a6172d1848daee22d70eb44aa78a43c"
dependencies = [
"icu_locale_core",
"icu_locale_fallback_data",
"icu_provider",
"potential_utf",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locale_fallback_data"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3632ebbd0c5c3637ba1a40bdf8288f2cb9d2268e73441a69d5fc73d7cfbaa87c"
[[package]]
name = "icu_normalizer"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599"
dependencies = [
"icu_collections",
"icu_normalizer_data",
@@ -2923,15 +2943,15 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc"
[[package]]
name = "icu_properties"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f"
dependencies = [
"displaydoc",
"icu_collections",
@@ -2944,18 +2964,20 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf"
[[package]]
name = "icu_provider"
version = "2.3.1"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514"
dependencies = [
"displaydoc",
"icu_locale_core",
"serde",
"stable_deref_trait",
"writeable",
"yoke",
"zerofrom",
@@ -2963,6 +2985,29 @@ dependencies = [
"zerovec",
]
[[package]]
name = "icu_segmenter"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61b195dc605f81aa181bec9b3bd14e26cc71b903e4644f6af578ca9772c7a710"
dependencies = [
"icu_collections",
"icu_locale_fallback",
"icu_provider",
"icu_segmenter_data",
"potential_utf",
"smallvec",
"utf8_iter",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_segmenter_data"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "913a7d294ad056474749152107309fdee3ec17d2482d1ab70fdc61a81c64b9a3"
[[package]]
name = "idna"
version = "1.1.0"
@@ -3473,21 +3518,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "metal"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15"
dependencies = [
"bitflags 2.13.2",
"block",
"core-graphics-types 0.2.0",
"foreign-types",
"log",
"objc",
"paste",
]
[[package]]
name = "minisign-verify"
version = "0.3.0"
@@ -3833,6 +3863,7 @@ dependencies = [
"objc2 0.6.5",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"objc2-uniform-type-identifiers",
]
[[package]]
@@ -3971,6 +4002,7 @@ checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
dependencies = [
"bitflags 2.13.2",
"block2 0.6.2",
"dispatch2",
"objc2 0.6.5",
"objc2-foundation 0.3.2",
]
@@ -4025,6 +4057,16 @@ dependencies = [
"objc2-quartz-core 0.3.2",
]
[[package]]
name = "objc2-uniform-type-identifiers"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7902ac02859fc1f7045f8b598c63f1ae0cc7efeaa06a9bc9f3d9a3c955974fa4"
dependencies = [
"objc2 0.6.5",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-user-notifications"
version = "0.3.2"
@@ -4419,6 +4461,8 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
dependencies = [
"serde_core",
"writeable",
"zerovec",
]
@@ -5759,11 +5803,12 @@ dependencies = [
[[package]]
name = "tinystr"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a"
dependencies = [
"displaydoc",
"serde_core",
"zerovec",
]
@@ -7354,9 +7399,9 @@ dependencies = [
[[package]]
name = "yoke"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3"
dependencies = [
"stable_deref_trait",
"yoke-derive",
@@ -7618,14 +7663,16 @@ dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "zerovec"
version = "0.11.8"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40"
dependencies = [
"serde",
"yoke",
"zerofrom",
"zerovec-derive",
@@ -7633,9 +7680,9 @@ dependencies = [
[[package]]
name = "zerovec-derive"
version = "0.11.6"
version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a"
dependencies = [
"proc-macro2",
"quote",
+2 -2
View File
@@ -10,8 +10,8 @@ description = "Downloads, prepares and starts Horizon builds"
[dependencies]
# GPUI Fast (longbridge/gpui-fast): Zed's GPUI with retained views and cached scroll layers, under
# its own name. wgpu renders on Linux, not blade (#3812).
gpui = { package = "gpui-fast", version = "0.1.12" }
gpui_platform = { package = "gpui-fast-platform", version = "0.1.12", features = ["wayland", "x11", "font-kit"] }
gpui = { package = "gpui-fast", version = "0.1.13" }
gpui_platform = { package = "gpui-fast-platform", version = "0.1.13", features = ["wayland", "x11", "font-kit"] }
ureq = { version = "2", features = ["json", "tls"], default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+3
View File
@@ -214,6 +214,9 @@
"Starting": "Startet",
"Show in folder": "Im Ordner anzeigen",
"Settings": "Einstellungen",
"Settings were not saved.": "Die Einstellungen wurden nicht gespeichert.",
"Your changes are still in this window. Retry to save them. {error}": "Deine Änderungen sind noch in diesem Fenster vorhanden. Versuche erneut, sie zu speichern. {error}",
"Retry": "Erneut versuchen",
"Doesn't port": "Wird nicht portiert",
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
"Could not read {name}: {error}": "{name} konnte nicht gelesen werden: {error}",
+3
View File
@@ -214,6 +214,9 @@
"Starting": "Iniciando",
"Show in folder": "Mostrar en la carpeta",
"Settings": "Ajustes",
"Settings were not saved.": "No se han guardado los ajustes.",
"Your changes are still in this window. Retry to save them. {error}": "Tus cambios siguen en esta ventana. Vuelve a intentarlo para guardarlos. {error}",
"Retry": "Reintentar",
"Doesn't port": "No se porta",
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Nunca se envía: el nombre o UUID de tu cuenta, nombres de mundos, direcciones de servidores, chat ni tu dirección IP. Cada informe lleva solo la fecha del día, bajo un ID de instalación aleatorio que puedes eliminar.",
"Could not read {name}: {error}": "No se pudo leer {name}: {error}",
+3
View File
@@ -214,6 +214,9 @@
"Starting": "Démarrage",
"Show in folder": "Afficher dans le dossier",
"Settings": "Paramètres",
"Settings were not saved.": "Les paramètres n’ont pas été enregistrés.",
"Your changes are still in this window. Retry to save them. {error}": "Vos modifications sont toujours dans cette fenêtre. Réessayez pour les enregistrer. {error}",
"Retry": "Réessayer",
"Doesn't port": "Non porté",
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
"Could not read {name}: {error}": "Impossible de lire {name} : {error}",
+3
View File
@@ -5,6 +5,9 @@
"Versions": "バージョン",
"Patch notes": "更新履歴",
"Settings": "設定",
"Settings were not saved.": "設定を保存できませんでした。",
"Your changes are still in this window. Retry to save them. {error}": "変更内容はこのウィンドウに残っています。もう一度保存をお試しください。{error}",
"Retry": "再試行",
"Accounts, updates, and the launcher itself.": "アカウント、アップデート、ランチャーの設定",
"Account": "アカウント",
"Updates": "アップデート",
+3
View File
@@ -214,6 +214,9 @@
"Starting": "Uruchamianie",
"Show in folder": "Pokaż w folderze",
"Settings": "Ustawienia",
"Settings were not saved.": "Nie udało się zapisać ustawień.",
"Your changes are still in this window. Retry to save them. {error}": "Twoje zmiany nadal są w tym oknie. Spróbuj ponownie je zapisać. {error}",
"Retry": "Spróbuj ponownie",
"Doesn't port": "Nieprzenoszone",
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
"Could not read {name}: {error}": "Nie udało się odczytać {name}: {error}",
+3
View File
@@ -5,6 +5,9 @@
"Versions": "Versões",
"Patch notes": "Notas da atualização",
"Settings": "Configurações",
"Settings were not saved.": "As configurações não foram salvas.",
"Your changes are still in this window. Retry to save them. {error}": "Suas alterações ainda estão nesta janela. Tente novamente para salvá-las. {error}",
"Retry": "Tentar novamente",
"Accounts, updates, and the launcher itself.": "Contas, atualizações e o próprio launcher.",
"Account": "Conta",
"Updates": "Atualizações",
+3
View File
@@ -214,6 +214,9 @@
"Starting": "Запуск",
"Show in folder": "Показать в папке",
"Settings": "Настройки",
"Settings were not saved.": "Не удалось сохранить настройки.",
"Your changes are still in this window. Retry to save them. {error}": "Ваши изменения остаются в этом окне. Попробуйте сохранить их ещё раз. {error}",
"Retry": "Повторить",
"Doesn't port": "Не портируется",
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
"Could not read {name}: {error}": "Не удалось прочитать {name}: {error}",
+3
View File
@@ -5,6 +5,9 @@
"Versions": "Sürümler",
"Patch notes": "Sürüm Notları",
"Settings": "Ayarlar",
"Settings were not saved.": "Ayarlar kaydedilemedi.",
"Your changes are still in this window. Retry to save them. {error}": "Değişiklikleriniz hâlâ bu pencerede duruyor. Kaydetmek için tekrar deneyin. {error}",
"Retry": "Tekrar dene",
"Accounts, updates, and the launcher itself.": "Hesaplar, güncellemeler ve başlatıcının kendisi.",
"Account": "Hesap",
"Updates": "Güncellemeler",
+3
View File
@@ -214,6 +214,9 @@
"Starting": "Запуск",
"Show in folder": "Показати в папці",
"Settings": "Налаштування",
"Settings were not saved.": "Не вдалося зберегти налаштування.",
"Your changes are still in this window. Retry to save them. {error}": "Ваші зміни залишаються в цьому вікні. Спробуйте зберегти їх ще раз. {error}",
"Retry": "Повторити",
"Doesn't port": "Не портується",
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
"Could not read {name}: {error}": "Не вдалося прочитати {name}: {error}",
+3
View File
@@ -214,6 +214,9 @@
"Starting": "正在启动",
"Show in folder": "在文件夹中显示",
"Settings": "设置",
"Settings were not saved.": "设置未保存。",
"Your changes are still in this window. Retry to save them. {error}": "你的更改仍保留在此窗口中。请重试以保存更改。{error}",
"Retry": "重试",
"Doesn't port": "未移植",
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
"Could not read {name}: {error}": "无法读取 {name}:{error}",
+11 -5
View File
@@ -2317,7 +2317,7 @@ pub fn remove_version(tag: &str) -> Result<(), String> {
let mut settings = Settings::load();
if settings.selected.as_deref() == Some(tag) {
settings.selected = installed().first().cloned();
settings.save();
settings.save()?;
}
Ok(())
}
@@ -2666,7 +2666,7 @@ pub fn install_tag(tag: &str, as_update: bool, progress: impl Fn(f32)) -> Result
let mut settings = Settings::load();
if !as_update || select_after_update(settings.selected.as_deref(), &before) {
settings.selected = Some(release.tag.clone());
settings.save();
settings.save()?;
}
if as_update && release.nightly.is_some() {
crate::nightly::after_update(&release.tag);
@@ -3400,19 +3400,25 @@ impl Settings {
pub fn keep_newest(&self) -> usize {
self.keep_versions.unwrap_or(KEEP_NEWEST).max(1)
}
pub fn save(&self) {
pub fn save(&self) -> Result<(), String> {
// The window and background jobs share the process's temporary file.
static SAVE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = SAVE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
// A write that stops halfway leaves settings.json unreadable, and `load` answers
// defaults for it: the file is replaced only once it is whole.
let dir = data_dir();
let path = dir.join("settings.json");
let tmp = dir.join(format!("settings.{}.tmp", std::process::id()));
let json =
serde_json::to_vec_pretty(self).map_err(|e| format!("{}: {e}", path.display()))?;
let written = fs::create_dir_all(&dir)
.and_then(|()| fs::write(&tmp, serde_json::to_string_pretty(self).unwrap()))
.and_then(|()| fs::write(&tmp, json))
.and_then(|()| fs::rename(&tmp, &path));
if let Err(e) = written {
let _ = fs::remove_file(&tmp);
eprintln!("Horizon: settings.json is not saved: {e}");
return Err(format!("{}: {e}", path.display()));
}
Ok(())
}
}
+1 -1
View File
@@ -114,7 +114,7 @@ pub fn import(path: &Path, requested: Option<&str>) -> Result<String, String> {
// Read again: the window may have saved other settings during the copy.
let mut settings = backend::Settings::load();
settings.selected = Some(tag.clone());
settings.save();
settings.save()?;
Ok(tag)
}
+20 -19
View File
@@ -1599,7 +1599,7 @@ fn execute(command: Cmd, as_json: bool) -> Result<(), String> {
if let Some(key) = key {
let value = value.ok_or("settings KEY VALUE: the value is missing")?;
set_setting(&mut settings, &key, &value)?;
settings.save();
settings.save()?;
horizon_launcher::proxy::announce(settings.proxy.clone());
}
let all = settings_json(&settings);
@@ -1615,7 +1615,7 @@ fn execute(command: Cmd, as_json: bool) -> Result<(), String> {
let mut settings = backend::Settings::load();
if let Some(line) = args {
settings.game_args = line;
settings.save();
settings.save()?;
}
let line = settings.game_args;
if as_json {
@@ -1677,7 +1677,7 @@ fn execute(command: Cmd, as_json: bool) -> Result<(), String> {
Cmd::DesktopEntry { remove } => {
let mut settings = backend::Settings::load();
settings.desktop_entry = Some(!remove);
settings.save();
settings.save()?;
if remove {
horizon_launcher::desktop::remove()?;
println!("Removed Horizon from the applications menu.");
@@ -2273,7 +2273,15 @@ fn github_native(
.collect::<Result<Vec<_>, _>>()
});
github_mods::discard(found);
for o in done? {
let done = done?;
// The window's trust notice, once. Nothing is turned on: native mods are always on.
let mut settings = backend::Settings::load();
if !settings.native_mods_noticed {
eprintln!("{}", native_status::NOTICE);
settings.native_mods_noticed = true;
settings.save()?;
}
for o in done {
println!(
"Installed {} {} from GitHub ({}), sha256 {}.",
o.id,
@@ -2282,13 +2290,6 @@ fn github_native(
o.sha256
);
}
// The window's trust notice, once. Nothing is turned on: native mods are always on.
let mut settings = backend::Settings::load();
if !settings.native_mods_noticed {
eprintln!("{}", native_status::NOTICE);
settings.native_mods_noticed = true;
settings.save();
}
println!("Build it next: horizon-launcher native-build --trust-native-code");
println!(
"Developing a mod? horizon-launcher settings native-profile dev (or native-build --profile dev): a mod edit rebuilds about 6× faster, while game logic runs up to about twice as slow. Use it while developing, not for playing."
@@ -2575,6 +2576,14 @@ fn mods(action: ModsCmd, as_json: bool) -> Result<(), String> {
Some(target) => java_instance::install(&*source, target, &p, &home, &line)?,
None => installed::install(&*source, &p, everything, &home, &line)?,
};
// The first native mod: the window's trust notice, once.
let mut settings = backend::Settings::load();
let native = done.iter().any(|e| native_status::is_native_file(&e.file));
if native && !settings.native_mods_noticed {
eprintln!("{}", native_status::NOTICE);
settings.native_mods_noticed = true;
settings.save()?;
}
if as_json {
json(done.iter().map(entry_json).collect());
}
@@ -2584,14 +2593,6 @@ fn mods(action: ModsCmd, as_json: bool) -> Result<(), String> {
println!(" not ported: {s}");
}
}
// The first native mod: the window's trust notice, once.
let mut settings = backend::Settings::load();
let native = done.iter().any(|e| native_status::is_native_file(&e.file));
if native && !settings.native_mods_noticed {
eprintln!("{}", native_status::NOTICE);
settings.native_mods_noticed = true;
settings.save();
}
}
ModsCmd::Update { project, check, .. } => {
let list = installed::Installed::load(&home).entries;
+1 -1
View File
@@ -128,7 +128,7 @@ impl Launcher {
// The question and Settings say what the "I'm playing" ping holds, beside the switches.
if !self.settings.ping_notice_seen {
self.settings.ping_notice_seen = true;
self.settings.save();
self.save_settings();
}
self.collection.view = None;
}
+1 -1
View File
@@ -30,7 +30,7 @@ impl Launcher {
let toggle = switch("dev-offline", self.settings.dev_offline, &t).on_click(cx.listener(
|this, _: &ClickEvent, _, cx| {
this.settings.dev_offline = !this.settings.dev_offline;
this.settings.save();
this.save_settings();
cx.notify();
},
));
+1 -1
View File
@@ -125,7 +125,7 @@ impl Launcher {
self.github.newer.clear();
// The review above was the trust notice; the Mods page offers Build.
self.settings.native_mods_noticed = true;
self.settings.save();
self.save_settings();
}
self.instance_mods_changed();
self.github.message = Some(result);
+1 -1
View File
@@ -183,7 +183,7 @@ impl Launcher {
.child(tr("Got it"))
.on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
this.settings.ping_notice_seen = true;
this.settings.save();
this.save_settings();
cx.notify();
})),
),
+14 -5
View File
@@ -380,6 +380,13 @@ fn find_in(all: &[Instance], query: &str) -> Result<Instance, String> {
/// and forgets it: its choices, and the selection when Play started it. Horizon's own instance,
/// the game's home, stays.
pub fn delete(query: &str) -> Result<Instance, String> {
let (gone, saved) = delete_with_selection(query)?;
saved?;
Ok(gone)
}
/// A completed deletion stays completed even if saving the new selection fails.
pub fn delete_with_selection(query: &str) -> Result<(Instance, Result<(), String>), String> {
let gone = delete_in(
&backend::game_home(),
&backend::data_dir(),
@@ -387,11 +394,13 @@ pub fn delete(query: &str) -> Result<Instance, String> {
&crate::desktop_seams::to_trash,
)?;
let mut settings = backend::Settings::load();
if settings.instance.as_ref() == Some(&gone.id) {
let saved = if settings.instance.as_ref() == Some(&gone.id) {
settings.instance = None;
settings.save();
}
Ok(gone)
settings.save()
} else {
Ok(())
};
Ok((gone, saved))
}
fn delete_in(
@@ -429,7 +438,7 @@ pub fn select(query: &str) -> Result<Instance, String> {
let instance = find(query)?;
let mut settings = backend::Settings::load();
settings.instance = (instance.kind != Kind::Default).then(|| instance.id.clone());
settings.save();
settings.save()?;
Ok(instance)
}
+7 -4
View File
@@ -116,7 +116,7 @@ impl Launcher {
self.native_update = None;
self.settings.instance =
(instance.kind != InstanceKind::Default).then(|| instance.id.clone());
self.settings.save();
self.save_settings();
}
/// A texture from the selected version's prepared files, its first frame, drawn as pixels.
@@ -849,13 +849,16 @@ impl Launcher {
if self.client.is_some() || self.starting_client.is_some() {
return;
}
match instances::delete(id) {
Ok(_) => {
match instances::delete_with_selection(id) {
Ok((_, saved)) => {
self.instances.confirm_delete = None;
self.instances.delete_error = None;
// Play starts Horizon again; the window's copy of the settings says so too.
if self.settings.instance.as_deref() == Some(id) {
self.settings.instance = None;
self.settings.save();
self.save_settings();
} else if let Err(error) = saved {
self.settings_save_error = Some(error);
}
self.reload_instances();
}
+1 -1
View File
@@ -85,7 +85,7 @@ impl Launcher {
})
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
this.settings.language = choice;
this.settings.save();
this.save_settings();
i18n::set(this.settings.language());
this.language_menu = false;
cx.notify();
+77 -20
View File
@@ -243,6 +243,8 @@ struct Launcher {
/// Why the last picture for Home's background was refused.
background_error: Option<String>,
settings: Settings,
/// The last failed settings write, kept across page changes until a save succeeds.
settings_save_error: Option<String>,
/// The Discord, sponsor and website links: built in until links.json on main has been read.
links: backend::Links,
preparing: bool,
@@ -413,7 +415,7 @@ impl Launcher {
s.partner_seen_secs += 10;
if s.partner_seen_secs % 60 == 0 || s.partner_seen_secs >= partner::SEEN_BEFORE_HIDE
{
s.save();
this.save_settings();
cx.notify();
}
true
@@ -511,6 +513,7 @@ impl Launcher {
proxy_focused: false,
proxy_focus: cx.focus_handle(),
settings,
settings_save_error: None,
links: backend::Links::default(),
preparing: false,
prepare_log: VecDeque::new(),
@@ -636,6 +639,53 @@ impl Launcher {
this
}
/// Keeps the current choices in memory if writing fails; every page offers the same retry.
fn save_settings(&mut self) -> bool {
self.settings_save_error = self.settings.save().err();
self.settings_save_error.is_none()
}
fn settings_save_notice(&self, cx: &mut Context<Self>) -> Option<Div> {
let error = self.settings_save_error.as_ref()?;
let t = self.theme;
Some(
div()
.flex_none()
.px(px(theme::PAGE_GUTTER))
.py(px(12.))
.child(
crate::plate::plate(&t.night)
.flex()
.items_center()
.gap(px(16.))
.p(px(16.))
.child(
div()
.relative()
.flex_1()
.min_w_0()
.child(
shell::text(theme::night::UI, t.danger)
.child(tr("Settings were not saved.")),
)
.child(
shell::text(theme::night::CAPTION, t.ink_soft).child(trf(
"Your changes are still in this window. Retry to save them. {error}",
&[("error", error)],
)),
),
)
.child(
button("settings-save-retry", "Retry", Kind::Secondary, &t)
.on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
this.save_settings();
cx.notify();
})),
),
),
)
}
fn go(&mut self, page: Page) {
// The game may have made up a name since, and the CLI or another window may have
// changed the accounts: read them again (small files, once a switch).
@@ -1030,12 +1080,14 @@ impl Launcher {
if as_update.is_none_or(|(_, select)| select) {
self.settings.selected = Some(tag);
}
self.settings.save();
// After the save: the selected version is never pruned.
let tx = self.tx.clone();
std::thread::spawn(move || {
let _ = tx.send(Msg::Pruned(backend::prune_old_versions()));
});
// Pruning reads settings from disk: protect the selection only once
// it has been saved there. A failed save leaves all versions in place.
if self.save_settings() {
let tx = self.tx.clone();
std::thread::spawn(move || {
let _ = tx.send(Msg::Pruned(backend::prune_old_versions()));
});
}
}
Err(e) => self.install_error = Some((tag, e)),
}
@@ -1043,8 +1095,12 @@ impl Launcher {
Msg::Removed(tag, result) => {
self.removing.retain(|t| *t != tag);
self.installed = backend::installed();
self.settings.selected =
selection_after_removal(&self.installed, Settings::load().selected);
let selected = if self.settings_save_error.is_some() {
self.settings.selected.take()
} else {
Settings::load().selected
};
self.settings.selected = selection_after_removal(&self.installed, selected);
if let Err(e) = result {
self.install_error = Some((tag, e));
}
@@ -1242,7 +1298,7 @@ impl Launcher {
self.client_error = None;
if !self.settings.owner_notice_seen {
self.settings.owner_notice_seen = true;
self.settings.save();
self.save_settings();
}
}
changed = true;
@@ -1642,7 +1698,7 @@ impl Launcher {
if let Some(path) = paths.pop() {
let _ = this.update(cx, |this, cx| {
this.settings.minecraft_dir = Some(path);
this.settings.save();
this.save_settings();
cx.notify();
});
}
@@ -2615,7 +2671,7 @@ impl Launcher {
let hide = partner::hide_button("partner-hide", &t).on_click(cx.listener(
|this, _: &ClickEvent, _, cx| {
this.settings.partner_hidden = true;
this.settings.save();
this.save_settings();
cx.notify();
},
));
@@ -3263,7 +3319,7 @@ impl Launcher {
let toggle = switch("auto-install", self.settings.auto_install(), &t).on_click(
cx.listener(|this, _: &ClickEvent, _, cx| {
this.settings.install_new_versions = Some(!this.settings.auto_install());
this.settings.save();
this.save_settings();
if this.settings.auto_install() {
this.install_update();
}
@@ -3273,14 +3329,14 @@ impl Launcher {
let prune = switch("auto-prune", self.settings.auto_prune(), &t).on_click(cx.listener(
|this, _: &ClickEvent, _, cx| {
this.settings.prune_old_versions = Some(!this.settings.auto_prune());
this.settings.save();
this.save_settings();
cx.notify();
},
));
let opening = switch("opening", !self.settings.skip_opening, &t).on_click(cx.listener(
|this, _: &ClickEvent, _, cx| {
this.settings.skip_opening = !this.settings.skip_opening;
this.settings.save();
this.save_settings();
cx.notify();
},
));
@@ -3289,7 +3345,7 @@ impl Launcher {
cx.listener(|this, _: &ClickEvent, _, cx| {
let on = !this.settings.desktop_entry();
this.settings.desktop_entry = Some(on);
this.settings.save();
this.save_settings();
let _ = if on {
horizon_launcher::desktop::install(backend::exe_path()).map(|_| ())
} else {
@@ -3390,7 +3446,7 @@ impl Launcher {
move |choice, window, cx| {
let _ = this.update(cx, |this, cx| {
this.settings.appearance = choice;
this.settings.save();
this.save_settings();
titlebar::apply_appearance(window, choice);
this.theme = this.settings_theme(window);
cx.notify();
@@ -3655,7 +3711,7 @@ impl Launcher {
line_edit::Key::Escape => window.blur(cx),
line_edit::Key::Edited => {
this.settings.game_args = this.args_input.text().to_string();
this.settings.save();
this.save_settings();
}
_ => {}
}
@@ -3747,7 +3803,7 @@ impl Launcher {
fn save_proxy(&mut self) {
let value = self.proxy_input.text().trim().to_owned();
self.settings.proxy = (!value.is_empty()).then_some(value);
self.settings.save();
self.save_settings();
horizon_launcher::proxy::announce(self.settings.proxy.clone());
}
@@ -4054,7 +4110,7 @@ impl Launcher {
button(format!("use-{tag}"), "Use", Kind::Ghost, &t).on_click(cx.listener(
move |this, _: &ClickEvent, _, cx| {
this.settings.selected = Some(t1.clone());
this.settings.save();
this.save_settings();
cx.notify();
},
)),
@@ -4831,6 +4887,7 @@ impl Render for Launcher {
.bg(t.side)
.child(sky)
.children(titlebar::render(&t, window))
.children(self.settings_save_notice(cx))
.child(root.flex_1().min_h_0())
.children(titlebar::resize_handles(window))
}
+2 -2
View File
@@ -1092,7 +1092,7 @@ impl Launcher {
button("native-notice-install", "Install", Kind::Primary, &t).on_click(
cx.listener(|this, _: &ClickEvent, _, cx| {
this.settings.native_mods_noticed = true;
this.settings.save();
this.save_settings();
if let Some((source, id, known)) =
this.mods.native_notice.take()
{
@@ -1548,7 +1548,7 @@ impl Launcher {
this.settings.native_profile = Some(choice);
this.settings.native_release = false;
this.native_done = None;
this.settings.save();
this.save_settings();
cx.notify();
});
}
+1 -1
View File
@@ -35,7 +35,7 @@ impl Launcher {
self.owner_screen_open = false;
if !self.settings.owner_notice_seen {
self.settings.owner_notice_seen = true;
self.settings.save();
self.save_settings();
}
}
+1 -1
View File
@@ -1182,7 +1182,7 @@ impl Launcher {
super::partner::hide_button("partner-servers-hide", &t).on_click(cx.listener(
|this, _: &ClickEvent, _, cx| {
this.settings.partner_hidden = true;
this.settings.save();
this.save_settings();
cx.notify();
},
)),
+12 -6
View File
@@ -60,7 +60,7 @@ impl Launcher {
})
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
this.settings.accent = accent;
this.settings.save();
this.save_settings();
this.theme = this.settings_theme(window);
cx.notify();
})),
@@ -154,13 +154,19 @@ impl Launcher {
}))
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _: &MouseUpEvent, _, _| this.finish_hue()),
cx.listener(|this, _: &MouseUpEvent, _, cx| {
this.finish_hue();
cx.notify();
}),
)
// `on_mouse_up` fires only while the strip is hovered, so a drag released off it would
// keep its accent in memory and never write it.
.on_mouse_up_out(
MouseButton::Left,
cx.listener(|this, _: &MouseUpEvent, _, _| this.finish_hue()),
cx.listener(|this, _: &MouseUpEvent, _, cx| {
this.finish_hue();
cx.notify();
}),
)
}
@@ -168,7 +174,7 @@ impl Launcher {
fn finish_hue(&mut self) {
if self.hue_dragging {
self.hue_dragging = false;
self.settings.save();
self.save_settings();
}
}
@@ -215,7 +221,7 @@ impl Launcher {
match themes::keep_background(&backend::data_dir(), path) {
Ok(name) => {
self.settings.background = Some(name);
self.settings.save();
self.save_settings();
self.background_error = None;
}
Err(e) => self.background_error = Some(e),
@@ -242,7 +248,7 @@ impl Launcher {
button("background-reset", "Landscape", Kind::Ghost, &t).on_click(cx.listener(
|this, _: &ClickEvent, _, cx| {
this.settings.background = None;
this.settings.save();
this.save_settings();
cx.notify();
},
)),
+5 -2
View File
@@ -24,7 +24,10 @@ impl Launcher {
/// The game started: get out of its way as the player asked. Read from `settings.json`, so
/// `horizon-launcher settings while-playing …` counts at once.
pub(super) fn step_aside(&mut self, cx: &mut Context<Self>) {
self.settings.while_playing = Settings::load().while_playing;
// Keep a choice whose save failed available for Retry instead of replacing it from disk.
if self.settings_save_error.is_none() {
self.settings.while_playing = Settings::load().while_playing;
}
let how = self.settings.while_playing.here();
if how == WhilePlaying::Stay {
return;
@@ -67,7 +70,7 @@ impl Launcher {
move |choice, _, cx| {
let _ = this.update(cx, |this, cx| {
this.settings.while_playing = choice;
this.settings.save();
this.save_settings();
cx.notify();
});
}
+1 -1
View File
@@ -26,7 +26,7 @@
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<string>12.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
+33
View File
@@ -0,0 +1,33 @@
import unittest
from tools import check_cpu_baseline as baseline
# llvm-objdump -d --no-show-raw-insn output: a stripped Windows program (only section labels) and an ELF one.
DISASSEMBLY = """
/tmp/horizon-client.exe:\tfile format coff-x86-64
Disassembly of section .text:
0000000140001000 <.text>:
140001000: \tpopcntq\t%rdx, %rax
140001005: \ttzcntl\t%ecx, %eax
14000100a: \tvpaddd\t%ymm0, %ymm1, %ymm2
0000000140002000 <mi_bitmap_popcount>:
140002000: \tlzcntq\t%r8, %r9
140002005: \tmovq\t%rax, %rbx
140002008: \tpopcnt\trax, rcx
"""
class ForbiddenUsesTests(unittest.TestCase):
def test_reports_popcnt_and_lzcnt_with_their_symbol_and_nothing_else(self):
uses = list(baseline.forbidden_uses(DISASSEMBLY.splitlines(keepends=True)))
self.assertEqual(uses, [
("140001000", ".text", "popcnt"),
("140002000", "mi_bitmap_popcount", "lzcnt"),
("140002008", "mi_bitmap_popcount", "popcnt"),
])
if __name__ == "__main__":
unittest.main()
+9
View File
@@ -83,6 +83,15 @@ class ClaimTests(unittest.TestCase):
do("/claim", api)
self.assertEqual(api.calls, [])
def test_trackers_and_proposals_are_not_claimed(self):
for name in ("Tracker", "proposal", "not claimable"):
t = thread()
t["labels"] = [{"name": "bug"}, {"name": name}]
api = Fake({5: t})
with self.assertRaisesRegex(cc.Refused, "isn't claimed as a whole"):
do("/claim", api)
self.assertEqual(api.calls, [], name)
def test_already_yours(self):
with self.assertRaisesRegex(cc.Refused, "already hold"):
do("/claim", Fake({5: thread(holders=("NewComer",))}))
+2 -1
View File
@@ -13,7 +13,8 @@ class RenderHandHookTests(unittest.TestCase):
def test_each_hand_can_be_cancelled(self):
source = (ROOT / "crates/horizon-client/src/game.rs").read_text()
start = source.index(" let mut canceled = false;", source.index("fn hand_geometry("))
end = source.index(" // The main hand on its arm", start)
# The pose hook after it (render_hand_pose) is not part of the cancellation dispatch.
end = source.index(" let (mut x, mut y, mut z, mut pitch", start)
dispatch = source[start:end]
self.assertLess(source.index("if !shown[i]", source.index("fn hand_geometry(")), start)
self.assertLess(end, source.index("let Some(stack) = item", end))
+79
View File
@@ -0,0 +1,79 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2018"
name = "libmimalloc-sys"
version = "0.1.49"
authors = ["Octavian Oncescu <[email protected]>"]
build = "build.rs"
links = "mimalloc"
exclude = [
"/c_src/mimalloc/v2/bin",
"/c_src/mimalloc/v2/cmake",
"/c_src/mimalloc/v2/doc",
"/c_src/mimalloc/v2/docs",
"/c_src/mimalloc/v2/ide",
"/c_src/mimalloc/v2/test",
"/c_src/mimalloc/v3/bin",
"/c_src/mimalloc/v3/cmake",
"/c_src/mimalloc/v3/doc",
"/c_src/mimalloc/v3/docs",
"/c_src/mimalloc/v3/ide",
"/c_src/mimalloc/v3/test",
]
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Sys crate wrapping the mimalloc allocator"
readme = false
keywords = [
"allocator",
"encrypted-heap",
"performance",
]
categories = [
"memory-management",
"api-bindings",
]
license = "MIT"
repository = "https://github.com/purpleprotocol/mimalloc_rust/tree/master/libmimalloc-sys"
[package.metadata.docs.rs]
features = ["extended"]
[features]
arena = []
debug = []
debug_in_debug = []
extended = ["cty"]
local_dynamic_tls = []
no_thp = []
override = []
secure = []
v2 = []
win_direct_tls = []
[lib]
name = "libmimalloc_sys"
path = "src/lib.rs"
[dependencies.cty]
version = "0.2"
optional = true
[dev-dependencies.libc]
version = "0.2"
[build-dependencies.cc]
version = "1.2"
+25
View File
@@ -0,0 +1,25 @@
Copyright 2019 Octavian Oncescu
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.
+138
View File
@@ -0,0 +1,138 @@
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
fn main() {
let mut build = cc::Build::new();
// Horizon vendors only mimalloc v3 (THIRD_PARTY_NOTICES.md).
assert!(
env::var("CARGO_FEATURE_V2").is_err(),
"third_party/libmimalloc-sys holds only mimalloc v3: the v2 feature is not available"
);
let version = "v3";
let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
let include_root = Path::new(&cargo_manifest_dir)
.join("c_src")
.join("mimalloc")
.join(version);
let include_dir = include_root
.join("include")
.to_str()
.expect("include path is not valid UTF-8")
.to_string();
let static_source = include_root.join("src").join("static.c");
// Make the include directory available to consumers via the `DEP_MIMALLOC_INCLUDE_DIR`
// environment variable.
println!("cargo:INCLUDE_DIR={include_dir}");
build.include(format!("c_src/mimalloc/{version}/include"));
build.include(format!("c_src/mimalloc/{version}/src"));
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("target_os not defined!");
let target_family = env::var("CARGO_CFG_TARGET_FAMILY").expect("target_family not defined!");
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
let target_vendor = env::var("CARGO_CFG_TARGET_VENDOR").expect("target_vendor not defined!");
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("target_arch not defined!");
let cargo_debug = env::var("DEBUG")
.map(|value| value == "true" || value == "1")
.unwrap_or(false);
let debug_enabled = env::var_os("CARGO_FEATURE_DEBUG").is_some()
|| (env::var_os("CARGO_FEATURE_DEBUG_IN_DEBUG").is_some() && cargo_debug);
if target_family != "windows" {
build.flag("-Wno-error=date-time");
}
if target_env == "msvc" {
// Mimalloc expects the MSVC/clang-cl build to use the C++ atomics path.
build.cpp(true);
build.std("c++17");
build.flag_if_supported("/Zc:__cplusplus");
let wrapper =
PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set")).join("mimalloc-static.cc");
let include = static_source.to_string_lossy().replace('\\', "/");
fs::write(&wrapper, format!("#include \"{include}\"\n"))
.expect("failed to write mimalloc C++ wrapper");
build.file(wrapper);
} else {
build.file(&static_source);
}
let compiler = build.get_compiler();
if env::var_os("CARGO_FEATURE_OVERRIDE").is_some() {
// Overriding malloc is only available on windows in shared mode, but we
// only ever build a static lib.
if target_family != "windows" {
build.define("MI_MALLOC_OVERRIDE", None);
}
if target_vendor == "apple" {
build.define("MI_OSX_ZONE", Some("1"));
build.define("MI_OSX_INTERPOSE", Some("1"));
}
if !compiler.is_like_msvc() {
build.flag_if_supported("-fno-builtin-malloc");
}
}
if env::var_os("CARGO_FEATURE_SECURE").is_some() {
build.define("MI_SECURE", "4");
}
if target_os == "windows" && env::var_os("CARGO_FEATURE_WIN_DIRECT_TLS").is_some() {
build.define("MI_WIN_DIRECT_TLS", "1");
}
let dynamic_tls = env::var("CARGO_FEATURE_LOCAL_DYNAMIC_TLS").is_ok();
if target_family == "unix" && target_os != "haiku" {
if dynamic_tls {
build.flag_if_supported("-ftls-model=local-dynamic");
} else {
build.flag_if_supported("-ftls-model=initial-exec");
}
}
if (target_os == "linux" || target_os == "android")
&& env::var_os("CARGO_FEATURE_NO_THP").is_some()
{
build.define("MI_NO_THP", "1");
}
if debug_enabled {
build.define("MI_DEBUG", "3");
build.define("MI_SHOW_ERRORS", "1");
} else {
// Remove heavy debug assertions etc.
build.define("MI_DEBUG", "0");
if !cargo_debug {
build.define("MI_BUILD_RELEASE", None);
build.define("NDEBUG", None);
}
}
build.compile("mimalloc");
// on armv6 we need to link with libatomic
if target_os == "linux" && target_arch == "arm" {
// Embrace the atomic capability library across various platforms.
// For instance, on certain platforms, llvm has relocated the atomic of the arm32 architecture to libclang_rt.builtins.a
// while some use libatomic.a, and others use libatomic_ops.a.
let atomic_name = env::var("DEP_ATOMIC").unwrap_or("atomic".to_owned());
println!("cargo:rustc-link-lib={}", atomic_name);
}
// Link with libs needed on Windows
if target_os == "windows" {
// https://github.com/microsoft/mimalloc/blob/af21001f7a65eafb8fb16460b018ebf9d75e2ad8/CMakeLists.txt#L487
let libs = ["psapi", "shell32", "user32", "advapi32", "bcrypt"];
for lib in libs {
println!("cargo:rustc-link-lib={}", lib);
}
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018-2025 Microsoft Corporation, Daan Leijen
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,66 @@
/* ----------------------------------------------------------------------------
Copyright (c) 2018-2020 Microsoft Research, Daan Leijen
This is free software; you can redistribute it and/or modify it under the
terms of the MIT license. A copy of the license can be found in the file
"LICENSE" at the root of this distribution.
-----------------------------------------------------------------------------*/
#pragma once
#ifndef MIMALLOC_NEW_DELETE_H
#define MIMALLOC_NEW_DELETE_H
// ----------------------------------------------------------------------------
// This header provides convenient overrides for the new and
// delete operations in C++.
//
// This header should be included in only one source file!
//
// On Windows, or when linking dynamically with mimalloc, these
// can be more performant than the standard new-delete operations.
// See <https://en.cppreference.com/w/cpp/memory/new/operator_new>
// ---------------------------------------------------------------------------
#if defined(__cplusplus)
#include <new>
#include <mimalloc.h>
#if defined(_MSC_VER) && defined(_Ret_notnull_) && defined(_Post_writable_byte_size_)
// stay consistent with VCRT definitions
#define mi_decl_new(n) mi_decl_nodiscard mi_decl_restrict _Ret_notnull_ _Post_writable_byte_size_(n)
#define mi_decl_new_nothrow(n) mi_decl_nodiscard mi_decl_restrict _Ret_maybenull_ _Success_(return != NULL) _Post_writable_byte_size_(n)
#else
#define mi_decl_new(n) mi_decl_nodiscard mi_decl_restrict
#define mi_decl_new_nothrow(n) mi_decl_nodiscard mi_decl_restrict
#endif
void operator delete(void* p) noexcept { mi_free(p); };
void operator delete[](void* p) noexcept { mi_free(p); };
void operator delete (void* p, const std::nothrow_t&) noexcept { mi_free(p); }
void operator delete[](void* p, const std::nothrow_t&) noexcept { mi_free(p); }
mi_decl_new(n) void* operator new(std::size_t n) noexcept(false) { return mi_new(n); }
mi_decl_new(n) void* operator new[](std::size_t n) noexcept(false) { return mi_new(n); }
mi_decl_new_nothrow(n) void* operator new (std::size_t n, const std::nothrow_t& tag) noexcept { (void)(tag); return mi_new_nothrow(n); }
mi_decl_new_nothrow(n) void* operator new[](std::size_t n, const std::nothrow_t& tag) noexcept { (void)(tag); return mi_new_nothrow(n); }
#if (__cplusplus >= 201402L || _MSC_VER >= 1916)
void operator delete (void* p, std::size_t n) noexcept { mi_free_size(p,n); };
void operator delete[](void* p, std::size_t n) noexcept { mi_free_size(p,n); };
#endif
#if (__cplusplus > 201402L || defined(__cpp_aligned_new))
void operator delete (void* p, std::align_val_t al) noexcept { mi_free_aligned(p, static_cast<size_t>(al)); }
void operator delete[](void* p, std::align_val_t al) noexcept { mi_free_aligned(p, static_cast<size_t>(al)); }
void operator delete (void* p, std::size_t n, std::align_val_t al) noexcept { mi_free_size_aligned(p, n, static_cast<size_t>(al)); };
void operator delete[](void* p, std::size_t n, std::align_val_t al) noexcept { mi_free_size_aligned(p, n, static_cast<size_t>(al)); };
void operator delete (void* p, std::align_val_t al, const std::nothrow_t&) noexcept { mi_free_aligned(p, static_cast<size_t>(al)); }
void operator delete[](void* p, std::align_val_t al, const std::nothrow_t&) noexcept { mi_free_aligned(p, static_cast<size_t>(al)); }
void* operator new (std::size_t n, std::align_val_t al) noexcept(false) { return mi_new_aligned(n, static_cast<size_t>(al)); }
void* operator new[](std::size_t n, std::align_val_t al) noexcept(false) { return mi_new_aligned(n, static_cast<size_t>(al)); }
void* operator new (std::size_t n, std::align_val_t al, const std::nothrow_t&) noexcept { return mi_new_aligned_nothrow(n, static_cast<size_t>(al)); }
void* operator new[](std::size_t n, std::align_val_t al, const std::nothrow_t&) noexcept { return mi_new_aligned_nothrow(n, static_cast<size_t>(al)); }
#endif
#endif
#endif // MIMALLOC_NEW_DELETE_H
Loaded 100 of 203 files, more files were not shown because too many files have changed in this diff. Show more