Merge main and retain raid harness observations

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
Florian ERNSTandCodex committed 2026-11-05 19:16:54 +01:00
commit de4029c1df
278 files changed
+6719 -980

No files matched your search

+1
View File
@@ -35,6 +35,7 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
- **No hardcoded user-facing strings.** Keep UI translation-ready from the start. In the launcher (`launcher/`), wrap visible text with `tr("...")` or `trf("... {arg} ...", &[("arg", &val)])` from `crate::i18n` instead of raw string literals or `format!`. In the game, use translation keys (`Text::tr`, `lang::get`), never hardcoded English strings.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
issue is taken: pick another. `/unclaim` when you stop; a claim with no open PR linking it is released after 5 days.
- **Controllers reach what registers.** Draw controls with a registering helper (`ui::button`, `settings_shell::foot_button`); `graphics_style::button` and `screen_draw::button` only paint, and registering one part of a screen ends its crosshair, so register all of it.
## Crates
+2 -5
View File
@@ -608,14 +608,11 @@ pub enum Item {
/// The Extras page: headers (language keys) and items.
pub fn rows() -> Vec<Result<Item, &'static str>> {
use Item::*;
// Fullbright, Dynamic lights and Hide block-break particles change the picture: they
// are on the graphics screen's Quality page (`graphics_ui::rows`), not here.
let mut rows = vec![
Err("horizon.extras.header.lighting"),
Ok(Fullbright),
Ok(DynamicLights),
Err("horizon.extras.header.third_person"),
Ok(OwnNameTag),
Err("horizon.extras.header.particles"),
Ok(HideBlockParticles),
Err("horizon.extras.header.inventory"),
Ok(MouseTweaks),
Ok(MouseTweaksWheel),
@@ -1172,6 +1172,14 @@ pub fn frame_inputs(
let k = super::native_shadow::distortion(s.distance, s.resolution);
2.0 * s.distance / s.resolution.max(1) as f32 * (1.0 - k)
}),
// Shader API 0.2's display fields, which 0.1 packs don't declare: SDR's values, also
// with HDR on, until HDR output is served for 0.2 packs.
hdr: 0,
headroom: 1.0,
paper_white: 80.0,
peak: 80.0,
black_level: 0.0,
padding: [0; 3],
}
}
@@ -319,7 +319,7 @@ mod tests {
#[test]
fn a_native_weather_stage_validates() {
use horizon_shader_api::stages::StageKind;
let source = StageKind::Weather.prelude()
let source = StageKind::Weather.prelude(horizon_shader_api::Api::V0_1)
+ "fn weather(input: WeatherInput) -> vec4<f32> {\n \
return input.color * vec4(input.light, input.position.x, f32(input.snow));\n}\n";
let text = native_source(&source);
+196 -60
View File
@@ -1,6 +1,6 @@
//! Horizon's graphics settings screen: every video setting, vanilla's and
//! Horizon's, on one list of pages (General, Quality, Performance, Advanced,
//! HDR) with a page list beside it, a search field over it and a description
//! Horizon's, on one list of pages (General, Quality, Performance, Shaders,
//! Advanced, HDR) with a page list beside it, a search field over it and a description
//! of the hovered setting with its cost. Which settings it shows, in what
//! pages, with what description and cost, and its input live here; how it
//! looks comes from `graphics_style`.
@@ -24,15 +24,18 @@ pub enum Page {
General,
Quality,
Performance,
/// Shader packs, either kind (`shader_pack_list`), and what they cost.
Shaders,
Advanced,
/// HDR output's own settings.
Hdr,
}
impl Page {
pub const ALL: [Page; 5] = [
pub const ALL: [Page; 6] = [
Page::General,
Page::Quality,
Page::Performance,
Page::Shaders,
Page::Advanced,
Page::Hdr,
];
@@ -41,6 +44,7 @@ impl Page {
Page::General => "General",
Page::Quality => "Quality",
Page::Performance => "Performance",
Page::Shaders => "Shaders",
Page::Advanced => "Advanced",
Page::Hdr => "HDR",
}
@@ -51,6 +55,7 @@ impl Page {
Page::General => "How far you see, the window and the HUD.",
Page::Quality => "The sky and weather, lighting, textures and the image.",
Page::Performance => "Chunk loading, culling and power use.",
Page::Shaders => "Shader packs, Horizon's and shaders/ folder ones, and their options.",
Page::Advanced => "How frames reach the screen, and the performance overlay.",
Page::Hdr => "Brighter-than-white output on an HDR display.",
}
@@ -106,6 +111,8 @@ pub enum H {
HdrLighting,
BatteryMode,
ShadowCache,
/// Opens the shader pack list (`options_ui::Page::ShaderPacks`).
ShaderPacks,
GraphicsApi,
TranslucentBatching,
HdrSmoothGradients,
@@ -115,6 +122,8 @@ pub enum H {
pub enum Item {
Vanilla(Opt),
Horizon(H),
/// A Horizon extra that changes the picture (`extras::Item`, kept in `horizon-extras.txt`).
Extra(crate::extras::Item),
}
#[derive(Clone, Copy)]
@@ -137,6 +146,14 @@ fn h(opt: H, cost: Cost, text: &'static str) -> Setting {
text,
}
}
/// An extra's row: its description is its own language key.
fn e(item: crate::extras::Item, cost: Cost) -> Setting {
Setting {
item: Item::Extra(item),
cost,
text: item.description_key(),
}
}
/// Every page's settings, in named groups of related rows.
pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
@@ -153,11 +170,18 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
vec![
(
"Preset",
vec![h(
H::Preset,
None,
"Sets the options below together. Changing one makes it Custom.",
)],
vec![
h(
H::Preset,
None,
"Sets the options below together. Changing one makes it Custom.",
),
v(
Opt::Preset,
None,
"Vanilla's own preset: sets only vanilla's options, as Minecraft does.",
),
],
),
(
"Distance",
@@ -196,6 +220,16 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
vec![
v(Opt::GuiScale, None, "Size of menus and the HUD."),
v(Opt::Fullscreen, None, "Fills the screen (borderless)."),
v(
Opt::ExclusiveFullscreen,
None,
"Fullscreen takes the display over, at the mode below.",
),
v(
Opt::ExclusiveMode,
None,
"The display mode exclusive fullscreen uses. Applies when you leave.",
),
v(
Opt::Vsync,
None,
@@ -211,17 +245,12 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
None,
"Lowers the frame rate when minimised, or idle too.",
),
v(
Opt::Fov,
Low,
"Field of view. Wider shows more terrain at once.",
),
],
),
(
"Interface",
vec![
v(Opt::ViewBobbing, None, "Sways the view while walking."),
v(Opt::MenuBlur, Low, "Blurs the world behind menus."),
v(Opt::AttackIndicator, None, "Shows attack cooldown."),
v(Opt::Autosave, None, "Shows when the world saves."),
],
@@ -253,6 +282,7 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
"How far rain and snow fall around you.",
),
v(Opt::Particles, Medium, "How many particles effects spawn."),
e(crate::extras::Item::HideBlockParticles, None),
],
),
(
@@ -280,6 +310,8 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
),
v(Opt::Vignette, None, "Darkens the screen's edges."),
v(Opt::ChunkFade, None, "Fades newly loaded chunks in."),
e(crate::extras::Item::DynamicLights, Low),
e(crate::extras::Item::Fullbright, None),
],
),
(
@@ -290,6 +322,16 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
None,
"Smaller copies of block textures for distant terrain: less shimmer.",
),
v(
Opt::TextureFiltering,
Low,
"How textures are sampled: sharper at a slant with Anisotropic.",
),
v(
Opt::MaxAnisotropy,
Low,
"How far Anisotropic filtering goes. Higher is sharper at a slant.",
),
v(
Opt::CutoutLeaves,
Medium,
@@ -405,11 +447,6 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
Saves,
"Draws or drops leaf faces inside canopies. Off keeps vanilla's fancy leaves.",
),
h(
H::ShadowCache,
Saves,
"Reuses the pack shadow map when sun, view and world match. Off by default.",
),
],
),
(
@@ -462,6 +499,24 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
),
],
),
(
Page::Shaders,
vec![(
"Shader packs",
vec![
h(
H::ShaderPacks,
High,
"Picks a shader pack, Horizon's or a shaders/ folder one, and sets its options.",
),
h(
H::ShadowCache,
Saves,
"Reuses the pack shadow map when sun, view and world match. Off by default.",
),
],
)],
),
(
Page::Advanced,
vec![
@@ -865,6 +920,8 @@ impl H {
}
H::BatteryMode => labelled("Battery Mode", on_off(g.battery)),
H::ShadowCache => labelled("Shadow Cache", on_off(g.shadow_cache)),
// A press across the row, as Calibrate...: the list it opens shows the pick.
H::ShaderPacks => Text::literal(lang::get("Shader Packs...")),
H::GraphicsApi => labelled("Graphics API", g.api.label()),
// These two are greyed out where the device can't (`Item::blocked`): Off, as
// they are there, while the saved value waits for a device that can.
@@ -1068,6 +1125,10 @@ impl Item {
Some("Only applies to asynchronous entity culling.")
}
Item::Horizon(H::GpuCulling) => crate::graphics::gpu_culling_offer().reason(),
// VideoSettingsScreen.tick: Max Anisotropy only works with Anisotropic filtering.
Item::Vanilla(Opt::MaxAnisotropy) if s.file.int("textureFiltering") != 2 => {
Some("Only with Anisotropic texture filtering.")
}
_ => None,
}
}
@@ -1075,7 +1136,10 @@ impl Item {
fn steps(self, s: &Settings) -> bool {
self.switch(s).is_none()
&& self.slider(s).is_none()
&& !matches!(self, Item::Horizon(H::HdrKey | H::HdrCalibrate))
&& !matches!(
self,
Item::Horizon(H::HdrKey | H::HdrCalibrate | H::ShaderPacks)
)
}
/// On/off settings and their value.
pub fn switch(self, s: &Settings) -> Option<bool> {
@@ -1108,9 +1172,10 @@ impl Item {
Item::Horizon(H::HdrSmoothGradients) => {
g.hdr_smooth_gradients && crate::graphics::smooth_gradients_offered()
}
Item::Vanilla(Opt::ViewBobbing) => s.view_bobbing,
Item::Vanilla(Opt::Vsync) => s.vsync,
Item::Extra(e) => return e.switch(&s.extras),
Item::Vanilla(Opt::Fullscreen) => s.fullscreen,
Item::Vanilla(Opt::ExclusiveFullscreen) => s.file.bool("exclusiveFullscreen"),
Item::Vanilla(Opt::SmoothLighting) => s.smooth_lighting,
Item::Vanilla(Opt::EntityShadows) => s.entity_shadows,
Item::Vanilla(Opt::Vignette) => s.vignette,
@@ -1124,6 +1189,7 @@ impl Item {
match self {
Item::Vanilla(o) => o.slider_position(s),
Item::Horizon(h) => h.slider(s),
Item::Extra(_) => None,
}
}
pub fn label(self, s: &Settings) -> Text {
@@ -1134,6 +1200,10 @@ impl Item {
),
Item::Vanilla(o) => o.label(s),
Item::Horizon(h) => h.label(s),
Item::Extra(e) => Text::option_name_value(
Text::literal(e.caption_text()),
Text::literal(e.value(&s.extras)),
),
}
}
pub fn set_slider(self, s: &mut Settings, v: f32) {
@@ -1141,8 +1211,10 @@ impl Item {
return;
}
match self {
Item::Vanilla(o) => o.set_slider(s, v),
// Options.applyGraphicsPreset, as Video Settings' slider does it.
Item::Vanilla(o) => crate::options_ui::set_slider(o, s, v),
Item::Horizon(h) => h.set_slider(s, v),
Item::Extra(_) => {}
}
self.touched(s);
}
@@ -1153,6 +1225,7 @@ impl Item {
match self {
Item::Vanilla(o) => o.cycle(s, back, framebuffer),
Item::Horizon(h) => h.cycle(s, back),
Item::Extra(e) => e.cycle(&mut s.extras, back),
}
self.touched(s);
}
@@ -1160,36 +1233,39 @@ impl Item {
fn touched(self, s: &mut Settings) {
let presetless = matches!(
self,
Item::Horizon(
H::Preset
| H::BlueMapAutoDetect
| H::BlueMapRadius
| H::Overlay
| H::ScreenshotQuality
| H::Pacing
| H::PresentMode
| H::Workers
| H::FrameGeneration
| H::BatteryMode
| H::ShadowCache
| H::GraphicsApi
| H::TranslucentBatching
| H::HdrSmoothGradients
| H::GpuCulling
) | Item::Vanilla(
Opt::GuiScale
| Opt::Fullscreen
| Opt::Vsync
| Opt::MaxFps
| Opt::Fov
| Opt::Gamma
| Opt::InactivityFps
| Opt::ViewBobbing
| Opt::ChunkFade
| Opt::Vignette
| Opt::Autosave
| Opt::AttackIndicator
)
Item::Extra(_)
| Item::Horizon(
H::Preset
| H::BlueMapAutoDetect
| H::BlueMapRadius
| H::Overlay
| H::ScreenshotQuality
| H::Pacing
| H::PresentMode
| H::Workers
| H::FrameGeneration
| H::BatteryMode
| H::ShadowCache
| H::GraphicsApi
| H::TranslucentBatching
| H::HdrSmoothGradients
| H::GpuCulling
)
| Item::Vanilla(
Opt::GuiScale
| Opt::Fullscreen
| Opt::ExclusiveFullscreen
| Opt::ExclusiveMode
| Opt::Vsync
| Opt::MaxFps
| Opt::Gamma
| Opt::InactivityFps
| Opt::MenuBlur
| Opt::ChunkFade
| Opt::Vignette
| Opt::Autosave
| Opt::AttackIndicator
)
);
if !presetless {
s.horizon.preset = Preset::Custom;
@@ -1199,6 +1275,7 @@ impl Item {
match self {
Item::Vanilla(o) => o.slider_steps(),
Item::Horizon(h) => h.slider_steps(),
Item::Extra(_) => None,
}
}
pub fn step_slider(self, s: &mut Settings, forward: bool, width: f32) {
@@ -1224,6 +1301,8 @@ pub enum Action {
BindKey(&'static str),
/// Start HDR calibration.
Calibrate,
/// Open the shader pack list.
ShaderPacks,
Done,
}
@@ -1299,6 +1378,10 @@ impl GraphicsUi {
pub(crate) fn scroll_amount(&self) -> f32 {
self.scroll.amount
}
/// The page the plate shows while nothing is searched.
pub fn page(&self) -> Page {
self.page
}
/// The screen opens, at `page`.
pub fn open(&mut self, page: Page) {
*self = Self {
@@ -1424,8 +1507,10 @@ impl GraphicsUi {
s.vsync = b.vsync;
s.max_fps = b.max_fps;
s.inactivity_fps_limit = b.inactivity_fps_limit.clone();
s.fov = b.fov;
s.view_bobbing = b.view_bobbing;
s.menu_blur = b.menu_blur;
s.extras.fullbright = b.extras.fullbright;
s.extras.dynamic_lights = b.extras.dynamic_lights;
s.extras.hide_block_particles = b.extras.hide_block_particles;
s.attack_indicator = b.attack_indicator;
s.autosave_indicator = b.autosave_indicator;
s.clouds = b.clouds.clone();
@@ -1444,6 +1529,17 @@ impl GraphicsUi {
s.prioritize_chunk_updates = b.prioritize_chunk_updates;
// Presets set it beside the options above.
s.graphics_preset = b.graphics_preset.clone();
// The vanilla rows options.txt keeps as it read them.
for key in [
"exclusiveFullscreen",
"textureFiltering",
"maxAnisotropyBit",
] {
if let Some(value) = b.file.get(key) {
s.file.set(key, value.clone());
}
}
s.file.fullscreen_resolution = b.file.fullscreen_resolution.clone();
self.changed = false;
}
@@ -1713,6 +1809,7 @@ impl GraphicsUi {
vec![Action::ButtonClick, Action::Changed]
}
Item::Horizon(H::HdrCalibrate) => vec![Action::ButtonClick, Action::Calibrate],
Item::Horizon(H::ShaderPacks) => vec![Action::ButtonClick, Action::ShaderPacks],
Item::Horizon(H::HdrKey) => {
vec![
Action::ButtonClick,
@@ -2112,7 +2209,7 @@ mod tests {
let mut ui = GraphicsUi::default();
let cols = Columns::of(size.0, size.1);
assert!((cols.bottom - cols.top) / style::ROW >= 5.0);
let [x, y, w, h] = cols.page(4);
let [x, y, w, h] = cols.page(Page::ALL.len() - 1);
assert!(y >= 0.0 && x + w <= size.0);
// Undo, over Done, stays clear of the page list.
assert!(cols.button(1)[1] >= y + h);
@@ -2170,7 +2267,7 @@ mod tests {
.is_some_and(|r| r.item == Item::Vanilla(Opt::Vsync))
}));
ui.search.set_value("hdr");
let [x, y, w, h] = cols.page(4);
let [x, y, w, h] = cols.page(Page::ALL.len() - 1);
press(&mut ui, &mut s, (x + w / 2.0, y + h / 2.0));
assert_eq!(ui.current(&ui.lines(&s).0), Some(Page::Hdr));
let [x, y, ..] = cols.clear();
@@ -2370,19 +2467,58 @@ mod tests {
assert!(!ui.scroll.held());
}
/// The rows only vanilla's Video Settings had work here as they do there: its preset
/// applies its distances (`Options.applyGraphicsPreset`), Max Anisotropy waits for
/// Anisotropic filtering, Exclusive Fullscreen is a tick box of `options.txt`.
#[test]
fn vanilla_video_rows_work_on_the_graphics_screen() {
let mut s = Settings::default();
let preset = Item::Vanilla(Opt::Preset);
preset.set_slider(&mut s, 0.0);
assert_eq!(
(s.graphics_preset.as_str(), s.render_distance, s.cloud_range),
("fast", 8, 32)
);
preset.set_slider(&mut s, 2.0 / 3.0);
assert_eq!(
(s.graphics_preset.as_str(), s.render_distance, s.cloud_range),
("fabulous", 32, 128)
);
let anisotropy = Item::Vanilla(Opt::MaxAnisotropy);
let filtering = Item::Vanilla(Opt::TextureFiltering);
while s.file.int("textureFiltering") == 2 {
filtering.cycle(&mut s, false, FB);
}
assert!(anisotropy.blocked(&s).is_some());
while s.file.int("textureFiltering") != 2 {
filtering.cycle(&mut s, false, FB);
}
assert!(anisotropy.blocked(&s).is_none());
let exclusive = Item::Vanilla(Opt::ExclusiveFullscreen);
assert_eq!(exclusive.switch(&s), Some(false));
exclusive.cycle(&mut s, false, FB);
assert_eq!(exclusive.switch(&s), Some(true));
assert!(s.file.bool("exclusiveFullscreen"));
// The graphics extras switch their own setting.
let lights = Item::Extra(crate::extras::Item::DynamicLights);
lights.cycle(&mut s, false, FB);
assert!(s.extras.dynamic_lights);
assert_eq!(lights.switch(&s), Some(true));
}
#[test]
fn the_wheel_scrolls_and_stops_at_both_ends_without_changing_values() {
let mut s = Settings::default();
let mut ui = GraphicsUi::default();
s.fov = 90.0;
let fov = row(&mut ui, &s, Item::Vanilla(Opt::Fov));
s.gamma = 0.25;
let gamma = row(&mut ui, &s, Item::Vanilla(Opt::Gamma));
// A slider being changed doesn't take the wheel either: a controller scrolls with it.
press(&mut ui, &mut s, (fov[0] + 4.0, fov[1] + 4.0));
press(&mut ui, &mut s, (gamma[0] + 4.0, gamma[1] + 4.0));
assert!(ui.can_change_value);
let before = ui.scroll.amount;
ui.scroll(&s, SIZE, -1.0);
assert_eq!(ui.scroll.amount, before + 18.0);
assert_eq!(s.fov, 90.0);
assert_eq!(s.gamma, 0.25);
let (cols, _, content) = ui.layout(&s, SIZE.0, SIZE.1);
ui.scroll(&s, SIZE, -1000.0);
assert_eq!(ui.scroll.amount, content - (cols.bottom - cols.top));
+308 -81
View File
@@ -1637,6 +1637,9 @@ pub struct OptionsUi {
/// The list's rows as last laid out: each row's top and bottom (GUI
/// pixels), for the narration's row under the mouse.
row_bands: Vec<[f32; 2]>,
/// The Extras or PvP & HUD items that take a whole row (`wide_extras`), measured
/// when the page is drawn.
wide_extras: Vec<crate::extras::Item>,
}
impl Default for OptionsUi {
fn default() -> Self {
@@ -1693,6 +1696,7 @@ impl Default for OptionsUi {
framebuffer: [1280, 720],
split_guest: false,
row_bands: Vec::new(),
wide_extras: Vec::new(),
}
}
}
@@ -1744,6 +1748,40 @@ fn option_widget(s: &Settings, opt: Opt, x: f32, y: f32, w: f32) -> Widget {
Kind::Cycle => button(x, y, w, opt.label(s), Act::Cycle(opt)),
}
}
/// The Extras or PvP & HUD page's rows: its items under their headers.
fn extras_rows(page: Page) -> Vec<Result<crate::extras::Item, &'static str>> {
match page {
Page::Pvp => crate::pvp::rows()
.into_iter()
.map(|r| r.map(crate::extras::Item::Pvp))
.collect(),
_ => crate::extras::rows(),
}
}
/// The items of `page`'s rows whose label, with any of their first values, is wider than a
/// half-width button holds (`width` measures text with the screen's font): those take the
/// whole row, so no label slides past its edges, in any language.
fn wide_extras(s: &Settings, page: Page, width: impl Fn(&str) -> f32) -> Vec<crate::extras::Item> {
// A button's label keeps 2 px off each edge (`AbstractButton.renderString`'s margin),
// past which it slides back and forth.
let room = 150.0 - 4.0;
extras_rows(page)
.into_iter()
.filter_map(Result::ok)
.filter(|&item| {
let mut e = s.extras.clone();
(0..4).any(|_| {
let label = Text::option_name_value(
Text::literal(item.caption_text()),
Text::literal(item.value(&e)),
)
.plain();
item.cycle(&mut e, false);
width(&label) > room
})
})
.collect()
}
/// What a button opening one of Horizon's pages says on hover.
fn horizon_page_tooltip(page: Page) -> Option<&'static str> {
Some(match page {
@@ -2305,6 +2343,12 @@ impl OptionsUi {
self.calibration = Some(crate::hdr_calibration::Calibration::new(s));
self.push(Page::HdrCalibration);
}
// The Shaders page's Shader Packs...: the list of either kind, as the page
// opens it from anywhere.
Action::ShaderPacks => {
events.push(Event::ListShaderPacks);
self.push(Page::ShaderPacks);
}
Action::Done => events.extend(self.back(s)),
}
}
@@ -2337,6 +2381,11 @@ impl OptionsUi {
_ => None,
}
}
/// Whether a video settings screen is open, Horizon's or vanilla's: the fullscreen mode
/// waits for it to close, and Ctrl with the wheel steps the GUI scale.
pub fn video_open(&self) -> bool {
self.page() == Page::Video || Self::graphics_page(self.page()).is_some()
}
/// Whether the controller screen is open: a controller button pressed now may bind.
pub fn controller_open(&self) -> bool {
Self::controller_page(self.page()).is_some()
@@ -2482,8 +2531,9 @@ impl OptionsUi {
Page::SplitSetup => Vec::new(),
Page::Extras | Page::Pvp => {
// Each section's switches two to a row, as vanilla's
// OptionsList.addSmall lays out an options screen.
let extra = |item: crate::extras::Item| {
// OptionsList.addSmall lays out an options screen; one whose label would run
// past half a row in this language (`wide_extras`) gets the whole row.
let extra = |item: crate::extras::Item, width: f32| {
let value = Text::option_name_value(
Text::literal(item.caption_text()),
Text::literal(item.value(&s.extras)),
@@ -2495,7 +2545,7 @@ impl OptionsUi {
} else {
Text::literal(value)
};
let widget = button(0.0, 0.0, 150.0, label, Act::Extra(item));
let widget = button(0.0, 0.0, width, label, Act::Extra(item));
// A setting of a switch that is off does nothing: inactive,
// as vanilla greys out an option that can't apply.
if item
@@ -2507,37 +2557,29 @@ impl OptionsUi {
widget
}
};
// The PvP preset first; on the Extras page beside the way to the PvP & HUD page.
// PvP & HUD opens with its preset; Extras doesn't repeat it, nor the way there
// (Horizon Settings has it).
let preset = button(0.0, 0.0, 150.0, tr("horizon.pvp.preset"), Act::PvpPreset);
let mut rows = vec![Row::Buttons(
preset,
(self.page() == Page::Extras).then(|| {
button(
0.0,
0.0,
150.0,
tr("horizon.pages.open.pvp"),
Act::Push(Page::Pvp),
)
}),
)];
let mut rows = Vec::new();
if self.page() == Page::Pvp {
rows.push(Row::Buttons(preset, None));
}
let mut section = Vec::new();
let flush = |rows: &mut Vec<Row>, section: &mut Vec<crate::extras::Item>| {
rows.extend(
section
.chunks(2)
.map(|c| Row::Buttons(extra(c[0]), c.get(1).map(|&b| extra(b)))),
);
section.clear();
let mut half = None;
for item in section.drain(..) {
if self.wide_extras.contains(&item) {
rows.extend(half.take().map(|h| Row::Buttons(extra(h, 150.0), None)));
rows.push(Row::Buttons(extra(item, 310.0), None));
} else if let Some(h) = half.take() {
rows.push(Row::Buttons(extra(h, 150.0), Some(extra(item, 150.0))));
} else {
half = Some(item);
}
}
rows.extend(half.map(|h| Row::Buttons(extra(h, 150.0), None)));
};
let page_rows = match self.page() {
Page::Pvp => crate::pvp::rows()
.into_iter()
.map(|r| r.map(crate::extras::Item::Pvp))
.collect(),
_ => crate::extras::rows(),
};
for row in page_rows {
for row in extras_rows(self.page()) {
match row {
Err(header) => {
flush(&mut rows, &mut section);
@@ -2551,24 +2593,15 @@ impl OptionsUi {
}
Page::Horizon => {
// Two to a row, as vanilla's OptionsList.addSmall lays out
// buttons; each opens one of Horizon's settings pages.
// buttons; each opens one of Horizon's settings pages. Each page has
// one way in: graphics and shader packs are Video Settings' (the graphics
// screen), the controller is Controls', mods are the Options screen's.
let open =
|key: &str, page: Page| button(0.0, 0.0, 150.0, tr(key), Act::Push(page));
vec![
Row::Buttons(
open("horizon.pages.open.graphics", Page::Graphics),
Some(open(
"horizon.pages.open.graphics_advanced",
Page::GraphicsAdvanced,
)),
),
Row::Buttons(
open("horizon.pages.open.shader_packs", Page::ShaderPacks),
Some(open("horizon.pages.open.controller", Page::Controller)),
),
Row::Buttons(
open("horizon.pages.open.extras", Page::Extras),
Some(open("horizon.pages.open.mods", Page::Mods)),
Some(open("horizon.pages.open.pvp", Page::Pvp)),
),
Row::Buttons(
open(
@@ -2579,9 +2612,8 @@ impl OptionsUi {
Page::SplitSetup
},
),
Some(open("horizon.pages.open.pvp", Page::Pvp)),
Some(open("horizon.pages.open.studio", Page::Studio)),
),
Row::Buttons(open("horizon.pages.open.studio", Page::Studio), None),
// No AI hides the AI Agent page and everything AI.
Row::Buttons(
self.extra_button(s, crate::extras::Item::DiscordPresence),
@@ -2639,24 +2671,10 @@ impl OptionsUi {
rows
}
Page::Video => {
// Horizon's graphics settings screen, at General and at Advanced.
let mut rows = vec![Row::Buttons(
button(
0.0,
0.0,
150.0,
tr("horizon.pages.open.horizon_graphics"),
Act::Push(Page::Graphics),
),
Some(button(
0.0,
0.0,
150.0,
tr("horizon.pages.open.graphics_advanced"),
Act::Push(Page::GraphicsAdvanced),
)),
)];
rows.push(Row::Header("options.video.display.header"));
// Vanilla's VideoSettingsScreen as vanilla lays it out. No menu opens it (Video
// Settings... opens the graphics screen, which has all of it); it stays for
// `horizon-ctl options video-vanilla` and the tests that pin vanilla's screen.
let mut rows = vec![Row::Header("options.video.display.header")];
rows.push(Row::Big(Opt::ExclusiveMode));
rows.extend(pairs(&[
Opt::MaxFps,
@@ -3004,7 +3022,9 @@ impl OptionsUi {
let entries = [
("options.skinCustomisation", Some(Page::Skin)),
("options.sounds", Some(Page::Sound)),
("options.video", Some(Page::Video)),
// Video Settings... opens Horizon's graphics screen, which holds every
// video option, as Sodium's replaces vanilla's.
("options.video", Some(Page::Graphics)),
("options.controls", Some(Page::Controls)),
(
"options.language",
@@ -3604,6 +3624,9 @@ impl OptionsUi {
crate::screen_draw::paint(batch, atlas, &draws);
return;
}
if matches!(self.page(), Page::Extras | Page::Pvp) {
self.wide_extras = wide_extras(s, self.page(), |t| atlas.font.width(t));
}
let (widgets, top, bottom, content) = self.layout(s, w, h);
self.size = (w, h);
let in_world = self.in_world;
@@ -5941,8 +5964,6 @@ mod tests {
let (texts, missed) = page_texts("de_de", &[Page::Horizon, Page::Extras, Page::PadBinds]);
for want in [
"Splitscreen …",
"Partikel",
"Blockpartikel weg: Aus",
"Weicher Chat: Aus",
"Nachrichtenmarkierung: An",
"Kanäle für UI-Töne: Aus",
@@ -5961,7 +5982,7 @@ mod tests {
assert_eq!(missed, Vec::<String>::new());
let (texts, _) = page_texts("pl_pl", &[Page::Extras, Page::PadBinds]);
for want in [
"Ukryj cząstki bloków: Wył.",
"Płynny czat: Wył.",
"Pasek szybkiego wyboru: poprzednie pole",
] {
assert!(texts.iter().any(|t| t == want), "{want:?} not in {texts:?}");
@@ -6584,12 +6605,11 @@ mod tests {
Row::Pair(a, b) => *a == opt || *b == Some(opt),
_ => false,
}));
assert!(rows.iter().any(|row| matches!(row, Row::Buttons(..))));
}
ui.settings_search.set_value("no matching video setting");
let rows = ui.list_rows(&settings);
assert!(headers(ui.list_rows(&settings)).is_empty());
assert!(rows.iter().all(|row| matches!(row, Row::Buttons(..))));
assert!(rows.is_empty(), "{}", rows.len());
ui.settings_search.set_value("");
assert_eq!(headers(ui.list_rows(&settings)), all_headers);
}
@@ -7567,6 +7587,222 @@ mod tests {
assert_eq!(toggles(&mut ui, &s), [(0, false), (1, true)]);
}
/// An Extras or PvP & HUD label wider than half a row takes the whole row, so every label
/// fits its button: "Keep channels for UI sounds: Off" did not.
#[test]
fn extras_labels_fit_their_buttons() {
// Vanilla's font: 6 px a glyph, a few narrower ones.
let width = |t: &str| {
t.chars()
.map(|c| match c {
'i' | '\'' | ':' | '.' | ',' | '!' => 2.0,
'l' => 3.0,
' ' | 't' | 'I' => 4.0,
'f' | 'k' => 5.0,
_ => 6.0,
})
.sum::<f32>()
};
let s = Settings::default();
let mut ui = OptionsUi::default();
for page in [Page::Extras, Page::Pvp] {
ui.stack = vec![Page::Main, page];
ui.wide_extras = wide_extras(&s, page, width);
let rows = ui.list_rows(&s);
let buttons: Vec<&Widget> = rows
.iter()
.flat_map(|r| match r {
Row::Buttons(a, b) => std::iter::once(a).chain(b.as_ref()).collect(),
_ => Vec::new(),
})
.collect();
for b in &buttons {
let WKind::Button { label, .. } = &b.kind else {
continue;
};
let label = label.plain();
assert!(width(&label) <= b.w - 4.0, "{page:?}: {label} on {}", b.w);
}
// Short ones keep vanilla's two to a row.
assert!(buttons.iter().any(|b| b.w == 150.0), "{page:?}");
}
ui.stack = vec![Page::Main, Page::Extras];
ui.wide_extras = wide_extras(&s, Page::Extras, width);
for item in [
crate::extras::Item::UiSoundReserve,
crate::extras::Item::WheelCyclesSettings,
] {
assert!(ui.wide_extras.contains(&item), "{item:?}");
}
assert!(!ui.wide_extras.contains(&crate::extras::Item::AutoReconnect));
}
/// Every setting a player reaches, from the Options screen through every button that opens
/// a page, is in one place, and every page has one way in. Vanilla's own repeats stay as
/// vanilla has them; the graphics screen stands for Video Settings, so the vanilla video
/// options it shows count as Video Settings' (Menu Background Blur is in both of
/// vanilla's). Vanilla's Video Settings rows are all on the graphics screen.
#[test]
fn every_setting_and_page_is_reached_from_one_place() {
use std::collections::{BTreeMap, BTreeSet};
let mut s = Settings::default();
// The Music page shows once its extra is on.
s.extras.music_page = true;
let mut ui = OptionsUi::default();
let mut places: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut ways_in: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut put = |id: String, page: &str| {
places.entry(id).or_default().insert(page.to_owned());
};
// The graphics screen, drawn by `graphics_ui`.
let mut graphics_rows = BTreeSet::new();
for (page, groups) in crate::graphics_ui::rows() {
for row in groups.into_iter().flat_map(|(_, rows)| rows) {
use crate::graphics_ui::{H, Item};
let id = match row.item {
// Vanilla's Graphics API button sets the same setting.
Item::Horizon(H::GraphicsApi) => "graphics-api".to_owned(),
Item::Horizon(H::ShaderPacks) => {
ways_in
.entry(format!("{:?}", Page::ShaderPacks))
.or_default()
.insert(format!("Graphics/{page:?}"));
continue;
}
Item::Vanilla(o) => format!("{o:?}"),
Item::Horizon(h) => format!("graphics:{h:?}"),
Item::Extra(e) => format!("extra:{e:?}"),
};
graphics_rows.insert(id.clone());
put(id, "Graphics");
}
}
// Every page from the Options screen, through its buttons.
let mut seen = BTreeSet::new();
let mut todo = vec![Page::Main];
while let Some(page) = todo.pop() {
if !seen.insert(format!("{page:?}")) {
continue;
}
ui.stack = vec![page];
let (widgets, _, _, _) = ui.layout(&s, 427.0, 4000.0);
let name = format!("{page:?}");
for w in &widgets {
let id = match &w.kind {
WKind::Slider { opt, .. } => format!("{opt:?}"),
WKind::Button { act, .. } => match act {
Act::Cycle(Opt::GraphicsBackend) => "graphics-api".to_owned(),
Act::Cycle(opt) => format!("{opt:?}"),
Act::Extra(e) => format!("extra:{e:?}"),
Act::PvpPreset => "pvp-preset".to_owned(),
Act::Push(to) => {
ways_in
.entry(format!("{to:?}"))
.or_default()
.insert(name.clone());
// The graphics screen's pages are one screen, listed above.
if OptionsUi::graphics_page(*to).is_none() {
todo.push(*to);
}
continue;
}
_ => continue,
},
_ => continue,
};
put(id, &name);
}
}
// Vanilla's own: the same option on two of its screens.
let vanilla_repeats: BTreeMap<&str, &[&str]> = [
("MenuBlur", &["Accessibility", "Graphics"][..]),
("ShowSubtitles", &["Accessibility", "Sound"][..]),
("Narrator", &["Accessibility", "Chat"][..]),
("ChatOpacity", &["Accessibility", "Chat"][..]),
("TextBackgroundOpacity", &["Accessibility", "Chat"][..]),
("ChatLineSpacing", &["Accessibility", "Chat"][..]),
]
.into();
for (id, pages) in &places {
let pages: Vec<&str> = pages.iter().map(String::as_str).collect();
match vanilla_repeats.get(id.as_str()) {
Some(expected) => assert_eq!(&pages, expected, "{id}"),
None => assert_eq!(pages.len(), 1, "{id} is on {pages:?}"),
}
}
// AccessibilityOptionsScreen links Controls, as vanilla's does.
for (page, from) in &ways_in {
let from: Vec<&str> = from.iter().map(String::as_str).collect();
match page.as_str() {
"Controls" => assert_eq!(from, ["Accessibility", "Main"]),
_ => assert_eq!(from.len(), 1, "{page} opens from {from:?}"),
}
}
assert_eq!(
ways_in.get("ShaderPacks").map(|f| f.len()),
Some(1),
"{ways_in:?}"
);
assert!(
!seen.contains("Video"),
"vanilla's own layout has no way in"
);
// Every row of vanilla's Video Settings is on the graphics screen.
ui.stack = vec![Page::Video];
for row in ui.list_rows(&s) {
let opts = match row {
Row::Big(o) => vec![o],
Row::Pair(a, b) => std::iter::once(a).chain(b).collect(),
_ => Vec::new(),
};
for o in opts {
let id = match o {
Opt::GraphicsBackend => "graphics-api".to_owned(),
o => format!("{o:?}"),
};
// macOS has one graphics API: the graphics screen leaves the row out.
if id == "graphics-api" && cfg!(target_os = "macos") {
continue;
}
assert!(
graphics_rows.contains(&id),
"{id} is not on the graphics screen"
);
}
}
}
/// Shader packs, both kinds, are on the graphics screen's Shaders page: its Shader
/// Packs... opens the pack list, listed afresh, and Done comes back to that page.
#[test]
fn the_graphics_shaders_page_opens_the_shader_pack_list() {
use crate::graphics_ui::{H, Item, Target};
use winit::keyboard::KeyCode;
let atlas = atlas();
let mut s = Settings::default();
let mut ui = OptionsUi {
size: (427.0, 240.0),
..Default::default()
};
ui.show(Page::Graphics);
ui.graphics.open(crate::graphics_ui::Page::Shaders);
let packs = Target::Item(Item::Horizon(H::ShaderPacks));
assert!(
(0..40).any(|_| {
ui.key(&mut s, &atlas, KeyCode::Tab, None);
ui.graphics.focus == Some(packs)
}),
"{:?}",
ui.graphics.focus
);
let events = ui.key(&mut s, &atlas, KeyCode::Enter, None);
assert_eq!(ui.page(), Page::ShaderPacks);
assert!(events.contains(&Event::ListShaderPacks), "{events:?}");
ui.back(&mut s);
assert_eq!(ui.page(), Page::Graphics);
assert_eq!(ui.graphics.page(), crate::graphics_ui::Page::Shaders);
}
#[test]
fn horizon_settings_hang_off_options_and_reach_every_horizon_page() {
let s = Settings::default();
@@ -7592,18 +7828,15 @@ mod tests {
&main[main.len() - 3..],
[Page::Horizon, Page::Mods, Page::Credits]
);
// Video Settings... is the graphics screen, as Sodium's.
assert!(main.contains(&Page::Graphics) && !main.contains(&Page::Video));
ui.push(Page::Horizon);
assert_eq!(
pushes(&mut ui),
[
Page::Graphics,
Page::GraphicsAdvanced,
Page::ShaderPacks,
Page::Controller,
Page::Extras,
Page::Mods,
Page::SplitSetup,
Page::Pvp,
Page::SplitSetup,
Page::Studio,
Page::Agent,
]
@@ -7632,12 +7865,6 @@ mod tests {
.collect();
assert!(acts.contains(&&Act::Extra(crate::extras::Item::AgentControl)));
assert!(acts.contains(&&Act::CopyAgentDetails));
// Vanilla's own pages keep the Horizon pages they had.
ui.stack = vec![Page::Main, Page::Video];
let video = pushes(&mut ui);
for page in [Page::Graphics, Page::GraphicsAdvanced] {
assert!(video.contains(&page), "{page:?}");
}
ui.stack = vec![Page::Main, Page::Controls];
assert!(pushes(&mut ui).contains(&Page::Controller));
}
+13 -9
View File
@@ -17,6 +17,7 @@ use horizon_core::{
geometry::{Aabb, Axis, Vec3, trig},
random::{LegacyRandom, RandomSource},
};
use horizon_protocol::entity_data::PositionSource;
use horizon_world::block_collision::BlockCollisionRegistry;
use std::{
array,
@@ -50,9 +51,10 @@ pub struct ParticleOptions {
/// `GeyserParticleOptions.waterBlocks` and `GeyserBaseParticleOptions`'
/// `waterBlocks` and `burstImpulseBase` (0 where the type has none).
pub geyser: Option<(i32, f32)>,
/// `VibrationParticleOption`: the listener the signal flies to and its
/// arrival time in ticks (`ParticleEngine::vibration` is the same particle).
pub vibration: Option<([f64; 3], i32)>,
/// `VibrationParticleOption`: the listener's `PositionSource`, the point
/// the signal flies to and its arrival time in ticks
/// (`ParticleEngine::vibration` is the same particle).
pub vibration: Option<(PositionSource, [f64; 3], i32)>,
/// A native mod's own particle type, by its namespaced name (`mymod:spark`):
/// `SimpleParticleType` plus the provider `particles/<name>.json` names.
pub mod_particle: Option<String>,
@@ -94,10 +96,11 @@ impl ParticleOptions {
..Self::simple(ty)
}
}
/// `VibrationParticleOption(destination, arrivalInTicks)`.
pub fn vibration(dest: [f64; 3], ticks: i32) -> Self {
/// `VibrationParticleOption(destination, arrivalInTicks)`: the listener's
/// position source and the point this engine flies the signal to.
pub fn vibration(source: PositionSource, dest: [f64; 3], ticks: i32) -> Self {
Self {
vibration: Some((dest, ticks)),
vibration: Some((source, dest, ticks)),
..Self::simple(ParticleType::Vibration)
}
}
@@ -2988,8 +2991,8 @@ impl ParticleEngine {
// the listener the signal flies to and its arrival time; the sprite
// is the set's own draw from the engine's random.
T::Vibration => {
let (dest, ticks) = o.vibration?;
let mut q = Self::vibration_particle(p, dest, ticks);
let (_, dest, ticks) = o.vibration.as_ref()?;
let mut q = Self::vibration_particle(p, *dest, *ticks);
q.set = set;
q.sprite = self.sprite_from_set(ty);
q
@@ -5644,7 +5647,8 @@ mod tests {
#[test]
fn a_vibration_option_makes_the_signal_its_provider_draws() {
let mut engine = ParticleEngine::new(Path::new(""), &HashMap::new(), &HashSet::new());
let options = ParticleOptions::vibration([10.5, 70.5, -3.5], 20);
let options =
ParticleOptions::vibration(PositionSource::Block([10, 70, -4]), [10.5, 70.5, -3.5], 20);
// Within the camera's 32 blocks, so only the provider decides.
engine.add_with(&options, [0.5, 1.5, 0.5], [0.0; 3], false, false);
assert_eq!(engine.made().get("vibration"), Some(&1));
+191 -1
View File
@@ -575,8 +575,53 @@ pub struct Hud {
/// Hits in a row the server confirmed, until this player is hurt or 3 seconds pass.
pub combo: bool,
pub combo_corner: Corner,
/// Frames drawn in the last second.
pub fps: bool,
pub fps_corner: Corner,
/// Left and right clicks in the last second.
pub cps: bool,
pub cps_corner: Corner,
/// The movement keys, mouse buttons and jump, lit while held, with clicks per second.
pub keys: bool,
pub keys_corner: Corner,
/// The computer's local time.
pub clock: bool,
pub clock_corner: Corner,
/// How far the last hit landed from the eyes, for 3 seconds; it only measures.
pub reach: bool,
pub reach_corner: Corner,
/// Horizontal speed in blocks per second.
pub speed: bool,
pub speed_corner: Corner,
/// A dark box behind each widget.
pub background: bool,
/// The boxes' opacity, in percent.
pub opacity: u8,
/// The widgets' text colour (durability keeps its own).
pub text: Colour,
/// A shadow under the widgets' text, as vanilla's HUD text has.
pub shadow: bool,
}
impl Hud {
/// Whether any widget is on: with none, the HUD draws nothing and reads nothing.
pub fn any(&self) -> bool {
[
self.armour,
self.effects,
self.held,
self.ping,
self.coords,
self.server,
self.combo,
self.fps,
self.cps,
self.keys,
self.clock,
self.reach,
self.speed,
]
.contains(&true)
}
}
impl Default for Hud {
fn default() -> Self {
@@ -596,11 +641,35 @@ impl Default for Hud {
server_corner: Corner::TopLeft,
combo: false,
combo_corner: Corner::TopLeft,
fps: false,
fps_corner: Corner::TopLeft,
cps: false,
cps_corner: Corner::TopLeft,
keys: false,
keys_corner: Corner::BottomLeft,
clock: false,
clock_corner: Corner::TopRight,
reach: false,
reach_corner: Corner::TopLeft,
speed: false,
speed_corner: Corner::TopLeft,
background: true,
opacity: HUD_OPACITY,
text: Colour::White,
shadow: true,
}
}
}
pub use crate::extras::Corner;
/// The widgets' boxes at first: the extras' 0x78 alpha, 47%.
pub const HUD_OPACITY: u8 = 47;
pub const HUD_OPACITIES: (u8, u8) = (0, 100);
impl Hud {
/// The boxes' colour, ARGB.
pub fn background_argb(&self) -> u32 {
alpha(self.opacity) << 24
}
}
/// The value of the view options while they follow the world.
fn world_label() -> String {
crate::lang::get_or("horizon.pvp.view.world", "World's")
@@ -690,6 +759,8 @@ pub struct Pvp {
pub hud: Hud,
pub tweaks: Tweaks,
pub chat: ChatOptions,
/// Where each HUD widget sits and its size.
pub layout: crate::pvp_hud::Layout,
}
fn flag(value: &str, d: bool) -> bool {
match value {
@@ -743,6 +814,14 @@ impl Pvp {
"hudHeld" => u.held = flag(value, u.held),
"hudHeldCorner" => u.held_corner = Corner::parse(value, u.held_corner),
"hudBackground" => u.background = flag(value, u.background),
"hudOpacity" => u.opacity = num(value, HUD_OPACITIES, u.opacity),
"hudTextColour" => {
u.text = Colour::parse(value)
.filter(|c| *c != Colour::Inverted)
.unwrap_or(u.text)
}
"hudShadow" => u.shadow = flag(value, u.shadow),
"hudPlace" => self.layout.parse(value),
"hudPing" => u.ping = flag(value, u.ping),
"hudPingCorner" => u.ping_corner = Corner::parse(value, u.ping_corner),
"hudCoords" => u.coords = flag(value, u.coords),
@@ -751,6 +830,18 @@ impl Pvp {
"hudServerCorner" => u.server_corner = Corner::parse(value, u.server_corner),
"hudCombo" => u.combo = flag(value, u.combo),
"hudComboCorner" => u.combo_corner = Corner::parse(value, u.combo_corner),
"hudFps" => u.fps = flag(value, u.fps),
"hudFpsCorner" => u.fps_corner = Corner::parse(value, u.fps_corner),
"hudCps" => u.cps = flag(value, u.cps),
"hudCpsCorner" => u.cps_corner = Corner::parse(value, u.cps_corner),
"hudKeys" => u.keys = flag(value, u.keys),
"hudKeysCorner" => u.keys_corner = Corner::parse(value, u.keys_corner),
"hudClock" => u.clock = flag(value, u.clock),
"hudClockCorner" => u.clock_corner = Corner::parse(value, u.clock_corner),
"hudReach" => u.reach = flag(value, u.reach),
"hudReachCorner" => u.reach_corner = Corner::parse(value, u.reach_corner),
"hudSpeed" => u.speed = flag(value, u.speed),
"hudSpeedCorner" => u.speed_corner = Corner::parse(value, u.speed_corner),
"numericEnchants" => t.numeric_enchants = flag(value, t.numeric_enchants),
"lowFire" => t.fire_lower = num(value, FIRE_LOWER, t.fire_lower),
"shieldSize" => t.shield_size = num(value, SHIELD_SIZES, t.shield_size),
@@ -838,6 +929,34 @@ impl Pvp {
u.combo,
u.combo_corner.name(),
);
let _ = write!(
out,
"hudFps:{}\nhudFpsCorner:{}\nhudCps:{}\nhudCpsCorner:{}\nhudKeys:{}\nhudKeysCorner:{}\n",
u.fps,
u.fps_corner.name(),
u.cps,
u.cps_corner.name(),
u.keys,
u.keys_corner.name(),
);
let _ = write!(
out,
"hudClock:{}\nhudClockCorner:{}\nhudReach:{}\nhudReachCorner:{}\nhudSpeed:{}\nhudSpeedCorner:{}\n",
u.clock,
u.clock_corner.name(),
u.reach,
u.reach_corner.name(),
u.speed,
u.speed_corner.name(),
);
let _ = write!(
out,
"hudOpacity:{}\nhudTextColour:{}\nhudShadow:{}\n",
u.opacity,
u.text.name(),
u.shadow,
);
self.layout.serialize(out);
let t = &self.tweaks;
let _ = write!(
out,
@@ -955,7 +1074,25 @@ opts! {
HudCombo, "hud_combo", "Combo counter",
"Hits in a row the server confirmed, until you are hurt or 3 seconds pass without one.";
HudComboCorner, "hud_combo_corner", "Combo corner", "Where the combo shows.";
HudFps, "hud_fps", "FPS", "Frames drawn in the last second.";
HudFpsCorner, "hud_fps_corner", "FPS corner", "Where the FPS shows.";
HudCps, "hud_cps", "Clicks per second", "Your left and right clicks in the last second.";
HudCpsCorner, "hud_cps_corner", "CPS corner", "Where the clicks per second show.";
HudKeys, "hud_keys", "Keystrokes",
"Your movement keys, mouse buttons and jump, lit while you hold them, with clicks per second.";
HudKeysCorner, "hud_keys_corner", "Keystrokes corner", "Where the keystrokes show.";
HudClock, "hud_clock", "Clock", "Your computer's time.";
HudClockCorner, "hud_clock_corner", "Clock corner", "Where the clock shows.";
HudReach, "hud_reach", "Reach",
"How far from your eyes your last hit landed, for 3 seconds. It only measures: your reach stays vanilla's.";
HudReachCorner, "hud_reach_corner", "Reach corner", "Where the reach shows.";
HudSpeed, "hud_speed", "Speed", "How fast you move across the ground, in blocks per second.";
HudSpeedCorner, "hud_speed_corner", "Speed corner", "Where the speed shows.";
HudBackground, "hud_background", "Widget backgrounds", "A dark box behind each widget.";
HudOpacity, "hud_opacity", "Background opacity", "How dark the boxes behind the widgets are.";
HudTextColour, "hud_text_colour", "Text colour",
"The widgets' text colour. Durability keeps its green to red.";
HudShadow, "hud_shadow", "Text shadow", "A shadow under the widgets' text, as vanilla's HUD text has.";
NumericEnchants, "numeric_enchants", "Enchantment levels as numbers",
"Tooltips and the enchanting table show Sharpness 5 instead of Sharpness V.";
FireLower, "fire_lower", "Lower fire",
@@ -1030,7 +1167,22 @@ pub fn rows() -> Vec<Result<Opt, &'static str>> {
HudServerCorner,
HudCombo,
HudComboCorner,
HudFps,
HudFpsCorner,
HudCps,
HudCpsCorner,
HudKeys,
HudKeysCorner,
HudClock,
HudClockCorner,
HudReach,
HudReachCorner,
HudSpeed,
HudSpeedCorner,
HudBackground,
HudOpacity,
HudTextColour,
HudShadow,
]
.map(Ok),
);
@@ -1054,7 +1206,15 @@ impl Opt {
OutlineColour | OutlineOpacity | OutlineThickness | OutlineFill => Some(Outline),
HitColour | HitOpacity => Some(Hit),
HudArmour | HudEffects | HudHeld | HudPing | HudCoords | HudServer | HudCombo
| HudBackground | NumericEnchants => None,
| HudFps | HudCps | HudKeys | HudClock | HudReach | HudSpeed | HudBackground
| HudTextColour | HudShadow | NumericEnchants => None,
HudOpacity => Some(HudBackground),
HudFpsCorner => Some(HudFps),
HudCpsCorner => Some(HudCps),
HudKeysCorner => Some(HudKeys),
HudClockCorner => Some(HudClock),
HudReachCorner => Some(HudReach),
HudSpeedCorner => Some(HudSpeed),
FireLower | ShieldSize | TotemSize | GlintColour | ClearWater | ClearLava => None,
ViewTime | ViewWeather => None,
ChatTimestamps | ChatCompact | ChatCopy | ChatSearch => None,
@@ -1081,10 +1241,17 @@ impl Opt {
Opt::HudEffects => p.hud.effects,
Opt::HudHeld => p.hud.held,
Opt::HudBackground => p.hud.background,
Opt::HudShadow => p.hud.shadow,
Opt::HudPing => p.hud.ping,
Opt::HudCoords => p.hud.coords,
Opt::HudServer => p.hud.server,
Opt::HudCombo => p.hud.combo,
Opt::HudFps => p.hud.fps,
Opt::HudCps => p.hud.cps,
Opt::HudKeys => p.hud.keys,
Opt::HudClock => p.hud.clock,
Opt::HudReach => p.hud.reach,
Opt::HudSpeed => p.hud.speed,
Opt::NumericEnchants => p.tweaks.numeric_enchants,
Opt::ClearWater => p.tweaks.clear_water,
Opt::ClearLava => p.tweaks.clear_lava,
@@ -1124,6 +1291,14 @@ impl Opt {
Opt::HudCoordsCorner => corner_label(p.hud.coords_corner),
Opt::HudServerCorner => corner_label(p.hud.server_corner),
Opt::HudComboCorner => corner_label(p.hud.combo_corner),
Opt::HudFpsCorner => corner_label(p.hud.fps_corner),
Opt::HudCpsCorner => corner_label(p.hud.cps_corner),
Opt::HudKeysCorner => corner_label(p.hud.keys_corner),
Opt::HudClockCorner => corner_label(p.hud.clock_corner),
Opt::HudReachCorner => corner_label(p.hud.reach_corner),
Opt::HudSpeedCorner => corner_label(p.hud.speed_corner),
Opt::HudOpacity => format!("{}%", p.hud.opacity),
Opt::HudTextColour => p.hud.text.label(),
Opt::FireLower => match p.tweaks.fire_lower {
0 => crate::lang::get_or("Off", "Off"),
v => format!("{v}%"),
@@ -1191,6 +1366,9 @@ impl Opt {
Opt::HudHeld => u.held = !u.held,
Opt::HudHeldCorner => u.held_corner = corner(u.held_corner),
Opt::HudBackground => u.background = !u.background,
Opt::HudOpacity => u.opacity = step_by(u.opacity, HUD_OPACITIES, 10, back),
Opt::HudTextColour => u.text = u.text.step_solid(back),
Opt::HudShadow => u.shadow = !u.shadow,
Opt::HudPing => u.ping = !u.ping,
Opt::HudPingCorner => u.ping_corner = corner(u.ping_corner),
Opt::HudCoords => u.coords = !u.coords,
@@ -1199,6 +1377,18 @@ impl Opt {
Opt::HudServerCorner => u.server_corner = corner(u.server_corner),
Opt::HudCombo => u.combo = !u.combo,
Opt::HudComboCorner => u.combo_corner = corner(u.combo_corner),
Opt::HudFps => u.fps = !u.fps,
Opt::HudFpsCorner => u.fps_corner = corner(u.fps_corner),
Opt::HudCps => u.cps = !u.cps,
Opt::HudCpsCorner => u.cps_corner = corner(u.cps_corner),
Opt::HudKeys => u.keys = !u.keys,
Opt::HudKeysCorner => u.keys_corner = corner(u.keys_corner),
Opt::HudClock => u.clock = !u.clock,
Opt::HudClockCorner => u.clock_corner = corner(u.clock_corner),
Opt::HudReach => u.reach = !u.reach,
Opt::HudReachCorner => u.reach_corner = corner(u.reach_corner),
Opt::HudSpeed => u.speed = !u.speed,
Opt::HudSpeedCorner => u.speed_corner = corner(u.speed_corner),
Opt::NumericEnchants => p.tweaks.numeric_enchants = !p.tweaks.numeric_enchants,
Opt::FireLower => t.fire_lower = step_by(t.fire_lower, FIRE_LOWER, 10, back),
Opt::ShieldSize => t.shield_size = step_by(t.shield_size, SHIELD_SIZES, 10, back),
+451
View File
@@ -214,6 +214,320 @@ impl Combo {
}
}
/// Clicks per second: the presses of the last second, kept for the widgets that are on.
#[derive(Default)]
pub struct Clicks {
/// Press times in seconds, oldest first; at most [`Clicks::KEEP`].
times: std::collections::VecDeque<f64>,
}
impl Clicks {
/// More than any hand clicks in a second: the ring never grows past it.
const KEEP: usize = 64;
pub fn press(&mut self, now: f64) {
if self.times.len() == Self::KEEP {
self.times.pop_front();
}
self.times.push_back(now);
}
/// The presses in the second up to `now`.
pub fn per_second(&mut self, now: f64) -> u32 {
while self.times.front().is_some_and(|&t| now - t >= 1.0) {
self.times.pop_front();
}
self.times.len() as u32
}
}
/// Horizontal speed in blocks per second, from the player's own position sampled at most
/// every [`Speed::SAMPLE`] seconds (nothing is asked of the server).
#[derive(Default)]
pub struct Speed {
last: Option<([f64; 2], f64)>,
/// Tenths of a block per second.
tenths: u32,
}
impl Speed {
pub const SAMPLE: f64 = 0.25;
pub fn sample(&mut self, x: f64, z: f64, now: f64) -> u32 {
match self.last {
Some((_, at)) if now - at < Self::SAMPLE => {}
Some(([lx, lz], at)) => {
let d = ((x - lx).powi(2) + (z - lz).powi(2)).sqrt();
// A teleport or respawn isn't speed.
let v = d / (now - at);
self.tenths = if v > 200.0 {
0
} else {
(v * 10.0).round() as u32
};
self.last = Some(([x, z], now));
}
None => self.last = Some(([x, z], now)),
}
self.tenths
}
}
/// The reach display: how far the last hit this player made was, measured from the eyes to
/// the point the crosshair's ray met the target's box. It measures what the client already
/// computed for the attack and changes nothing about it.
#[derive(Default)]
pub struct Reach {
last: Option<(f64, f64)>,
}
impl Reach {
/// How long the last hit's distance stays shown.
pub const SHOWN: f64 = 3.0;
pub fn hit(&mut self, distance: f64, now: f64) {
self.last = Some((distance, now));
}
/// Hundredths of a block, while the last hit is recent.
pub fn hundredths(&self, now: f64) -> Option<u32> {
let (d, at) = self.last?;
(now - at <= Self::SHOWN).then(|| (d * 100.0).round() as u32)
}
}
/// `n` hundredths as "3.05".
pub fn hundredths(n: u32) -> String {
format!("{}.{:02}", n / 100, n % 100)
}
/// `n` tenths as "4.3".
pub fn tenths(n: u32) -> String {
format!("{}.{}", n / 10, n % 10)
}
/// The local time as "14:05".
pub fn clock(hour: u8, minute: u8) -> String {
format!("{hour:02}:{minute:02}")
}
/// The keystrokes widget's keys, in drawing order.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Key {
Forward,
Left,
Back,
Right,
Attack,
Use,
Jump,
}
impl Key {
pub const ALL: [Key; 7] = [
Key::Forward,
Key::Left,
Key::Back,
Key::Right,
Key::Attack,
Key::Use,
Key::Jump,
];
/// The key mapping it shows.
pub fn mapping(self) -> &'static str {
match self {
Key::Forward => "key.forward",
Key::Left => "key.left",
Key::Back => "key.back",
Key::Right => "key.right",
Key::Attack => "key.attack",
Key::Use => "key.use",
Key::Jump => "key.jump",
}
}
}
/// The keystrokes' key size and the gap between keys, in GUI pixels.
pub const KEY: f32 = 22.0;
pub const KEY_GAP: f32 = 2.0;
/// The space bar's height.
pub const SPACE: f32 = 12.0;
/// Where each key of the keystrokes widget sits in its box: W above A S D, the two mouse
/// buttons under them, the space bar at the bottom, as `(x, y, w, h)`.
pub fn key_rect(key: Key) -> (f32, f32, f32, f32) {
let step = KEY + KEY_GAP;
let full = 3.0 * KEY + 2.0 * KEY_GAP;
let half = (full - KEY_GAP) / 2.0;
match key {
Key::Forward => (step, 0.0, KEY, KEY),
Key::Left => (0.0, step, KEY, KEY),
Key::Back => (step, step, KEY, KEY),
Key::Right => (2.0 * step, step, KEY, KEY),
Key::Attack => (0.0, 2.0 * step, half, KEY),
Key::Use => (half + KEY_GAP, 2.0 * step, half, KEY),
Key::Jump => (0.0, 3.0 * step, full, SPACE),
}
}
/// The keystrokes widget's size.
pub fn keys_size() -> (f32, f32) {
let (_, y, w, h) = key_rect(Key::Jump);
(w, y + h)
}
/// The HUD widgets, for their places on screen ([`Layout`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WidgetId {
Armour,
Effects,
Held,
Ping,
Coords,
Server,
Combo,
Fps,
Cps,
Keys,
Clock,
Reach,
Speed,
}
impl WidgetId {
pub const ALL: [WidgetId; 13] = [
WidgetId::Armour,
WidgetId::Effects,
WidgetId::Held,
WidgetId::Ping,
WidgetId::Coords,
WidgetId::Server,
WidgetId::Combo,
WidgetId::Fps,
WidgetId::Cps,
WidgetId::Keys,
WidgetId::Clock,
WidgetId::Reach,
WidgetId::Speed,
];
pub fn name(self) -> &'static str {
match self {
WidgetId::Armour => "armour",
WidgetId::Effects => "effects",
WidgetId::Held => "held",
WidgetId::Ping => "ping",
WidgetId::Coords => "coords",
WidgetId::Server => "server",
WidgetId::Combo => "combo",
WidgetId::Fps => "fps",
WidgetId::Cps => "cps",
WidgetId::Keys => "keys",
WidgetId::Clock => "clock",
WidgetId::Reach => "reach",
WidgetId::Speed => "speed",
}
}
pub fn index(self) -> usize {
self as usize
}
}
/// Where a widget sits: in its corner, stacked with the others there (`at` is `None`), or at a
/// place the player dragged it to, as fractions of the room the screen leaves around the box
/// (0 at the left or top edge, 1 at the right or bottom), so it stays on screen at any size.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Place {
pub at: Option<(f32, f32)>,
/// Its size in percent.
pub scale: u8,
}
impl Default for Place {
fn default() -> Self {
Self {
at: None,
scale: 100,
}
}
}
pub const SCALES: (u8, u8) = (50, 200);
/// Every widget's place.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Layout(pub [Place; 13]);
impl Layout {
pub fn get(&self, id: WidgetId) -> Place {
self.0[id.index()]
}
pub fn get_mut(&mut self, id: WidgetId) -> &mut Place {
&mut self.0[id.index()]
}
/// One `hudPlace` value: `armour,0.25,0.5,150`, or `armour,corner,,150`.
pub fn parse(&mut self, value: &str) {
let mut parts = value.split(',');
let Some(id) = parts
.next()
.and_then(|n| WidgetId::ALL.into_iter().find(|w| w.name() == n))
else {
return;
};
let (x, y, scale) = (parts.next(), parts.next(), parts.next());
let frac = |v: Option<&str>| {
v.and_then(|v| v.parse::<f32>().ok())
.filter(|v| v.is_finite())
.map(|v| v.clamp(0.0, 1.0))
};
let place = self.get_mut(id);
place.at = frac(x).zip(frac(y));
if let Some(s) = scale.and_then(|s| s.parse::<u8>().ok()) {
place.scale = s.clamp(SCALES.0, SCALES.1);
}
}
/// The `hudPlace` lines of the widgets that left their corner or their size.
pub fn serialize(&self, out: &mut String) {
use std::fmt::Write;
for id in WidgetId::ALL {
let p = self.get(id);
if p == Place::default() {
continue;
}
let _ = match p.at {
Some((x, y)) => writeln!(out, "hudPlace:{},{x:.4},{y:.4},{}", id.name(), p.scale),
None => writeln!(out, "hudPlace:{},corner,,{}", id.name(), p.scale),
};
}
}
}
/// A box `w` x `h` placed at fractions `(fx, fy)` of the room left on a `screen`.
pub fn from_fractions((fx, fy): (f32, f32), (w, h): (f32, f32), screen: (f32, f32)) -> (f32, f32) {
(
(fx * (screen.0 - w).max(0.0)).round(),
(fy * (screen.1 - h).max(0.0)).round(),
)
}
/// The fractions that put a `w` x `h` box at `(x, y)` on a `screen`, kept on screen.
pub fn to_fractions((x, y): (f32, f32), (w, h): (f32, f32), screen: (f32, f32)) -> (f32, f32) {
let frac = |v: f32, room: f32| {
if room > 0.0 {
(v / room).clamp(0.0, 1.0)
} else {
0.0
}
};
(frac(x, screen.0 - w), frac(y, screen.1 - h))
}
/// How near, in GUI pixels, an edge must come to another to snap to it.
pub const SNAP: f32 = 4.0;
/// Snaps a box's start `x` of length `w` along one axis of length `screen`: to the screen's
/// edges and middle, and to the edges of the `others` boxes (`(start, length)`), where one
/// is within [`SNAP`]. Returns the new start and the line it snapped to, if any.
pub fn snap(x: f32, w: f32, screen: f32, others: &[(f32, f32)]) -> (f32, Option<f32>) {
let mut best: Option<(f32, f32, f32)> = None;
let mut consider = |start: f32, line: f32| {
let d = (start - x).abs();
if d <= SNAP && best.is_none_or(|(b, _, _)| d < b) {
best = Some((d, start, line));
}
};
// The screen's edges, with the corners' margin, and its middle.
consider(2.0, 0.0);
consider(screen - 2.0 - w, screen);
consider((screen - w) / 2.0, screen / 2.0);
for &(o, ow) in others {
// Side by side, or lined up on the same edge.
consider(o + ow, o + ow);
consider(o - w, o);
consider(o, o);
consider(o + ow - w, o + ow);
}
match best {
Some((_, start, line)) => (start.clamp(0.0, (screen - w).max(0.0)), Some(line)),
None => (x.clamp(0.0, (screen - w).max(0.0)), None),
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -338,4 +652,141 @@ mod tests {
assert_eq!(h, 18.0 + 10.0 + 20.0 + 2.0 * PAD - 2.0);
assert!(size(&[]) == (0.0, 0.0));
}
#[test]
fn clicks_count_the_presses_of_the_last_second() {
let mut c = Clicks::default();
for t in [0.0, 0.1, 0.5, 0.9] {
c.press(t);
}
assert_eq!(c.per_second(0.95), 4);
// 1.0 s after the first press it has left the window.
assert_eq!(c.per_second(1.0), 3);
assert_eq!(c.per_second(1.6), 1);
assert_eq!(c.per_second(5.0), 0);
// The ring never keeps more than 64 presses.
for i in 0..200 {
c.press(10.0 + f64::from(i) * 0.001);
}
assert_eq!(c.per_second(10.2), 64);
}
#[test]
fn speed_is_horizontal_blocks_per_second_between_samples() {
let mut s = Speed::default();
assert_eq!(s.sample(0.0, 0.0, 0.0), 0);
// Sprinting: 5.612 blocks per second along x, with a jump (y is ignored).
assert_eq!(s.sample(0.5, 0.0, 0.1), 0, "too soon for a new sample");
assert_eq!(s.sample(1.403, 0.0, 0.25), 56);
// 3 east and 4 south in a second: 5 blocks per second.
assert_eq!(s.sample(1.403 + 3.0, 4.0, 1.25), 50);
// A teleport reads as standing still.
assert_eq!(s.sample(1000.0, 4.0, 1.5), 0);
assert_eq!(tenths(56), "5.6");
}
#[test]
fn reach_shows_the_last_hit_for_three_seconds() {
let mut r = Reach::default();
assert_eq!(r.hundredths(0.0), None);
r.hit(2.954, 1.0);
assert_eq!(r.hundredths(1.5), Some(295));
assert_eq!(hundredths(295), "2.95");
assert_eq!(hundredths(300), "3.00");
assert_eq!(r.hundredths(4.1), None);
}
#[test]
fn the_clock_reads_hours_and_minutes() {
assert_eq!(clock(14, 5), "14:05");
assert_eq!(clock(0, 0), "00:00");
}
#[test]
fn keystrokes_keys_tile_their_box_without_overlap() {
let (w, h) = keys_size();
assert_eq!((w, h), (70.0, 3.0 * 24.0 + 12.0));
for (i, a) in Key::ALL.iter().enumerate() {
let (x, y, kw, kh) = key_rect(*a);
assert!(
x >= 0.0 && y >= 0.0 && x + kw <= w && y + kh <= h,
"{a:?} inside"
);
for b in &Key::ALL[i + 1..] {
let (bx, by, bw, bh) = key_rect(*b);
let apart = x + kw <= bx || bx + bw <= x || y + kh <= by || by + bh <= y;
assert!(apart, "{a:?} and {b:?} overlap");
}
}
}
#[test]
fn places_round_trip_and_only_moved_widgets_are_written() {
let mut l = Layout::default();
let mut text = String::new();
l.serialize(&mut text);
assert_eq!(text, "", "a vanilla layout writes nothing");
*l.get_mut(WidgetId::Keys) = Place {
at: Some((0.25, 1.0)),
scale: 150,
};
l.get_mut(WidgetId::Fps).scale = 75;
l.serialize(&mut text);
assert_eq!(
text,
"hudPlace:fps,corner,,75\nhudPlace:keys,0.2500,1.0000,150\n"
);
let mut back = Layout::default();
for line in text.lines() {
back.parse(line.split_once(':').unwrap().1);
}
assert_eq!(back, l);
// Out of range values are clamped, unknown widgets and bad numbers ignored.
back.parse("speed,-3,7,250");
assert_eq!(back.get(WidgetId::Speed).at, Some((0.0, 1.0)));
assert_eq!(back.get(WidgetId::Speed).scale, 200);
back.parse("radar,0.5,0.5,100");
back.parse("clock,NaN,0.5,100");
assert_eq!(back.get(WidgetId::Clock).at, None);
}
#[test]
fn fractions_keep_a_box_on_screen_at_any_size() {
let screen = (480.0, 270.0);
let at = to_fractions((100.0, 50.0), (80.0, 20.0), screen);
assert_eq!(from_fractions(at, (80.0, 20.0), screen), (100.0, 50.0));
// A box at the right edge stays at the right edge on a wider screen.
let right = to_fractions((400.0, 0.0), (80.0, 20.0), screen);
assert_eq!(right, (1.0, 0.0));
assert_eq!(
from_fractions(right, (80.0, 20.0), (960.0, 540.0)),
(880.0, 0.0)
);
// Dragged past the edge: kept inside.
assert_eq!(
to_fractions((-30.0, 900.0), (80.0, 20.0), screen),
(0.0, 1.0)
);
}
#[test]
fn boxes_snap_to_screen_edges_middle_and_each_other() {
// Near the left margin.
assert_eq!(snap(4.5, 50.0, 480.0, &[]), (2.0, Some(0.0)));
// Centred: the middle of 480 for a 50-wide box starts at 215.
assert_eq!(snap(213.0, 50.0, 480.0, &[]), (215.0, Some(240.0)));
// Beside another box (100..160): its start goes to 160.
assert_eq!(
snap(163.0, 50.0, 480.0, &[(100.0, 60.0)]),
(160.0, Some(160.0))
);
// Lined up with its left edge.
assert_eq!(
snap(98.0, 50.0, 480.0, &[(100.0, 60.0)]),
(100.0, Some(100.0))
);
// Too far from anything: stays, but never off screen.
assert_eq!(snap(300.0, 50.0, 480.0, &[(100.0, 60.0)]), (300.0, None));
assert_eq!(snap(470.0, 50.0, 480.0, &[]), (430.0, None));
}
}
+43 -1
View File
@@ -614,7 +614,12 @@ fn button_with_room(
room: f32,
narration: impl FnOnce([f32; 4]) -> crate::widgets::Narratable,
) {
crate::widgets::record(&label.plain(), [x, y, w, h], active);
let plain = label.plain();
crate::widgets::record(&plain, [x, y, w, h], active);
// An icon button has no label to record, and A presses it all the same.
if active && plain.trim().is_empty() {
crate::widgets::target([x, y, w, h]);
}
crate::widgets::narratable(|| narration([x, y, w, h]).hovered(hover));
crate::widgets::widget_cursor(hover, active);
let sprite = if !active {
@@ -1846,6 +1851,43 @@ mod tests {
assert_eq!(scrolled_left(10.0, 40.0, 1_000), 10.0 - offset);
}
/// A controller reaches an icon button only through its target, and can't press a disabled one.
#[test]
fn an_active_icon_button_is_a_focus_target_and_a_disabled_one_is_not() {
let atlas = atlas();
let mut batch = HudBatch::new([640, 480]);
// Whatever this thread drew before is the last frame's, then gone.
crate::widgets::end_frame(u64::MAX - 1);
button(
&mut batch,
&atlas,
10.0,
20.0,
20.0,
20.0,
&Text::empty(),
true,
false,
);
button(
&mut batch,
&atlas,
40.0,
20.0,
20.0,
20.0,
&Text::empty(),
false,
false,
);
crate::widgets::end_frame(u64::MAX);
let targets: Vec<[f32; 4]> = crate::widgets::last_targets()
.iter()
.map(|t| t.rect)
.collect();
assert_eq!(targets, [[10.0, 20.0, 20.0, 20.0]]);
}
#[test]
fn a_glyph_leaves_a_label_that_fits_centred() {
let atlas = atlas();
@@ -415,6 +415,7 @@
"Frame Generation": "Raam Generasie",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Toon 'n opgemaakte raam tussen elke twee regte: gladder, 'n bietjie meer vertraging.",
"Shadow Cache": "Skadu-kas",
"Shader Packs...": "Shader-pakke...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Hergebruik die pakskadukaart wanneer son, uitsig en wêreld ooreenstem. Af by verstek.",
"Find BlueMap": "Vind BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Op bedieners, vind hul BlueMap-webkaart vir ver terrein. Sluit volgende aan.",
@@ -432,6 +432,7 @@
"Frame Generation": "جيل الإطار",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "يُظهر إطارًا مكوّنًا بين كل إطارين حقيقيين: أكثر سلاسة وتأخرًا قليلًا.",
"Shadow Cache": "ذاكرة التخزين المؤقت الظل",
"Shader Packs...": "حزم التظليل...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "يعيد استخدام خريطة ظل الحزمة عندما تتطابق الشمس والعرض والعالم. إيقاف بشكل افتراضي.",
"Find BlueMap": "ابحث عن بلو ماب",
"On servers, finds their BlueMap web map for far terrain. Next join.": "على الخوادم، يتم العثور على خريطة الويب BlueMap الخاصة بهم للتضاريس البعيدة. الانضمام التالي.",
@@ -407,6 +407,7 @@
"Frame Generation": "Çərçivə Yaradılması",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Hər iki real arasında düzəldilmiş çərçivəni göstərir: daha hamar, bir az daha gecikmə.",
"Shadow Cache": "Kölgə önbelleği",
"Shader Packs...": "Shader Paketləri...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Günəş, görünüş və dünya uyğunlaşdıqda paket kölgə xəritəsindən təkrar istifadə edir. Defolt olaraq söndürülür.",
"Find BlueMap": "BlueMap tapın",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Serverlərdə uzaq ərazilər üçün BlueMap veb xəritəsini tapır. Növbəti qoşulun.",
@@ -427,6 +427,7 @@
"Frame Generation": "Кадр быуын",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Һәр ике ысын араһында ҡоролған кадр күрһәтә: шымараҡ, бер аҙ күберәк лаг.",
"Shadow Cache": "Күләгә кэш",
"Shader Packs...": "Шейдер пакеттары...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Ҡояш, ҡараш һәм донъя тап килгәндә пакет күләгә картаһын ҡабаттан ҡуллана. Һүндерелгән по умолчанию.",
"Find BlueMap": "Зәңгәр картаны табығыҙ",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Серверҙарҙа, алыҫ ерҙәр өсөн уларҙы BlueMap веб-картаһын таба. Киләһе ҡушылыу.",
@@ -417,6 +417,7 @@
"Frame Generation": "Генерацыя фрэймаў",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Паказвае прыдуманую рамку паміж двума сапраўднымі: больш гладкая, з крыху большым адставаннем.",
"Shadow Cache": "Ценявы кэш",
"Shader Packs...": "Пакеты шэйдэраў...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Паўторна выкарыстоўвае карту ценяў зграі, калі сонца, выгляд і свет супадаюць. Выключана па змаўчанні.",
"Find BlueMap": "Знайдзіце BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "На серверах знаходзіць вэб-карту BlueMap для далёкай мясцовасці. Далей далучыцца.",
@@ -474,5 +474,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Аури и пътеки, които другите играчи носят. Изключено по подразбиране.",
"horizon.extras.cosmetics.shown": "Показано",
"horizon.extras.cosmetics.hidden": "Скрити",
"horizon.extras.cosmetics.name_tags": "Само етикети с имена"
"horizon.extras.cosmetics.name_tags": "Само етикети с имена",
"Shader Packs...": "Шейдър пакети..."
}
@@ -419,6 +419,7 @@
"Frame Generation": "Krouiñ frammoù",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Diskouez a ra ur framm savet etre pep daou hini gwirion : eeunoc'h, un tamm muioc'h a zilez.",
"Shadow Cache": "Grubuilh skeud",
"Shader Packs...": "Pakadoù skeudennoù...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Adimplij ar gartenn skeud pakad pa glot an heol, ar gwel hag ar bed. Diweredekaet dre ziouer.",
"Find BlueMap": "Kavout BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "War ar servijerioù e kaver o gartenn web BlueMap evit an tachennoù pell. Kemer perzh da heul.",
@@ -419,5 +419,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Aure i staze koje drugi igrači nose. Podrazumevano isključeno.",
"horizon.extras.cosmetics.shown": "Prikazano",
"horizon.extras.cosmetics.hidden": "Skriveno",
"horizon.extras.cosmetics.name_tags": "Samo oznake sa imenom"
"horizon.extras.cosmetics.name_tags": "Samo oznake sa imenom",
"Shader Packs...": "Shader paketi..."
}
@@ -467,5 +467,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Aures i rastres que porten altres jugadors. Desactivat per defecte.",
"horizon.extras.cosmetics.shown": "Es mostra",
"horizon.extras.cosmetics.hidden": "Ocult",
"horizon.extras.cosmetics.name_tags": "Només etiquetes de nom"
"horizon.extras.cosmetics.name_tags": "Només etiquetes de nom",
"Shader Packs...": "Paquets d'ombres..."
}
@@ -398,6 +398,7 @@
"Frame Generation": "Generování snímků",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Zobrazuje vytvořený rám mezi každým dvěma skutečnými: hladší, trochu více zpoždění.",
"Shadow Cache": "Stínová mezipaměť",
"Shader Packs...": "Shader Pack...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Znovu použije mapu stínu balíčku, když se slunce, pohled a svět shodují. Ve výchozím nastavení vypnuto.",
"Find BlueMap": "Najděte BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Na serverech najde jejich webovou mapu BlueMap pro vzdálený terén. Další připojení.",
@@ -423,6 +423,7 @@
"Frame Generation": "Кадр генерацийӗ",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Кашни икӗ чӑн-чӑн хушшинче тунӑ кадра кӑтартать: тикӗсрех, кӑштах кая юлса.",
"Shadow Cache": "Мӗлке кэшӗ",
"Shader Packs...": "Шейдер пакетӗсем...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Хӗвел, курӑну тата тӗнче пӗр пек чухне мӗлке карттипе тепӗр хут усӑ курать. Выключен по умолчанию.",
"Find BlueMap": "BlueMap тупӑр",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Серверсем ҫинче инҫетри ҫӗр ҫинчи BlueMap веб-карттине тупать. Тепӗр хут хутшӑнӑр.",
@@ -407,6 +407,7 @@
"Frame Generation": "Cynhyrchu Ffrâm",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Yn dangos ffrâm wedi'i gwneud i fyny rhwng pob dau go iawn: llyfnach, ychydig yn fwy oedi.",
"Shadow Cache": "Cache Cysgod",
"Shader Packs...": "Pecynnau Shader...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Yn ailddefnyddio'r map cysgodion pecyn pan fydd yr haul, yr olygfa a'r byd yn cyd-fynd. Wedi'i ddiffodd yn ddiofyn.",
"Find BlueMap": "Dewch o hyd i BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Ar weinyddion, yn dod o hyd i'w map gwe BlueMap ar gyfer tir pell. Ymunwch nesaf.",
@@ -444,5 +444,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Auraer og spor, som andre spillere bærer. Fra som standard.",
"horizon.extras.cosmetics.shown": "Vist",
"horizon.extras.cosmetics.hidden": "Skjult",
"horizon.extras.cosmetics.name_tags": "Kun navneskilte"
"horizon.extras.cosmetics.name_tags": "Kun navneskilte",
"Shader Packs...": "Shader-pakker..."
}
@@ -301,6 +301,18 @@
"Frame Generation": "Bildgenerierung",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Zeigt zwischen zwei echten Bildern ein errechnetes: flüssiger, etwas mehr Verzögerung.",
"Shadow Cache": "Schatten-Cache",
"Shaders": "Shader",
"Shader packs, Horizon's and shaders/ folder ones, and their options.": "Shader-Pakete, die von Horizon und die aus shaders/-Ordnern, und ihre Optionen.",
"Shader packs": "Shader-Pakete",
"Vanilla's own preset: sets only vanilla's options, as Minecraft does.": "Das Preset von Vanilla: setzt nur die Optionen von Vanilla, wie Minecraft.",
"Fullscreen takes the display over, at the mode below.": "Der Vollbildmodus übernimmt den Bildschirm, im Modus darunter.",
"The display mode exclusive fullscreen uses. Applies when you leave.": "Der Anzeigemodus des exklusiven Vollbilds. Gilt, wenn du das Menü verlässt.",
"Blurs the world behind menus.": "Verwischt die Welt hinter Menüs.",
"How textures are sampled: sharper at a slant with Anisotropic.": "Wie Texturen abgetastet werden: mit Anisotrop schärfer im schrägen Blick.",
"How far Anisotropic filtering goes. Higher is sharper at a slant.": "Wie weit der anisotrope Filter geht. Höher ist schärfer im schrägen Blick.",
"Only with Anisotropic texture filtering.": "Nur mit anisotropem Texturfilter.",
"Shader Packs...": "Shader-Pakete …",
"Picks a shader pack, Horizon's or a shaders/ folder one, and sets its options.": "Wählt ein Shader-Paket, eines von Horizon oder einen shaders/-Ordner, und stellt seine Optionen ein.",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Verwendet die Schattenkarte des Pakets erneut, wenn Sonne, Blick und Welt gleich bleiben. Standardmäßig aus.",
"Synchronous": "Synchron",
"Asynchronous": "Asynchron",
@@ -466,5 +466,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Αύρες και μονοπάτια φορούν άλλοι παίκτες. Απενεργοποίηση από προεπιλογή.",
"horizon.extras.cosmetics.shown": "Εμφανίζεται",
"horizon.extras.cosmetics.hidden": "Κρυφό",
"horizon.extras.cosmetics.name_tags": "Μόνο ετικέτες ονομάτων"
"horizon.extras.cosmetics.name_tags": "Μόνο ετικέτες ονομάτων",
"Shader Packs...": "Πακέτα Shader..."
}
@@ -469,6 +469,18 @@
"Frame Generation": "Frame Generation",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Shows a made-up frame between each two real ones: smoother, a little more lag.",
"Shadow Cache": "Shadow Cache",
"Shaders": "Shaders",
"Shader packs, Horizon's and shaders/ folder ones, and their options.": "Shader packs, Horizon's and shaders/ folder ones, and their options.",
"Shader packs": "Shader packs",
"Vanilla's own preset: sets only vanilla's options, as Minecraft does.": "Vanilla's own preset: sets only vanilla's options, as Minecraft does.",
"Fullscreen takes the display over, at the mode below.": "Fullscreen takes the display over, at the mode below.",
"The display mode exclusive fullscreen uses. Applies when you leave.": "The display mode exclusive fullscreen uses. Applies when you leave.",
"Blurs the world behind menus.": "Blurs the world behind menus.",
"How textures are sampled: sharper at a slant with Anisotropic.": "How textures are sampled: sharper at a slant with Anisotropic.",
"How far Anisotropic filtering goes. Higher is sharper at a slant.": "How far Anisotropic filtering goes. Higher is sharper at a slant.",
"Only with Anisotropic texture filtering.": "Only with Anisotropic texture filtering.",
"Shader Packs...": "Shader Packs...",
"Picks a shader pack, Horizon's or a shaders/ folder one, and sets its options.": "Picks a shader pack, Horizon's or a shaders/ folder one, and sets its options.",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Reuses the pack shadow map when sun, view and world match. Off by default.",
"Find BlueMap": "Find BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "On servers, finds their BlueMap web map for far terrain. Next join.",
@@ -664,6 +676,12 @@
"horizon.pvp.hud_held_corner.tooltip": "Where the count shows.",
"horizon.pvp.hud_background": "Widget backgrounds",
"horizon.pvp.hud_background.tooltip": "A dark box behind each widget.",
"horizon.pvp.hud_opacity": "Background opacity",
"horizon.pvp.hud_opacity.tooltip": "How dark the boxes behind the widgets are.",
"horizon.pvp.hud_text_colour": "Text colour",
"horizon.pvp.hud_text_colour.tooltip": "The widgets' text colour. Durability keeps its green to red.",
"horizon.pvp.hud_shadow": "Text shadow",
"horizon.pvp.hud_shadow.tooltip": "A shadow under the widgets' text, as vanilla's HUD text has.",
"horizon.pvp.header.hud": "HUD widgets",
"horizon.pvp.corner.top_left": "Top left",
"horizon.pvp.corner.top_right": "Top right",
@@ -693,6 +711,37 @@
"horizon.pvp.hud.direction.south": "South",
"horizon.pvp.hud.direction.east": "East",
"horizon.pvp.hud.direction.west": "West",
"horizon.pvp.hud_fps": "FPS",
"horizon.pvp.hud_fps.tooltip": "Frames drawn in the last second.",
"horizon.pvp.hud_fps_corner": "FPS corner",
"horizon.pvp.hud_fps_corner.tooltip": "Where the FPS shows.",
"horizon.pvp.hud_cps": "Clicks per second",
"horizon.pvp.hud_cps.tooltip": "Your left and right clicks in the last second.",
"horizon.pvp.hud_cps_corner": "CPS corner",
"horizon.pvp.hud_cps_corner.tooltip": "Where the clicks per second show.",
"horizon.pvp.hud_keys": "Keystrokes",
"horizon.pvp.hud_keys.tooltip": "Your movement keys, mouse buttons and jump, lit while you hold them, with clicks per second.",
"horizon.pvp.hud_keys_corner": "Keystrokes corner",
"horizon.pvp.hud_keys_corner.tooltip": "Where the keystrokes show.",
"horizon.pvp.hud_clock": "Clock",
"horizon.pvp.hud_clock.tooltip": "Your computer's time.",
"horizon.pvp.hud_clock_corner": "Clock corner",
"horizon.pvp.hud_clock_corner.tooltip": "Where the clock shows.",
"horizon.pvp.hud_reach": "Reach",
"horizon.pvp.hud_reach.tooltip": "How far from your eyes your last hit landed, for 3 seconds. It only measures: your reach stays vanilla's.",
"horizon.pvp.hud_reach_corner": "Reach corner",
"horizon.pvp.hud_reach_corner.tooltip": "Where the reach shows.",
"horizon.pvp.hud_speed": "Speed",
"horizon.pvp.hud_speed.tooltip": "How fast you move across the ground, in blocks per second.",
"horizon.pvp.hud_speed_corner": "Speed corner",
"horizon.pvp.hud_speed_corner.tooltip": "Where the speed shows.",
"horizon.pvp.hud.fps": "%s FPS",
"horizon.pvp.hud.cps": "CPS: %s | %s",
"horizon.pvp.hud.reach": "Reach: %s",
"horizon.pvp.hud.speed": "Speed: %s b/s",
"horizon.pvp.hud.keys.attack": "LMB",
"horizon.pvp.hud.keys.use": "RMB",
"horizon.pvp.hud.keys.cps": "%s CPS",
"horizon.pvp.numeric_enchants": "Enchantment levels as numbers",
"horizon.pvp.numeric_enchants.tooltip": "Tooltips and the enchanting table show Sharpness 5 instead of Sharpness V.",
"horizon.pvp.header.items": "Items",
@@ -412,6 +412,7 @@
"Frame Generation": "Frama Generacio",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Montras elfaritan kadron inter ĉiu du realaj: pli glata, iom pli da malfruo.",
"Shadow Cache": "Ombra Deponejo",
"Shader Packs...": "Shader-Pakoj...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Reuzas la pak-ombromapon kiam suno, vido kaj mondo kongruas. Malŝaltita defaŭlte.",
"Find BlueMap": "Trovu BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Sur serviloj, trovas ilian BlueMap-retmapon por malproksima tereno. Poste aliĝu.",
@@ -255,5 +255,6 @@
"horizon.tempban.success": "%1$s durante %2$s, hasta %3$s",
"horizon.tempban.list.empty": "No hay baneos temporales",
"horizon.tempban.list.count": "Hay %s baneo(s) temporal(es):",
"horizon.tempban.list.entry": "%1$s fue baneado por %2$s %3$s (hasta %4$s)"
"horizon.tempban.list.entry": "%1$s fue baneado por %2$s %3$s (hasta %4$s)",
"Shader Packs...": "Paquetes de shaders..."
}
@@ -413,6 +413,7 @@
"Frame Generation": "Frame Sorkuntza",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Bi erreal bakoitzaren artean osaturiko fotograma erakusten du: leunagoa, lag apur bat gehiago.",
"Shadow Cache": "Itzalen cachea",
"Shader Packs...": "Shader paketeak...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Berrerabili paketearen itzal-mapa eguzkia, ikuspegia eta mundua bat datozenean. Desaktibatuta lehenespenez.",
"Find BlueMap": "Bilatu BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Zerbitzarietan, BlueMap web-mapa aurkitzen du urruneko lursailetarako. Hurrengoa sartu.",
@@ -413,6 +413,7 @@
"Frame Generation": "نسل فریم",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "یک قاب ساخته شده را بین هر دو قاب واقعی نشان می دهد: صاف تر، کمی تاخیر بیشتر.",
"Shadow Cache": "کش سایه",
"Shader Packs...": "بسته های سایه زن...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "هنگامی که خورشید، نما و جهان مطابقت دارند، از نقشه سایه بسته دوباره استفاده می کند. به طور پیش فرض خاموش است.",
"Find BlueMap": "BlueMap را پیدا کنید",
"On servers, finds their BlueMap web map for far terrain. Next join.": "در سرورها، نقشه وب BlueMap خود را برای مناطق دوردست پیدا می کند. عضویت بعدی",
@@ -453,5 +453,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Aurat ja jäljet, joita muut pelaajat käyttävät. Oletuksena pois päältä.",
"horizon.extras.cosmetics.shown": "Näytetään",
"horizon.extras.cosmetics.hidden": "Piilotettu",
"horizon.extras.cosmetics.name_tags": "Vain nimilaput"
"horizon.extras.cosmetics.name_tags": "Vain nimilaput",
"Shader Packs...": "Shader Packit..."
}
@@ -425,5 +425,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Mga aura at trail na isinusuot ng ibang mga manlalaro. Naka-off bilang default.",
"horizon.extras.cosmetics.shown": "Ipinakita",
"horizon.extras.cosmetics.hidden": "Nakatago",
"horizon.extras.cosmetics.name_tags": "Mga name tag lang"
"horizon.extras.cosmetics.name_tags": "Mga name tag lang",
"Shader Packs...": "Mga Shader Pack..."
}
@@ -412,6 +412,7 @@
"Frame Generation": "Rammuættarlið",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Vísir eina gjørda rammu millum hvønn tveir veruligar: slættari, eitt sindur meira lag.",
"Shadow Cache": "Skuggagoymsla",
"Shader Packs...": "Skuggapakkar...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Endurnýtir pakka skuggakortið, tá sól, útsýni og heimur passa saman. Sløkt sum standard.",
"Find BlueMap": "Finn Blákort",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Á ambætarum, finnur teirra BlueMap vevkort fyri fjart lendi. Næsta melda teg til.",
@@ -334,6 +334,18 @@
"Frame Generation": "Génération d'images",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Insère une image calculée entre deux vraies : plus fluide, un peu plus de latence.",
"Shadow Cache": "Cache des ombres",
"Shaders": "Shaders",
"Shader packs, Horizon's and shaders/ folder ones, and their options.": "Les packs de shaders, ceux de Horizon et ceux en dossier shaders/, et leurs options.",
"Shader packs": "Packs de shaders",
"Vanilla's own preset: sets only vanilla's options, as Minecraft does.": "Le préréglage de vanilla : il ne règle que les options de vanilla, comme Minecraft.",
"Fullscreen takes the display over, at the mode below.": "Le plein écran prend tout l'écran, au mode ci-dessous.",
"The display mode exclusive fullscreen uses. Applies when you leave.": "Le mode d'affichage du plein écran exclusif. S'applique en quittant.",
"Blurs the world behind menus.": "Floute le monde derrière les menus.",
"How textures are sampled: sharper at a slant with Anisotropic.": "Comment les textures sont échantillonnées : plus nettes de biais en anisotrope.",
"How far Anisotropic filtering goes. Higher is sharper at a slant.": "Jusqu'où va le filtrage anisotrope. Plus haut est plus net de biais.",
"Only with Anisotropic texture filtering.": "Seulement avec le filtrage de textures anisotrope.",
"Shader Packs...": "Packs de shaders...",
"Picks a shader pack, Horizon's or a shaders/ folder one, and sets its options.": "Choisit un pack de shaders, de Horizon ou en dossier shaders/, et règle ses options.",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Réutilise la carte d'ombres du pack si soleil, vue et monde sont identiques. Désactivé par défaut.",
"Find BlueMap": "Trouver BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Sur les serveurs, trouve leur carte web BlueMap pour le terrain lointain. À la prochaine connexion.",
@@ -407,6 +407,7 @@
"Frame Generation": "Gjenerazion di cuadri",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Al mostre un cuadri fat jenfri ognidun dai doi vêrs: plui lissîf, un pocje di plui di ritarts.",
"Shadow Cache": "Cache ombre",
"Shader Packs...": "Pachetis ombrenadôrs...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Riutilizâ la mape ombre dal grup cuant che il soreli, la viste e il mont a corispuindin. Disactivât par automatic.",
"Find BlueMap": "Cjatâ la mape blu",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Sui servidôrs, al cjate la lôr mape web BlueMap par terens lontans. Prossime unîsi.",
@@ -409,6 +409,7 @@
"Frame Generation": "Giniúint Fráma",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Taispeánann sé fráma comhdhéanta idir an dá fhíor: níos réidhe, beagán níos moille.",
"Shadow Cache": "Scáth taisce",
"Shader Packs...": "Pacaí scáthaithe...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Athúsáideann sé an scáthléarscáil paca nuair a mheaitseálann an ghrian, an radharc agus an domhan. Off de réir réamhshocraithe.",
"Find BlueMap": "Aimsigh BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Ar fhreastalaithe, aimsíonn a léarscáil gréasáin BlueMap don tír-raon i bhfad. Ar Aghaidh páirt.",
@@ -409,6 +409,7 @@
"Frame Generation": "Gineadh frèam",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "A’ sealltainn frèam dèanta eadar gach dà fhìor: nas socair, beagan a bharrachd lag.",
"Shadow Cache": "Cache sgàile",
"Shader Packs...": "Pacaidean Shader...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Ath-chleachdadh mapa sgàil a’ phasgan nuair a bhios grian, sealladh agus saoghal a’ maidseadh. Cuir dheth gu bunaiteach.",
"Find BlueMap": "Lorg BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Air frithealaichean, lorg am mapa lìn BlueMap aca airson fearann fada. An ath cheangail.",
@@ -406,6 +406,7 @@
"Frame Generation": "Xeración de cadros",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Mostra un marco feito entre cada dous reais: máis suave, un pouco máis de retraso.",
"Shadow Cache": "Caché de sombras",
"Shader Packs...": "Paquetes de sombreadores...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Reutiliza o mapa de sombras do paquete cando o sol, a vista e o mundo coinciden. Desactivado por defecto.",
"Find BlueMap": "Busca BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Nos servidores, atopa o seu mapa web BlueMap para terreos afastados. A continuación únete.",
@@ -405,6 +405,7 @@
"Frame Generation": "Hoʻokumu ʻana i ka pā",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Hōʻike i kahi kiʻi i hana ʻia ma waena o kēlā me kēia ʻelua mau mea maoli: ʻoi aku ka maʻalahi, ʻoi aku ka lag.",
"Shadow Cache": "Kahu huna",
"Shader Packs...": "Nā Pūʻolo Shader...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Hoʻohana hou i ka palapala ʻāina malu i ka wā e pāʻani ai ka lā, ka nānā a me ka honua. Paʻa ma ka paʻamau.",
"Find BlueMap": "E ʻimi iā BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Ma nā kikowaena, ʻike lākou i kā lākou palapala pūnaewele BlueMap no ka ʻāina mamao. Hoʻohui aʻe.",
@@ -469,5 +469,6 @@
"horizon.extras.cosmetics_auras.tooltip": "הילות ושבילים ששחקנים אחרים לובשים. כבוי כברירת מחדל.",
"horizon.extras.cosmetics.shown": "מוצג",
"horizon.extras.cosmetics.hidden": "מוסתר",
"horizon.extras.cosmetics.name_tags": "תגי שם בלבד"
"horizon.extras.cosmetics.name_tags": "תגי שם בלבד",
"Shader Packs...": "חבילות Shader..."
}
@@ -402,6 +402,7 @@
"Frame Generation": "फ्रेम जनरेशन",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "प्रत्येक दो वास्तविक फ़्रेमों के बीच एक काल्पनिक फ़्रेम दिखाता है: अधिक सुचारू, थोड़ा अधिक लैग।",
"Shadow Cache": "शैडो कैश",
"Shader Packs...": "शेडर पैक्स...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "जब सूरज, दृश्य और दुनिया मेल खाते हैं तो पैक शैडो मैप का पुन उपयोग करता है। डिफ़ॉल्ट रूप से बंद।",
"Find BlueMap": "ब्लूमेप खोजें",
"On servers, finds their BlueMap web map for far terrain. Next join.": "सर्वर पर, दूर के भू-भाग के लिए उनका ब्लूमेप वेब मैप खोजता है। अगली शुरुआत पर।",
@@ -395,6 +395,7 @@
"Frame Generation": "Generiranje sličica",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Prikazuje izmišljenu sličicu između svake dvije stvarne: glađe, malo veća latencija.",
"Shadow Cache": "Predmemorija sjena",
"Shader Packs...": "Paketi shadera...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Ponovno koristi mapu sjena paketa kada se sunce, pogled i svijet podudaraju. Zadano isključeno.",
"Find BlueMap": "Pronađi BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Na poslužiteljima pronalazi njihov BlueMap web preglednik karata za udaljeni teren. Sljedeće pridruživanje.",
@@ -403,6 +403,7 @@
"Frame Generation": "Keretgenerálás",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Egy-egy kitalált keretet mutat a két valódi között: simább, kicsit több késés.",
"Shadow Cache": "Árnyék gyorsítótár",
"Shader Packs...": "Shader csomagok...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Újra felhasználja a csomag árnyéktérképét, amikor a nap, a kilátás és a világ megegyezik. Alapértelmezés szerint ki.",
"Find BlueMap": "Keresse meg a BlueMap-et",
"On servers, finds their BlueMap web map for far terrain. Next join.": "A szervereken megkeresi a BlueMap webtérképüket a távoli terepre. Következő csatlakozás.",
@@ -408,6 +408,7 @@
"Frame Generation": "Շրջանակների սերունդ",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Ցույց է տալիս պատրաստված շրջանակը յուրաքանչյուր երկու իրականի միջև՝ ավելի հարթ, մի փոքր ավելի ուշացումով:",
"Shadow Cache": "Ստվերային քեշ",
"Shader Packs...": "Shader փաթեթներ...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Կրկին օգտագործում է փաթեթի ստվերային քարտեզը, երբ արևը, տեսարանը և աշխարհը համընկնում են: Լռելյայն անջատված է:",
"Find BlueMap": "Գտեք BlueMap-ը",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Սերվերների վրա գտնում է իրենց BlueMap վեբ քարտեզը հեռավոր տեղանքի համար: Հաջորդ միանալ.",
@@ -389,6 +389,7 @@
"Frame Generation": "Pembuatan Bingkai (Frame Generation)",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Menampilkan bingkai buatan di antara setiap dua bingkai nyata: lebih mulus, dengan sedikit tambahan lag.",
"Shadow Cache": "Cache Bayangan",
"Shader Packs...": "Paket Shader...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Menggunakan kembali peta bayangan paket saat posisi matahari, pandangan, dan dunia cocok. Nonaktif secara default.",
"Find BlueMap": "Cari BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Pada server, mencari peta web BlueMap mereka untuk medan jauh. Pada saat bergabung berikutnya.",
@@ -405,6 +405,7 @@
"Frame Generation": "Ọgbọ Frame",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Na-egosi etiti emebere n'etiti ezigbo abụọ nke ọ bụla: dị nro, ntakịrị lag.",
"Shadow Cache": "Cache onyinyo",
"Shader Packs...": "Ngwungwu Shader...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Na-ejigharị maapụ onyinyo mkpọ mgbe anwụ, nlele na egwuregwu ụwa. Gbanyụọ na ndabara.",
"Find BlueMap": "Chọta BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Na sava, na-achọta maapụ weebụ BlueMap ha maka ebe dị anya. sonye ọzọ.",
@@ -469,5 +469,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Auras og slóðir sem aðrir leikmenn klæðast. Slökkt sjálfgefið.",
"horizon.extras.cosmetics.shown": "Sýnd",
"horizon.extras.cosmetics.hidden": "Falinn",
"horizon.extras.cosmetics.name_tags": "Eingöngu nafnamerki"
"horizon.extras.cosmetics.name_tags": "Eingöngu nafnamerki",
"Shader Packs...": "Shader pakkar..."
}
@@ -421,6 +421,7 @@
"Frame Generation": "Generazione di fotogrammi",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Mostra un fotogramma inventato tra ciascuno dei due fotogrammi reali: più fluido, con un po' più di ritardo.",
"Shadow Cache": "Cassa dell'Ombra",
"Shader Packs...": "Pacchetti Shader...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Riutilizza la mappa delle ombre del pacchetto quando sole, vista e mondo corrispondono. Disattivato per impostazione predefinita.",
"Find BlueMap": "Trova BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Sui server, trova la mappa web BlueMap per terreni lontani. Prossima partecipazione.",
@@ -425,6 +425,7 @@
"Frame Generation": "フレーム生成",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "2 つの実際のフレームの間に構成されたフレームを表示します。よりスムーズで、少し遅れが生じます。",
"Shadow Cache": "シャドウキャッシュ",
"Shader Packs...": "シェーダーパック...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "太陽、ビュー、ワールドが一致する場合、パック シャドウ マップを再利用します。デフォルトではオフです。",
"Find BlueMap": "ブルーマップを探す",
"On servers, finds their BlueMap web map for far terrain. Next join.": "サーバー上で、遠方の地形用の BlueMap Web マップを検索します。次に参加します。",
@@ -415,6 +415,7 @@
"Frame Generation": "ჩარჩოს თაობა",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "აჩვენებს შედგენილ ჩარჩოს თითოეულ ორ რეალურს შორის: უფრო გლუვი, ცოტა მეტი ჩამორჩენა.",
"Shadow Cache": "ჩრდილების ქეში",
"Shader Packs...": "შადერის პაკეტები...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "ხელახლა იყენებს პაკეტის ჩრდილის რუკას, როდესაც მზე, ხედი და სამყარო ემთხვევა. ნაგულისხმევად გამორთულია.",
"Find BlueMap": "იპოვეთ BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "სერვერებზე პოულობს მათ BlueMap ვებ რუკას შორეული რელიეფისთვის. შემდეგი შეერთება.",
@@ -415,6 +415,7 @@
"F2 screenshots at this many times the game's resolution. Higher takes longer.": "F2 скриншоттары ойынның ажыратымдылығын бірнеше есе арттырады. Жоғарырақ болу ұзағырақ уақыт алады.",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Әрбір екі нақты жақтаудың арасында жасалған жақтауды көрсетеді: тегіс, сәл кідіріс.",
"Shadow Cache": "Көлеңкелік кэш",
"Shader Packs...": "Шейдер бумалары...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Күн, көрініс және әлем сәйкес болған кезде пакеттің көлеңке картасын қайта пайдаланады. Әдепкі бойынша өшірулі.",
"Find BlueMap": "BlueMap табыңыз",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Серверлерде алыс жерлерге арналған BlueMap веб-картасын табады. Келесі қосылу.",
@@ -422,6 +422,7 @@
"Frame Generation": "ಫ್ರೇಮ್ ಜನರೇಷನ್",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "ಪ್ರತಿ ಎರಡು ನೈಜವಾದವುಗಳ ನಡುವೆ ಮಾಡಿದ ಚೌಕಟ್ಟನ್ನು ತೋರಿಸುತ್ತದೆ: ಸುಗಮ, ಸ್ವಲ್ಪ ಹೆಚ್ಚು ವಿಳಂಬ.",
"Shadow Cache": "ನೆರಳು ಸಂಗ್ರಹ",
"Shader Packs...": "ಶೇಡರ್ ಪ್ಯಾಕ್‌ಗಳು...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "ಸೂರ್ಯ, ವೀಕ್ಷಣೆ ಮತ್ತು ಪ್ರಪಂಚವು ಹೊಂದಿಕೆಯಾದಾಗ ಪ್ಯಾಕ್ ನೆರಳು ನಕ್ಷೆಯನ್ನು ಮರುಬಳಕೆ ಮಾಡುತ್ತದೆ. ಡೀಫಾಲ್ಟ್ ಆಗಿ ಆಫ್.",
"Find BlueMap": "ಬ್ಲೂಮ್ಯಾಪ್ ಅನ್ನು ಹುಡುಕಿ",
"On servers, finds their BlueMap web map for far terrain. Next join.": "ಸರ್ವರ್‌ಗಳಲ್ಲಿ, ದೂರದ ಭೂಪ್ರದೇಶಕ್ಕಾಗಿ ಅವರ ಬ್ಲೂಮ್ಯಾಪ್ ವೆಬ್ ನಕ್ಷೆಯನ್ನು ಕಂಡುಕೊಳ್ಳುತ್ತದೆ. ಮುಂದೆ ಸೇರಿಕೊಳ್ಳಿ.",
@@ -398,6 +398,7 @@
"Frame Generation": "프레임 생성",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "실제 프레임 사이에 가상의 프레임을 삽입합니다: 더 부드러워지지만 지연이 약간 증가합니다.",
"Shadow Cache": "그림자 캐시",
"Shader Packs...": "셰이더 팩...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "태양, 시점, 월드가 일치할 때 팩 그림자 맵을 재사용합니다. 기본값은 끔입니다.",
"Find BlueMap": "BlueMap 찾기",
"On servers, finds their BlueMap web map for far terrain. Next join.": "서버 접속 시 원거리 지형을 위한 BlueMap 웹 맵을 찾습니다. 다음 참여 시 적용됩니다.",
@@ -390,6 +390,7 @@
"F2 screenshots at this many times the game's resolution. Higher takes longer.": "F2 скриншоттору бул оюндун резолюциясына көп жолу. Жогорку көбүрөөк убакыт талап кылынат.",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Ар бир эки реалдуу кадрдын ортосунда жасалган кадрды көрсөтөт: жылмакай, бир аз артта калуу.",
"Shadow Cache": "Shadow кэш",
"Shader Packs...": "Шейдер топтомдору...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Күн, көрүнүш жана дүйнө дал келгенде, таңгак көлөкө картасын кайра колдонот. Демейки боюнча өчүк.",
"Find BlueMap": "BlueMap тап",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Серверлерде алардын BlueMap веб картасын алыскы аймактар үчүн табат. Кийинки кошулуу.",
@@ -409,6 +409,7 @@
"Frame Generation": "Generaziun di fotogramm",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Mustra un quader truà intra ognun di dü reai: püsee lisc, un pooch püsee ritardo.",
"Shadow Cache": "Cache d'ombra",
"Shader Packs...": "Pacchett de ombreggiatura...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Dupera ancamò la mapa del ombra del pachett quand el sul, la vista e el mund curispunden. Disattiva per predefiniziun.",
"Find BlueMap": "Truva BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Süi server, truva luur mapa web BlueMap per terren luntan. Prossim uniment.",
@@ -468,5 +468,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Aura ແລະ trails ຜູ້ນອື່ນໆໃສ່. ປິດຕາມຄ່າເລີ່ມຕົ້ນ.",
"horizon.extras.cosmetics.shown": "ສະແດງໃຫ້ເຫັນ",
"horizon.extras.cosmetics.hidden": "ເຊື່ອງໄວ້",
"horizon.extras.cosmetics.name_tags": "ປ້າຍຊື່ເທົ່ານັ້ນ"
"horizon.extras.cosmetics.name_tags": "ປ້າຍຊື່ເທົ່ານັ້ນ",
"Shader Packs...": "ຊອງ Shader..."
}
@@ -421,6 +421,7 @@
"Frame Generation": "Генерација на рамки",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Покажува изработена рамка помеѓу секоја две реални: помазна, малку повеќе заостанување.",
"Shadow Cache": "Кеш во сенка",
"Shader Packs...": "Пакети за шејдер...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Повторно ја користи мапата на сенки на пакетот кога сонцето, погледот и светот се совпаѓаат. Исклучено стандардно.",
"Find BlueMap": "Најдете BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "На серверите, ја наоѓа нивната веб-мапа BlueMap за далечен терен. Следно придружете се.",
@@ -417,6 +417,7 @@
"F2 screenshots at this many times the game's resolution. Higher takes longer.": "F2 дэлгэцийн агшин нь тоглоомын нягтралаас хэд дахин их. Өндөр нь илүү урт хугацаа шаарддаг.",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Хоёр бодит хоёрын хооронд зохиосон хүрээг харуулна: илүү зөөлөн, бага зэрэг хоцрогдолтой.",
"Shadow Cache": "Сүүдрийн кэш",
"Shader Packs...": "Shader багцууд...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Нар, харагдац, ертөнц таарах үед сүүдрийн зургийг дахин ашигладаг. Өгөгдмөлөөр унтраа.",
"Find BlueMap": "BlueMap-г олоорой",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Серверүүдээс хол газрын BlueMap вэб газрын зургийг олдог. Дараагийн нэгдэх.",
@@ -405,6 +405,7 @@
"Frame Generation": "Penjanaan Bingkai",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Menunjukkan bingkai yang dibuat antara setiap dua bingkai sebenar: lebih lancar, sedikit ketinggalan.",
"Shadow Cache": "Cache Bayangan",
"Shader Packs...": "Pek Shader...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Menggunakan semula peta bayangan pek apabila matahari, paparan dan dunia sepadan. Mati secara lalai.",
"Find BlueMap": "Cari BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Pada pelayan, temui peta web BlueMap mereka untuk rupa bumi yang jauh. Sertai seterusnya.",
@@ -436,5 +436,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Auras u trails plejers oħra jilbsu. Mitfi awtomatikament.",
"horizon.extras.cosmetics.shown": "murija",
"horizon.extras.cosmetics.hidden": "Moħbija",
"horizon.extras.cosmetics.name_tags": "Tikketti tal-isem biss"
"horizon.extras.cosmetics.name_tags": "Tikketti tal-isem biss",
"Shader Packs...": "Pakketti Shader..."
}
@@ -411,6 +411,7 @@
"Frame Generation": "Framegeneratie",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Toont een verzonnen frame tussen elke twee echte: vloeiender, iets meer vertraging.",
"Shadow Cache": "Schaduwcache",
"Shader Packs...": "Shader-pakketten...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Hergebruikt de roedelschaduwkaart wanneer zon, zicht en wereld overeenkomen. Standaard uitgeschakeld.",
"Find BlueMap": "Zoek BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Vindt op servers hun BlueMap-webkaart voor ver terrein. Volgende meedoen.",
@@ -453,5 +453,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Auraer og stier andre spillere har på seg. Av som standard.",
"horizon.extras.cosmetics.shown": "Vist",
"horizon.extras.cosmetics.hidden": "Skjult",
"horizon.extras.cosmetics.name_tags": "Kun navnelapper"
"horizon.extras.cosmetics.name_tags": "Kun navnelapper",
"Shader Packs...": "Shader-pakker..."
}
@@ -405,6 +405,7 @@
"Frame Generation": "Generacion de quadres",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Mòstra un encastre fabricat entre cadun dos reals: mai doç, un pauc mai de retard.",
"Shadow Cache": "Cache d'ombra",
"Shader Packs...": "Paquets de Shader...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Reutiliza la mapa d'ombra del paquet quand solelh, vista e mond correspondon. Desactivat per defaut.",
"Find BlueMap": "Trobar BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Sus de servidors, tròba lor mapa web BlueMap per de terrens luènhs. Seguent rejónher.",
@@ -316,6 +316,7 @@
"Frame Generation": "Generowanie klatek",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Pokazuje wygenerowaną klatkę między każdymi dwiema prawdziwymi: płynniej, trochę większe opóźnienie.",
"Shadow Cache": "Pamięć cieni",
"Shader Packs...": "Paczki shaderów…",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Używa ponownie mapy cieni paczki, gdy słońce, widok i świat się zgadzają. Domyślnie wyłączone.",
"Synchronous": "Synchroniczne",
"Asynchronous": "Asynchroniczne",
@@ -425,6 +425,7 @@
"Frame Generation": "Geração de quadros",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Mostra um quadro composto entre cada dois quadros reais: mais suave, um pouco mais de atraso.",
"Shadow Cache": "Cache de sombra",
"Shader Packs...": "Pacotes de sombreadores...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Reutiliza o mapa de sombras do pacote quando o sol, a visão e o mundo correspondem. Desativado por padrão.",
"Find BlueMap": "Encontre o BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Nos servidores, encontra seu mapa da web BlueMap para terrenos distantes. Próxima adesão.",
@@ -381,6 +381,7 @@
"Frame Generation": "Geração de fotogramas",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Mostra um fotograma gerado entre cada dois reais: maior fluidez, com uma ligeira latência adicional.",
"Shadow Cache": "Cache de sombras",
"Shader Packs...": "Pacotes de shaders...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Reutiliza o mapa de sombras do pacote quando o sol, a visão e o mundo coincidem. Desligado por predefinição.",
"Find BlueMap": "Encontrar BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Em servidores, encontra o mapa web BlueMap para terreno distante. Na próxima entrada.",
@@ -457,5 +457,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Aurele și traseele pe care le poartă alți jucători. Oprit implicit.",
"horizon.extras.cosmetics.shown": "Afisat",
"horizon.extras.cosmetics.hidden": "Ascuns",
"horizon.extras.cosmetics.name_tags": "Numai etichete de nume"
"horizon.extras.cosmetics.name_tags": "Numai etichete de nume",
"Shader Packs...": "Pachete de shader..."
}
@@ -226,5 +226,6 @@
"How frames reach the screen, and the performance overlay.": "Как кадры попадают на экран, и оверлей производительности.",
"Brighter-than-white output on an HDR display.": "Вывод ярче белого на HDR-дисплее.",
"Results for \"%s\"": "Результаты для «%s»",
"No settings match.": "Нет подходящих настроек."
"No settings match.": "Нет подходящих настроек.",
"Shader Packs...": "Шейдерпаки..."
}
@@ -423,6 +423,7 @@
"Frame Generation": "Кадрдары оҥоруу",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Икки дьиҥнээх икки ардыгар оҥоһуллубут рамканы көрдөрөр: сымнаҕас, кыратык хойутаан.",
"Shadow Cache": "Күлүк кэш",
"Shader Packs...": "Шейдер пакеттара...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Күн, көрүү уонна аан дойду сөп түбэстэхтэринэ, пакет күлүк хаартыскатын хаттаан туһанар. По умолчанию выключено.",
"Find BlueMap": "BlueMap бул",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Сервердарга ыраах сиргэ BlueMap веб-каарталарын булар. Салгыы холбоһуҥ.",
@@ -395,6 +395,7 @@
"Frame Generation": "Generovanie rámu",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Zobrazuje vytvorený rám medzi každým z dvoch skutočných: hladší, trochu viac oneskorenia.",
"Shadow Cache": "Tieňová vyrovnávacia pamäť",
"Shader Packs...": "Shader Pack...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Opätovne použije tieňovú mapu balenia, keď sa slnko, výhľad a svet zhodujú. Predvolene vypnuté.",
"Find BlueMap": "Nájdite BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Na serveroch nájde svoju webovú mapu BlueMap pre vzdialený terén. Ďalšie pripojenie.",
@@ -470,5 +470,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Avre in sledi, ki jih nosijo drugi igralci. Privzeto izklopljeno.",
"horizon.extras.cosmetics.shown": "Prikazano",
"horizon.extras.cosmetics.hidden": "Skrito",
"horizon.extras.cosmetics.name_tags": "Samo imenske oznake"
"horizon.extras.cosmetics.name_tags": "Samo imenske oznake",
"Shader Packs...": "Shader paketi..."
}
@@ -455,5 +455,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Auras iyo raad-raacyo ciyaartoy kale xidhaan. Demin sida caadiga ah.",
"horizon.extras.cosmetics.shown": "Muujiyay",
"horizon.extras.cosmetics.hidden": "Qarsoon",
"horizon.extras.cosmetics.name_tags": "Magac tags kaliya"
"horizon.extras.cosmetics.name_tags": "Magac tags kaliya",
"Shader Packs...": "Xirmooyinka Shader..."
}
@@ -408,6 +408,7 @@
"Frame Generation": "Gjenerimi i kornizës",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Tregon një kornizë të krijuar midis secilës dy të vërteta: më e butë, pak më shumë vonesë.",
"Shadow Cache": "Cache hije",
"Shader Packs...": "Paketat Shader...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Ripërdor hartën e hijes së paketës kur dielli, pamja dhe bota përputhen. Joaktiv si parazgjedhje.",
"Find BlueMap": "Gjeni BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Në serverë, gjen hartën e tyre të internetit BlueMap për terrene të largëta. Bashkohu tjetër.",
@@ -426,6 +426,7 @@
"Frame Generation": "Generacija okvira",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Prikazuje napravljeni okvir između svaka dva stvarna: glatkiji, malo više zaostajanja.",
"Shadow Cache": "Shadov Cache",
"Shader Packs...": "Shader paketi...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Ponovo koristi mapu senki paketa kada se poklope sunce, pogled i svet. Podrazumevano isključeno.",
"Find BlueMap": "Pronađite BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Na serverima pronalazi njihovu BlueMap veb mapu za daleki teren. Sledeće pridruživanje.",
@@ -426,6 +426,7 @@
"Frame Generation": "Генерација оквира",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Приказује направљени оквир између свака два стварна: глаткији, мало више заостајања.",
"Shadow Cache": "Схадов Цацхе",
"Shader Packs...": "Схадер пакети...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Поново користи мапу сенки пакета када се поклопе сунце, поглед и свет. Подразумевано искључено.",
"Find BlueMap": "Пронађите БлуеМап",
"On servers, finds their BlueMap web map for far terrain. Next join.": "На серверима проналази њихову БлуеМап веб мапу за далеки терен. Следеће придруживање.",
@@ -454,5 +454,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Auror och spår som andra spelare bär. Av som standard.",
"horizon.extras.cosmetics.shown": "Visas",
"horizon.extras.cosmetics.hidden": "Dold",
"horizon.extras.cosmetics.name_tags": "Endast namnlappar"
"horizon.extras.cosmetics.name_tags": "Endast namnlappar",
"Shader Packs...": "Shader-paket..."
}
@@ -415,6 +415,7 @@
"Frame Generation": "Gyneracyjo rōm",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Pokazuje stworzōny rōm miyndzy kożdymi dwōma prawdziwymi: gładkij, trocha wiyncyj ôdwłōczki.",
"Shadow Cache": "Pamiyńć podrzōdkowo ciyni",
"Shader Packs...": "Pakiety ciyniōw...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Zaś używo karty ciyni, kej słōńce, widok i świat sztimujōm. Wyłōnczōne domyślnie.",
"Find BlueMap": "Znojdź BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Na serwerach znojdzie jejich karta internetowo BlueMap do dalekigo terynu. Dalszy prziłōnczynie sie.",
@@ -422,6 +422,7 @@
"Frame Generation": "பிரேம் தலைமுறை",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "ஒவ்வொரு இரண்டு உண்மையானவற்றிற்கும் இடையே ஒரு தயாரிக்கப்பட்ட சட்டத்தைக் காட்டுகிறது: மென்மையானது, இன்னும் கொஞ்சம் தாமதம்.",
"Shadow Cache": "நிழல் கேச்",
"Shader Packs...": "ஷேடர் பேக்ஸ்...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "சூரியன், காட்சி மற்றும் உலகம் பொருந்தும்போது பேக் நிழல் வரைபடத்தை மீண்டும் பயன்படுத்துகிறது. இயல்பாக ஆஃப்.",
"Find BlueMap": "BlueMap ஐக் கண்டறியவும்",
"On servers, finds their BlueMap web map for far terrain. Next join.": "சேவையகங்களில், தொலைதூர நிலப்பரப்பிற்கான அவர்களின் BlueMap வலை வரைபடத்தைக் கண்டறியும். அடுத்து சேரவும்.",
@@ -429,6 +429,7 @@
"Frame Generation": "การสร้างเฟรม",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "แสดงเฟรมที่ถูกสร้างขึ้นระหว่างเฟรมจริงสองเฟรม: นุ่มนวลกว่า แลคกว่าเล็กน้อย",
"Shadow Cache": "แคชเงา",
"Shader Packs...": "เชเดอร์แพ็ค...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "ใช้แผนที่เงาแพ็คซ้ำเมื่อดวงอาทิตย์ มุมมอง และโลกตรงกัน ปิดโดยค่าเริ่มต้น",
"Find BlueMap": "ค้นหาบลูแมป",
"On servers, finds their BlueMap web map for far terrain. Next join.": "บนเซิร์ฟเวอร์ ค้นหาแผนที่เว็บ BlueMap สำหรับภูมิประเทศที่ห่างไกล เข้าร่วมต่อไป.",
@@ -221,5 +221,6 @@
"How frames reach the screen, and the performance overlay.": "Karelerin ekrana nasıl ulaştığı ve performans kaplaması.",
"Brighter-than-white output on an HDR display.": "HDR ekranda beyazdan parlak çıkış.",
"Results for \"%s\"": "\"%s\" için sonuçlar",
"No settings match.": "Eşleşen ayar yok."
"No settings match.": "Eşleşen ayar yok.",
"Shader Packs...": "Gölgelendirici Paketleri..."
}
@@ -416,6 +416,7 @@
"Frame Generation": "Рам буын",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Realәрбер реаль арада ясалган рамны күрсәтә: йомшак, бераз артта калу.",
"Shadow Cache": "Күләгә Кэш",
"Shader Packs...": "Шейдер пакетлар ...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Кояш, күренеш һәм дөнья туры килгәндә пакет күләгә картасын кабат куллана. Килешү буенча сүндерелгән.",
"Find BlueMap": "BlueMap табыгыз",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Серверларда, аларның BlueMap веб картасын ерак җирләр өчен табалар. Киләсе кушылыгыз.",
@@ -316,6 +316,7 @@
"Frame Generation": "Генерація кадрів",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Показує згенерований кадр між кожними двома справжніми: плавніше, трохи більша затримка.",
"Shadow Cache": "Кеш тіней",
"Shader Packs...": "Пакети шейдерів…",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Повторно використовує карту тіней пакета, коли сонце, вигляд і світ збігаються. Типово вимкнено.",
"Synchronous": "Синхронно",
"Asynchronous": "Асинхронно",
@@ -399,5 +399,6 @@
"horizon.repro.bundle": "ریپرو بنڈل: %s",
"horizon.repro.error": "ریپرو: %s",
"horizon.repro.no_recording": "برآمد کرنے کے لیے ابھی تک کوئی ریکارڈنگ نہیں ہے۔",
"horizon.repro.replay_started": "ری پلے شروع ہو گیا: %s"
"horizon.repro.replay_started": "ری پلے شروع ہو گیا: %s",
"Shader Packs...": "شیڈر پیکس..."
}
@@ -406,6 +406,7 @@
"Frame Generation": "Ramka yaratish",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Har ikki haqiqiy ramka o'rtasida tuzilgan ramkani ko'rsatadi: silliqroq, biroz kechikish.",
"Shadow Cache": "Soya keshi",
"Shader Packs...": "Shader toʻplamlari...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Quyosh, ko'rinish va dunyo mos kelganda to'plam soya xaritasidan qayta foydalanadi. Sukut boʻyicha oʻchirilgan.",
"Find BlueMap": "BlueMap-ni toping",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Serverlarda uzoq erlar uchun BlueMap veb-xaritasini topadi. Keyingi qo'shilish.",
@@ -468,5 +468,6 @@
"horizon.extras.cosmetics_auras.tooltip": "Aure e sentieri che altri zugadori i ga. Spento par indebołimento.",
"horizon.extras.cosmetics.shown": "Mostrà",
"horizon.extras.cosmetics.hidden": "Sconto",
"horizon.extras.cosmetics.name_tags": "Solo etichete de nome"
"horizon.extras.cosmetics.name_tags": "Solo etichete de nome",
"Shader Packs...": "Pachetti de Shader..."
}
@@ -420,6 +420,7 @@
"Frame Generation": "Tạo khung",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Hiển thị khung hình tạo sẵn giữa hai khung hình thực: mượt mà hơn, độ trễ hơn một chút.",
"Shadow Cache": "Bộ nhớ đệm bóng tối",
"Shader Packs...": "Gói đổ bóng...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Tái sử dụng bản đồ bóng gói khi mặt trời, chế độ xem và thế giới khớp với nhau. Tắt theo mặc định.",
"Find BlueMap": "Tìm BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Trên máy chủ, tìm bản đồ web BlueMap của họ cho địa hình xa. Tiếp theo tham gia.",
@@ -420,6 +420,7 @@
"Frame Generation": "ראַם דור",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "ווייזט אַ געמאכט-אַרויף ראַם צווישן יעדער צוויי פאַקטיש אָנעס: סמודער, אַ ביסל מער אָפּשטיי.",
"Shadow Cache": "שאָטן קאַש",
"Shader Packs...": "שאַדער פּאַקס ...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "ריוזז די פּאַק שאָטן מאַפּע ווען זון, מיינונג און וועלט גלייַכן. אַוועק דורך פעליקייַט.",
"Find BlueMap": "געפֿינען BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "אויף סערווערס, איר געפֿינען זייער BlueMap וועב מאַפּע פֿאַר ווייַט טעריין. ווייַטער פאַרבינדן.",
@@ -412,6 +412,7 @@
"Frame Generation": "Fireemu Iran",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Ṣe afihan fireemu ti a ṣe laarin ọkọọkan awọn gidi meji: didan, aisun diẹ sii.",
"Shadow Cache": "Kaṣe ojiji",
"Shader Packs...": "Awọn akopọ Shader...",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Tun lo maapu ojiji idii nigbati oorun, wiwo ati ibaamu agbaye. Paa nipa aiyipada.",
"Find BlueMap": "Wa BlueMap",
"On servers, finds their BlueMap web map for far terrain. Next join.": "Lori awọn olupin, rii maapu wẹẹbu BlueMap wọn fun ilẹ jijin. Next da.",
@@ -333,6 +333,7 @@
"Frame Generation": "帧生成",
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "在每两个真实帧之间插入一个生成帧:更流畅,延迟略增。",
"Shadow Cache": "阴影缓存",
"Shader Packs...": "光影包…",
"Reuses the pack shadow map when sun, view and world match. Off by default.": "太阳、视角和世界不变时重复使用资源包的阴影贴图。默认关闭。",
"Synchronous": "同步",
"Asynchronous": "异步",
@@ -268,6 +268,7 @@ const PAGES: Ty = Ty::Enum(&[
"extras",
"horizon",
"agent",
"video-vanilla",
]);
/// `--confirm`, required by destructive commands: the player agreed.
+120 -46
View File
@@ -9,8 +9,8 @@ use crate::{
items::{ItemStack, Items},
particles::{DYE_FIREWORK_COLORS, FireworkExplosion, FireworkShape},
};
use horizon_world::loot::PackOverlay;
use horizon_world::nbt::Tag;
use horizon_world::nbt::{JavaString, Tag};
use horizon_world::{component_patch::Patch, loot::PackOverlay};
use serde_json::Value;
use std::{
collections::{HashMap, HashSet},
@@ -273,6 +273,8 @@ pub struct Template {
pub count: u32,
pub damage: u32,
pub extra: Option<Box<StackData>>,
/// Result patch before decoding defaults, including explicit removals/zeroes.
pub components: Option<Tag>,
}
/// `ItemStackTemplate.CODEC` count: absent means 1, present must be an
/// integer in `1..=99` (`ExtraCodecs.intRange(1, 99)`), else the recipe is
@@ -288,15 +290,14 @@ pub(crate) fn template_count(v: &Value) -> Option<u32> {
}
impl Template {
fn parse(v: &Value) -> Option<Self> {
let (damage, extra) = match v.get("components") {
Some(c) => crate::item_data::decode_components(Some(&json_tag(c))),
None => (0, None),
};
let components = v.get("components").map(json_tag);
let (damage, extra) = crate::item_data::decode_components(components.as_ref());
Some(Self {
item: horizon_world::loot::recipe_result_id(v).map(Arc::from),
count: template_count(v)?,
damage,
extra,
components,
})
}
/// `ItemStackTemplate.create`.
@@ -314,56 +315,65 @@ impl Template {
/// `TransmuteRecipe.createWithOriginalComponents`: the input's component
/// patch on the result item, then the template's own components over it.
fn over(&self, input: &ItemStack, extra_count: u32) -> ItemStack {
let original = assembly_components(input);
let result = self
.components
.clone()
.unwrap_or_else(|| assembly_components(&self.create()));
// ItemStackTemplate.apply: the second patch wins for each component;
// a removal replaces a value rather than coexisting with it.
let entries = |tag: Tag| match tag {
Tag::Compound(map) => map
.into_iter()
.map(|(key, value)| {
let key = key.to_string_lossy();
match key.strip_prefix('!') {
Some(key) => (key.to_owned(), None),
None => (key.to_owned(), Some(value)),
}
})
.collect::<Vec<_>>(),
_ => Vec::new(),
};
let patch = Patch::new(entries(original).into_iter().chain(entries(result)));
let components = Tag::compound(patch.entries().iter().map(|(key, value)| match value {
Some(value) => (JavaString::from(key.as_str()), value.clone()),
None => (
JavaString::from(format!("!{key}").as_str()),
Tag::Compound(Default::default()),
),
}));
let (damage, extra) = crate::item_data::decode_components(Some(&components));
let mut out = ItemStack {
item: self.item.clone().unwrap_or_else(|| input.item.clone()),
count: self.count + extra_count,
damage: input.damage,
extra: input.extra.clone(),
damage,
extra,
};
if self.damage > 0 {
out.damage = self.damage;
}
if let Some(over) = &self.extra {
overlay(out.data_mut(), over);
if let Some(input) = input.data() {
// Runtime-only stack bookkeeping is not a saved component patch.
let data = out.data_mut();
data.wire = input.wire.clone();
data.bundle_selected = input.bundle_selected;
}
out.tidy();
out
}
}
/// `DataComponentPatch` application: every component the overlay sets wins.
fn overlay(dst: &mut StackData, src: &StackData) {
let none = StackData::default();
macro_rules! field {
($($f:ident),*) => {$(
if src.$f != none.$f {
dst.$f = src.$f.clone();
}
)*};
/// Assembly also carries the transient map postprocess component; the saved
/// ItemStack codec deliberately omits it. Do not turn it into saved data.
fn assembly_components(stack: &ItemStack) -> Tag {
let mut components = match crate::item_data::encode_components(stack) {
Some(Tag::Compound(map)) => map,
_ => Default::default(),
};
if let Some(postprocess) = stack.data().and_then(|d| d.map_post_processing) {
components.insert(
"minecraft:map_post_processing".into(),
Tag::Int(i32::from(postprocess)),
);
}
field!(
enchantments,
stored_enchantments,
repair_cost,
custom_name,
potion,
trim,
container,
charged,
fireworks,
map_id,
writable_pages,
written,
banner_patterns,
base_color,
ominous_banner,
ominous_amplifier,
dyed_color,
firework_explosion,
pot_decorations,
bundle_contents,
stew_effects,
map_post_processing
);
Tag::Compound(components)
}
/// A JSON recipe value as the NBT a component codec reads.
fn json_tag(v: &Value) -> Tag {
@@ -1989,6 +1999,70 @@ mod tests {
use crate::banner::BannerLayer;
use std::fs;
/// ItemStackTemplate.apply applies the result patch after the input patch.
#[test]
fn transmute_result_patch_preserves_inheritance_and_explicit_removals() {
let input_tag = json_tag(&serde_json::json!({
"minecraft:damage": 5, "minecraft:lore": ["Old"]
}));
let (damage, extra) = crate::item_data::decode_components(Some(&input_tag));
let input = ItemStack {
item: "minecraft:wooden_sword".into(),
count: 1,
damage,
extra,
};
for (components, expected_damage, removed) in [
(
serde_json::json!({"minecraft:custom_name": "Kept"}),
5,
false,
),
(
serde_json::json!({"minecraft:custom_name": "Up", "!minecraft:damage": {}}),
0,
true,
),
(
serde_json::json!({"minecraft:custom_name": "Zero", "minecraft:damage": 0}),
0,
false,
),
] {
let template = Template::parse(&serde_json::json!({
"id": "minecraft:stone_sword", "components": components
}))
.unwrap();
let output = template.over(&input, 0);
assert_eq!(output.item.as_ref(), "minecraft:stone_sword");
assert_eq!(output.damage, expected_damage);
let encoded = crate::item_data::encode_components(&output).unwrap();
assert_eq!(
encoded.get("minecraft:lore"),
input_tag.get("minecraft:lore")
);
assert_eq!(
encoded.get("minecraft:custom_name"),
Some(&json_tag(&components["minecraft:custom_name"]))
);
assert_eq!(encoded.get("!minecraft:damage").is_some(), removed);
if expected_damage == 0 {
assert!(encoded.get("minecraft:damage").is_none());
}
}
let mut map = input.clone();
map.data_mut().map_post_processing = Some(1);
let template = Template::parse(&serde_json::json!({"id": "minecraft:filled_map"})).unwrap();
let output = template.over(&map, 0);
assert_eq!(output.data().unwrap().map_post_processing, Some(1));
assert!(
crate::item_data::encode_components(&output)
.unwrap()
.get("minecraft:map_post_processing")
.is_none()
);
}
#[test]
fn result_count_is_bounded_like_vanilla() {
let parse = |s: &str| Template::parse(&serde_json::from_str(s).unwrap());
+22 -3
View File
@@ -119,6 +119,9 @@ mod dragon;
mod dynamic_lights;
mod enchant_effects;
mod end;
mod enderman;
#[cfg(test)]
mod enderman_placement_tests;
mod entity;
mod entity_culling;
mod entity_sounds;
@@ -2022,15 +2025,20 @@ struct WorldTables {
/// The values of the data map types native mods registered
/// (`native_data_map_types`).
data_maps: native_data_maps::DataMaps,
/// The entity type, biome and damage type tags with the packs' over the
/// game's own, by registry (`TagLoader.loadTagsForExistingRegistries`):
/// The entity type, biome, structure and damage type tags with the packs'
/// over the game's own, by registry (`TagLoader.loadTagsForExistingRegistries`):
/// what the predicates' `#tag`s of those registries read.
pack_tags: HashMap<&'static str, HashMap<String, HashSet<String>>>,
}
impl WorldTables {
/// The registries [`WorldTables::pack_tags`] holds.
const PACK_TAG_REGISTRIES: [&'static str; 3] = ["entity_type", "worldgen/biome", "damage_type"];
const PACK_TAG_REGISTRIES: [&'static str; 4] = [
"entity_type",
"worldgen/biome",
"worldgen/structure",
"damage_type",
];
}
impl Resources {
@@ -5932,7 +5940,15 @@ impl Game<'_> {
// EnderChestBlock.useWithoutItem
self.award_custom_stat("open_enderchest", 1);
self.anger_piglins(true);
// EnderChestBlockEntity.startOpen: the first opener's CONTAINER_OPEN
// (ContainerOpenersCounter.incrementOpeners), as for chests below.
if self.container_opener_event(key, true) {
self.post_block_game_event(
"minecraft:container_open",
[key.0, key.1, key.2],
Some(horizon_client::mobs::Who::Player),
None,
);
self.container_edge_sound(key, true);
}
} else if &*name == "minecraft:beacon" && self.open_station(pos, &name) {
@@ -17023,6 +17039,9 @@ impl Game<'_> {
}
/// A mouse button while playing (window or control socket).
fn play_mouse(&mut self, button: MouseButton, pressed: bool) -> Result<()> {
if pressed {
self.pvp_click(button);
}
if self.sleeping.is_some() {
return Ok(());
}
@@ -230,6 +230,9 @@ impl adv::World for View<'_, '_> {
"block" => items.block_in_tag(tag, id),
"entity_type" => self.0.res.mob_data().entity_has(tag, id),
"worldgen/biome" => self.0.res.mob_data().biome_has(tag, id),
"worldgen/structure" => vanilla_structure_tags(&self.0.options.resources)
.get(tag)
.is_some_and(|t| t.contains(id)),
"damage_type" => self
.0
.health_resources
@@ -240,6 +243,21 @@ impl adv::World for View<'_, '_> {
}
}
}
type Tags = std::collections::HashMap<String, std::collections::HashSet<String>>;
/// The game's own `worldgen/structure` tags (a world without data packs has
/// no `WorldTables` to hold them), loaded once for the resource root first
/// asked for, as `StructureSpawns::shared`.
fn vanilla_structure_tags(root: &std::path::Path) -> &'static Tags {
static TAGS: std::sync::OnceLock<Tags> = std::sync::OnceLock::new();
TAGS.get_or_init(|| {
horizon_client::items::load_tags(
root,
"worldgen/structure",
&horizon_world::loot::PackOverlay::NONE,
)
.unwrap_or_default()
})
}
fn now_text() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -2728,3 +2746,96 @@ mod tests {
);
}
}
#[cfg(test)]
mod structure_tag_tests {
use super::View;
use crate::game::{Audio, Game, Options, Progress, Settings, load_resources};
use horizon_client::advancements::{World, id_matches};
use serde_json::json;
/// `LocationPredicate.structures` names a `#tag`: the game's own
/// `worldgen/structure` tags, and with a world's data packs theirs over
/// the game's.
#[test]
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
fn location_structure_tags_hold_their_structures() {
let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
let settings = Settings::default();
let loaded = load_resources(
root.clone(),
None,
&root,
&settings,
std::time::Instant::now(),
false,
true,
&Progress::new(),
&mut |_, _| unreachable!(),
)
.unwrap();
let mut options = Options::parse(std::iter::empty()).unwrap().unwrap();
options.resources = root.clone();
options.execution_role = crate::game::execution_role::ExecutionRole::IntegratedServer;
let mut game = Game::new(
&loaded.resources,
loaded.pack,
loaded.gui.unwrap(),
options,
settings,
Audio::silent(&root),
None,
)
.unwrap();
let set = |s: &str| json!(s);
let holds = |game: &Game, tag: &str, id: &str| {
id_matches(&set(tag), "worldgen/structure", id, &View(game))
};
// A world without packs: the game's own tags.
assert!(game.world_tables.is_none());
assert!(holds(
&game,
"#minecraft:village",
"minecraft:village_taiga"
));
assert!(holds(&game, "#shipwreck", "minecraft:shipwreck_beached"));
assert!(!holds(&game, "#minecraft:village", "minecraft:shipwreck"));
assert!(!View(&game).in_tag("worldgen/structure", "mypack:village", "minecraft:igloo"));
// A pack's own tag (nesting the game's) and an addition to the game's.
let dir =
std::env::temp_dir().join(format!("horizon-structure-tags-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let pack = dir.join("datapacks").join("mypack");
let tags = pack.join("data/mypack/tags/worldgen/structure");
let game_tags = pack.join("data/minecraft/tags/worldgen/structure");
std::fs::create_dir_all(&tags).unwrap();
std::fs::create_dir_all(&game_tags).unwrap();
std::fs::write(pack.join("pack.mcmeta"), "{}").unwrap();
std::fs::write(
tags.join("village.json"),
r##"{"values": ["#minecraft:village", "minecraft:igloo"]}"##,
)
.unwrap();
std::fs::write(
game_tags.join("shipwreck.json"),
r#"{"values": ["minecraft:ocean_ruin_cold"]}"#,
)
.unwrap();
let settings = json!({"enabled": ["file/mypack"], "disabled": []});
std::fs::write(dir.join("datapacks.json"), settings.to_string()).unwrap();
game.world_root = dir.clone();
let (tables, _) = game.build_world_tables(false, &[]).unwrap();
game.world_tables = tables;
assert!(game.world_tables.is_some());
assert!(holds(&game, "#mypack:village", "minecraft:igloo"));
assert!(holds(&game, "#mypack:village", "minecraft:village_plains"));
assert!(!holds(&game, "#mypack:village", "minecraft:shipwreck"));
assert!(holds(
&game,
"#minecraft:shipwreck",
"minecraft:ocean_ruin_cold"
));
assert!(holds(&game, "#minecraft:shipwreck", "minecraft:shipwreck"));
let _ = std::fs::remove_dir_all(&dir);
}
}
+14 -1
View File
@@ -496,10 +496,18 @@ impl Game<'_> {
if &*stack.item != "minecraft:armor_stand" {
return false;
}
let Some((pos, stand)) = self.armor_stand_spawn(hit, stack) else {
let Some((pos, mut stand)) = self.armor_stand_spawn(hit, stack) else {
return false;
};
// EntityType.create(.., tryMoveDown, movedUp): the new stand falls onto what is under it,
// up to two blocks (a bottom slab, the block under replaced grass).
let drop = self.drop_offset_from(0.5, 1.975, (pos[0], pos[1], pos[2]), true);
stand.position.y = f64::from(pos[1]) + drop;
stand.previous = stand.position;
let foot = stand.position;
// EntityType.create's spawn yaw, `level.getRandom().nextFloat() * 360`, before
// ArmorStandItem.useOn turns the stand to face the player.
self.world.random.next_float();
self.next_entity += 1;
self.audio.play(
"minecraft:entity.armor_stand.place",
@@ -507,11 +515,16 @@ impl Game<'_> {
0.75,
0.8,
);
// ArmorStandItem.useOn: the stand's ENTITY_PLACE, by the player.
let ctx = self.player_context(None);
self.post_game_event("minecraft:entity_place", [foot.x, foot.y, foot.z], ctx);
self.world.modified.insert((pos[0] >> 4, pos[2] >> 4));
self.armor_stands.push(stand);
if !self.creative {
self.consume_held();
}
// ItemStack.useOn counts the use.
self.award_stat("used", &stack.item, 1);
// ArmorStandItem.useOn returns SUCCESS: the hand swings.
self.swing_arm();
true
+88 -12
View File
@@ -1106,7 +1106,7 @@ impl Game<'_> {
"seed" => {
let seed = self.level.seed.to_string();
let value = Text::copyable(seed);
self.system_message(Text::translate("commands.seed.success", &[value]));
self.feedback_unbroadcast(Text::translate("commands.seed.success", &[value]));
Ok(self.level.seed as i32)
}
"spawnpoint" => self.cmd_spawnpoint(ctx),
@@ -1205,7 +1205,7 @@ impl Game<'_> {
let usage = d.smart_usage(0, level);
let n = usage.len() as i32;
for (_, line) in usage {
self.system_message(Text::literal(format!("/{line}")));
self.feedback_unbroadcast(Text::literal(format!("/{line}")));
}
Ok(n)
}
@@ -1218,10 +1218,10 @@ impl Game<'_> {
let usage = d.smart_usage(last, level);
let prefix = &command[..parse.context.range.1.min(command.len())];
for (_, line) in &usage {
self.system_message(Text::literal(format!("/{prefix} {line}")));
self.feedback_unbroadcast(Text::literal(format!("/{prefix} {line}")));
}
if usage.is_empty() {
self.system_message(Text::literal(format!("/{prefix}")));
self.feedback_unbroadcast(Text::literal(format!("/{prefix}")));
}
Ok(usage.len().max(1) as i32)
}
@@ -2051,7 +2051,7 @@ impl Game<'_> {
fn cmd_time(&mut self, id: &str, ctx: &Context) -> R<i32> {
if id == "time.query.gametime" {
let t = self.world.game_time;
self.system_message(Text::tr("commands.time.query.gametime", &[&t]));
self.feedback_unbroadcast(Text::tr("commands.time.query.gametime", &[&t]));
return Ok((t % i64::from(i32::MAX)) as i32);
}
// `time of <clock>` names the clock; otherwise the dimension's default clock.
@@ -2167,7 +2167,10 @@ impl Game<'_> {
Ok(1)
}
"time.query.time" => {
self.system_message(Text::tr("commands.time.query.absolute", &[&clock, &total]));
self.feedback_unbroadcast(Text::tr(
"commands.time.query.absolute",
&[&clock, &total],
));
Ok((total % i64::from(i32::MAX)) as i32)
}
"time.query.timeline" | "time.query.repetition" => {
@@ -2185,14 +2188,14 @@ impl Game<'_> {
};
if id == "time.query.timeline" {
let ticks = period.map_or(total, |p| total.rem_euclid(p));
self.system_message(Text::tr(
self.feedback_unbroadcast(Text::tr(
"commands.time.query.timeline",
&[&timeline, &ticks],
));
Ok(ticks as i32)
} else {
let reps = period.map_or(0, |p| total / p);
self.system_message(Text::tr(
self.feedback_unbroadcast(Text::tr(
"commands.time.query.timeline.repetitions",
&[&timeline, &reps],
));
@@ -3245,7 +3248,7 @@ impl Game<'_> {
}
})
.unwrap_or(0);
self.system_message(Text::translate(
self.feedback_unbroadcast(Text::translate(
&format!("commands.experience.query.{unit}"),
&[self.target_name(target), Text::literal(value.to_string())],
));
@@ -3334,7 +3337,10 @@ impl Game<'_> {
Some(Arg::Bool(b)) => Value::Bool(*b),
Some(Arg::Int(i)) => Value::Int(*i),
_ => {
self.system_message(Text::tr("commands.gamerule.query", &[&rule.name, &current]));
self.feedback_unbroadcast(Text::tr(
"commands.gamerule.query",
&[&rule.name, &current],
));
return Ok(match current {
Value::Bool(b) => i32::from(b),
Value::Int(i) => i,
@@ -3360,7 +3366,7 @@ impl Game<'_> {
};
let Some(i) = DIFFICULTIES.iter().position(|d| ctx.has(d)) else {
let current = self.ui.difficulty;
self.system_message(Text::translate(
self.feedback_unbroadcast(Text::translate(
"commands.difficulty.query",
&[name(current)],
));
@@ -3978,7 +3984,7 @@ impl Game<'_> {
}
}
let count = players.len();
self.system_message(Text::translate(
self.feedback_unbroadcast(Text::translate(
"commands.list.players",
&[
Text::literal(count.to_string()),
@@ -4529,3 +4535,73 @@ mod selector_bounds_tests {
}
}
}
#[cfg(test)]
mod query_feedback_tests {
#[test]
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
fn a_silent_source_drops_query_results_but_still_stores_them() {
use crate::game::{Audio, Game, Options, Progress, Settings, load_resources};
let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
let settings = Settings::default();
let loaded = load_resources(
root.clone(),
None,
&root,
&settings,
std::time::Instant::now(),
false,
true,
&Progress::new(),
&mut |_, _| unreachable!(),
)
.unwrap();
let mut options = Options::parse(std::iter::empty()).unwrap().unwrap();
options.resources = root.clone();
options.execution_role = crate::game::execution_role::ExecutionRole::IntegratedServer;
let mut game = Game::new(
&loaded.resources,
loaded.pack,
loaded.gui.unwrap(),
options,
settings,
Audio::silent(&root),
None,
)
.unwrap();
game.build_registries();
game.set_allow_commands(true);
assert!(game.run_command("time set 1234"));
assert!(game.run_command("scoreboard objectives add q dummy"));
let queries = [
"time query time",
"time query day",
"time query day repetition",
"time query gametime",
"seed",
"gamerule keep_inventory",
"difficulty",
"list",
"help seed",
];
// A function's source (silent, like `run_command_quietly`) is told nothing.
for command in queries {
game.chat_capture = Some(Vec::new());
assert!(game.run_command_quietly(command), "{command}");
let lines = game.chat_capture.take().unwrap();
assert!(lines.is_empty(), "{command}: {:?}", lines[0].plain());
}
// The player's own source still sees each result.
for command in queries {
game.chat_capture = Some(Vec::new());
assert!(game.run_command(command), "{command}");
assert_eq!(game.chat_capture.take().unwrap().len(), 1, "{command}");
}
// The result still reaches `execute store`.
assert!(game.run_command_quietly("execute store result score #t q run time query day"));
game.chat_capture = Some(Vec::new());
assert!(game.run_command("scoreboard players get #t q"));
let lines = game.chat_capture.take().unwrap();
assert!(lines[0].plain().contains("1234"), "{:?}", lines[0].plain());
}
}
+4 -2
View File
@@ -55,7 +55,7 @@ const HELP: &[&str] = &[
"chat TEXT send a chat line or /command as if typed",
"type TEXT type into the open chat box (key enter tap sends)",
"chat-log [N] the newest N chat messages (default 10)",
"options [PAGE] open the options (video sound controls keybinds mouse chat accessibility language font packs mods graphics graphics-advanced graphics-hdr)",
"options [PAGE] open the options (video sound controls keybinds mouse chat accessibility language font packs mods graphics graphics-advanced graphics-hdr video-vanilla)",
"stats [reset] [raw] frame-time summary since the last reset (raw: every frame time too)",
"narration the narrator's status and the last messages it was asked to say",
"bench SECONDS reset stats, wait, then reply with the summary",
@@ -984,7 +984,9 @@ impl Game<'_> {
use horizon_client::options_ui::Page;
let page = match words.get(1).copied().unwrap_or("main") {
"main" => Page::Main,
"video" => Page::Video,
// What Video Settings... opens; vanilla's own layout, which no menu opens.
"video" => Page::Graphics,
"video-vanilla" => Page::Video,
"sound" => Page::Sound,
"controls" => Page::Controls,
"keybinds" => Page::KeyBinds,
+119 -9
View File
@@ -302,6 +302,26 @@ fn covered(m: &Mob, slot: Slot) -> bool {
}
}
/// How high the tallest item drawn on a body gets above the neck (pixels, `Item::top`): what
/// lifts the wearer's name tag. None when nothing is drawn on it: the wearer's own cosmetics hidden
/// (`worn` is None), another player's at "name tags only" or hidden, or every item `covered` (a
/// helmet over a hat). One max over at most five items, read when the item was parsed.
fn body_top(
extras: &horizon_client::extras::Extras,
own: bool,
worn: Option<&horizon_cosmetics::Worn>,
covered: impl Fn(Slot) -> bool,
) -> Option<f32> {
if !own && extras.effective_others() != Others::Shown {
return None;
}
worn?
.iter()
.filter(|i| !covered(i.slot))
.map(|i| i.top)
.reduce(f32::max)
}
impl Game<'_> {
/// Once a frame, before drawing: starts the service's client on first use and puts the
/// textures the last frame wanted into the atlas.
@@ -385,9 +405,9 @@ impl Game<'_> {
self.cosmetics.local
}
/// What goes by `m`'s name: Horizon's mark if they play Horizon (and the mark is on), and the
/// name tag style they wear if cosmetics are shown for them (name tags only counts). One
/// look in the cache for both.
/// What goes by `m`'s name: Horizon's mark if they play Horizon (and the mark is on), the
/// name tag style they wear if cosmetics are shown for them (name tags only counts), and how
/// high what is drawn on their body gets (`body_top`). One look in the cache for all three.
pub(super) fn cosmetics_marks(&self, m: &Mob, own: bool) -> super::names::Marks {
let mut marks = super::names::Marks::default();
let extras = &self.settings.extras;
@@ -407,6 +427,9 @@ impl Game<'_> {
marks.horizon = extras.horizon_marks && frame.horizon(uuid);
let worn = if styles { frame.worn(uuid) } else { None };
drop(frame);
if !horizon_client::mobs::trader::is_invisible(m) {
marks.top = body_top(extras, own, worn.as_ref(), |slot| covered(m, slot));
}
marks.style = worn.and_then(|w| self.cosmetics_tag(&w));
marks
}
@@ -600,9 +623,26 @@ mod tests {
"texture": {"png": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="},
"tag": {"line": "#a98fff"}}"##;
/// The service: every player asked about wears `TAG`; each request is logged.
#[derive(Clone, Default)]
struct Service(Arc<Mutex<Vec<String>>>);
/// A crown whose star tops out 20 pixels above the neck: a 2-pixel box on a head item
/// attached 18 pixels up (a 16×16 texture).
const CROWN: &str = r##"{"format": 1, "id": "crown", "slot": "head", "bone": "head",
"attach": {"offset": [0, -18, 0]},
"texture": {"png": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGklEQVR4nGM4scXmPyWYYdSAUQNGDRguBgAA+D23H0jXFFsAAAAASUVORK5CYII="},
"parts": [{"name": "star", "cubes": [{"origin": [-1, -2, -1], "size": [2, 2, 2], "uv": [0, 0]}]}]}"##;
/// The service: every player asked about wears its items (`TAG` by default); each request is
/// logged.
#[derive(Clone)]
struct Service(
Arc<Mutex<Vec<String>>>,
&'static [(&'static str, &'static str)],
);
impl Default for Service {
fn default() -> Self {
Service(Arc::default(), &[("tag", TAG)])
}
}
impl horizon_cosmetics::Transport for Service {
fn post(&self, path: &str, body: &[u8]) -> Result<Vec<u8>, String> {
@@ -614,7 +654,10 @@ mod tests {
.iter()
.map(|p| {
let p = p.as_str().unwrap().to_owned();
(p, serde_json::json!([{"id": "tag", "hash": "h"}]))
let items: Vec<_> = (self.1.iter())
.map(|(id, _)| serde_json::json!({"id": id, "hash": "h"}))
.collect();
(p, serde_json::Value::from(items))
})
.collect();
Ok(serde_json::json!({"ttl": 60, "players": players})
@@ -624,7 +667,10 @@ mod tests {
fn get(&self, path: &str) -> Result<Vec<u8>, String> {
self.0.lock().unwrap().push(format!("GET {path}"));
Ok(TAG.as_bytes().to_vec())
let (_, file) = (self.1.iter())
.find(|(id, _)| path.starts_with(&format!("/v1/items/{id}?")))
.ok_or_else(|| format!("no item at {path}"))?;
Ok(file.as_bytes().to_vec())
}
}
@@ -636,7 +682,14 @@ mod tests {
extras: &Extras,
patience: Duration,
) -> (Option<horizon_cosmetics::Worn>, Vec<String>) {
let service = Service::default();
other_player_from(Service::default(), extras, patience)
}
fn other_player_from(
service: Service,
extras: &Extras,
patience: Duration,
) -> (Option<horizon_cosmetics::Worn>, Vec<String>) {
let client = horizon_cosmetics::Cosmetics::new(service.clone(), None);
let deadline = Instant::now() + patience;
let worn = loop {
@@ -730,4 +783,61 @@ mod tests {
// An offline account in single player: nothing to ask about.
assert_eq!(own_uuid(Some(OFFLINE), true, true, || None), None);
}
#[test]
fn a_tall_cosmetic_lifts_the_name_tag_only_while_it_is_drawn() {
use super::super::names::{NAME_TAG_UNIT, cosmetic_lift};
use horizon_cosmetics::prefs::Others;
// Vanilla: EntityAttachment.NAME_TAG of a standing player, 1.8 + 0.5 above the feet;
// the tag's lowest pixel 10 text pixels under it.
const VANILLA: f64 = 2.3;
let unit = f64::from(NAME_TAG_UNIT);
let tag = |top: Option<f32>| VANILLA + top.map_or(0.0, |t| cosmetic_lift(t, 1.0));
// The star's top: 20 pixels above the neck, which is 1.501 blocks up the model, scaled
// by the avatar's 0.9375.
let star = 0.9375 * (1.501 + 20.0 / 16.0);
let both: &'static [(&str, &str)] = &[("tag", TAG), ("crown", CROWN)];
let shown = Extras {
cosmetics_others: Some(Others::Shown),
..Extras::default()
};
let (short, long) = (Duration::from_millis(300), Duration::from_secs(10));
let top_of = |extras: &Extras, patience| {
let (worn, _) = other_player_from(Service(Arc::default(), both), extras, patience);
body_top(extras, false, worn.as_ref(), |_| false)
};
// No cosmetics drawn on the body (a name tag style only): vanilla's height exactly.
let (worn, _) = other_player(&shown, long);
assert!(worn.is_some());
assert_eq!(
tag(body_top(&shown, false, worn.as_ref(), |_| false)),
VANILLA
);
// A small hat under the tag: vanilla's height too.
assert_eq!(cosmetic_lift(10.0, 1.0), 0.0);
// The crown: the whole tag just over the star.
let crowned = tag(top_of(&shown, long));
let bottom = crowned - 10.0 * unit;
assert!(
bottom > star && bottom < star + 2.0 * unit,
"{bottom} over {star}"
);
// A helmet over it: vanilla's height.
let (worn, _) = other_player_from(Service(Arc::default(), both), &shown, long);
let helmet = body_top(&shown, false, worn.as_ref(), |slot| slot == Slot::Head);
assert_eq!(tag(helmet), VANILLA);
// Others' cosmetics on name tags only, or hidden: nothing on the body, vanilla's height.
for others in [Others::NameTags, Others::Hidden] {
let extras = Extras {
cosmetics_others: Some(others),
..Extras::default()
};
let patience = if others == Others::Hidden {
short
} else {
long
};
assert_eq!(tag(top_of(&extras, patience)), VANILLA, "{others:?}");
}
}
}
+31 -3
View File
@@ -422,6 +422,12 @@ impl Game<'_> {
/// persistent: the despawn rule holds it like any other mob, and only
/// its NBT can make it persistent.
pub(super) fn spawn_mob_spawned(&mut self, kind: &str, at: [f64; 3]) -> Option<u32> {
// Ender Dragons have their own entity implementation rather than a
// `Mob` entry. Spawn eggs still go through `SpawnEggItem.spawnMob`,
// so route this special entity through the same spawned-entity path.
if matches!(kind, "ender_dragon" | "minecraft:ender_dragon") {
return self.spawn_dragon_spawned(at);
}
// The spawn is not a natural one (no jockeys, the patrol leader roll
// stays), but the mob is not persistent.
let id = self.spawn_mob_for(kind, at, true, false)?;
@@ -2660,6 +2666,14 @@ impl Game<'_> {
// `Level.setBlockAndUpdate`: neighbour and shape updates, so a crop
// above trampled farmland breaks and drops.
MobEvent::SetBlock { pos, state } => self.level_set(pos, state)?,
MobEvent::EndermanPlaceBlock {
mob,
pos,
carried,
occupants,
} => {
self.enderman_place_block(mob, pos, carried, &occupants)?;
}
MobEvent::CropRaided {
pos,
state,
@@ -2994,9 +3008,12 @@ impl Game<'_> {
horizon_client::mobs::happy_ghast::notify_leash_holder(m);
}
}
MobEvent::Vibration { from, to, ticks } => {
self.mob_vibration_particle(from, to, ticks)
}
MobEvent::Vibration {
from,
to,
ticks,
source,
} => self.mob_vibration_particle(source, from, to, ticks),
MobEvent::DigParticles { at } => self.warden_dig_particles(at),
MobEvent::BlockBreakEffect { pos, block } => {
// Turtle.aiStep broadcasts LevelEvent 2001, not the
@@ -5537,7 +5554,13 @@ impl Game<'_> {
return Ok(false);
}
}
// PlayerInteractTrigger: the name tag as it was before it named the mob.
let used = self.held().cloned();
if self.name_mob(index) {
if let Some(stack) = &used {
let target = self.mob_entity(&self.mobs[index]);
self.trigger_item_entity("minecraft:player_interacted_with_entity", stack, &target);
}
return Ok(true);
}
if self
@@ -5672,6 +5695,11 @@ impl Game<'_> {
if !self.mobs[index].kind.food.contains(&item)
&& self.interact_sheep_dye(index, self.hand, false)
{
// PlayerInteractTrigger: the dye as it was before it coloured the sheep.
if let Some(stack) = &held {
let target = self.mob_entity(&self.mobs[index]);
self.trigger_item_entity("minecraft:player_interacted_with_entity", stack, &target);
}
self.swing_arm();
return Ok(true);
}
@@ -156,12 +156,16 @@ impl Game<'_> {
self.tick_chunks.clear();
self.sync_tick_chunks();
self.mobs.clear();
self.dragon = None;
self.dropped.clear();
self.creatures.projectiles.clear();
self.tnts.clear();
self.boats.clear();
self.carts.clear();
self.armor_stands.clear();
self.crystals.clear();
self.paintings.clear();
self.item_frames.clear();
self.rockets.clear();
self.potions.clear_thrown();
self.level_events.clear();
@@ -1146,6 +1150,15 @@ impl Game<'_> {
let (p, v) = (m.position, m.velocity);
out.push(line(&m.kind.id, [p.x, p.y, p.z], [v.x, v.y, v.z], ""));
}
if let Some(d) = &self.dragon {
let p = d.position;
out.push(line(
"minecraft:ender_dragon",
[p.x, p.y, p.z],
[0.0; 3],
"",
));
}
for s in self
.armor_stands
.iter()
@@ -1226,5 +1239,23 @@ impl Game<'_> {
let (p, v) = (c.position, c.velocity);
out.push(line(c.kind.id(), [p.x, p.y, p.z], [v.x, v.y, v.z], ""));
}
for c in &self.crystals {
let p = c.position;
out.push(line("minecraft:end_crystal", [p.x, p.y, p.z], [0.0; 3], ""));
}
// A hanging entity's position is its box's centre (HangingEntity.recalculateBoundingBox).
let centre =
|(lo, hi): ([f64; 3], [f64; 3])| std::array::from_fn(|i| (lo[i] + hi[i]) / 2.0);
for p in &self.paintings {
out.push(line("minecraft:painting", centre(p.bounds()), [0.0; 3], ""));
}
for f in &self.item_frames {
let kind = if f.glow {
"minecraft:glow_item_frame"
} else {
"minecraft:item_frame"
};
out.push(line(kind, centre(f.bounds()), [0.0; 3], ""));
}
}
}
+35 -5
View File
@@ -767,10 +767,16 @@ impl Game<'_> {
}
out
}
/// `EntityType.getYOffset(level, pos, false, box)`: the entity box set on
/// `EntityType.getYOffset(level, pos, false, box)`: [`Self::drop_offset_from`] without
/// `movedUp`.
pub(super) fn drop_offset(&self, width: f64, height: f64, p: Pos) -> f64 {
self.drop_offset_from(width, height, p, false)
}
/// `EntityType.getYOffset(level, pos, movedUp, box)`: the entity box set on
/// top of the block (`setPos(x + 0.5, y + 1, z + 0.5)`) falls one block
/// against the collision shapes overlapping that block alone.
fn drop_offset(&self, width: f64, height: f64, p: Pos) -> f64 {
/// against the collision shapes overlapping that block, or, `moved_up`, two
/// blocks against those of the block and the one under it.
pub(super) fn drop_offset_from(&self, width: f64, height: f64, p: Pos, moved_up: bool) -> f64 {
use horizon_world::{
block_collision_scan::BlockCollisionScan, collision_context::CollisionContext,
};
@@ -782,7 +788,11 @@ impl Game<'_> {
Vec3::new(x + h, y + height, z + h),
);
let cell = Aabb::new(
Vec3::new(f64::from(p.0), f64::from(p.1), f64::from(p.2)),
Vec3::new(
f64::from(p.0),
f64::from(p.1) - if moved_up { 1.0 } else { 0.0 },
f64::from(p.2),
),
Vec3::new(
f64::from(p.0) + 1.0,
f64::from(p.1) + 1.0,
@@ -794,7 +804,7 @@ impl Game<'_> {
binding: self.collision,
subject: None,
};
let mut amount = -1.0;
let mut amount = if moved_up { -2.0 } else { -1.0 };
// Level.getCollisions(null, aabb): CollisionContext.empty().
if let Ok(scan) = BlockCollisionScan::new(
&mut scene,
@@ -818,6 +828,23 @@ impl Game<'_> {
} else {
format!("minecraft:{kind}")
};
// Ender Dragons are simulated separately from `Mob` and therefore do
// not have a mob-type registry entry for the generic dispenser path.
if name == "minecraft:ender_dragon" {
// `EntityType.getYOffset` uses the created entity's full 16 by 8
// bounding box when the dispenser asks to move it down.
let y_off = if try_move_down {
self.drop_offset(16.0, 8.0, p)
} else {
0.0
};
let at = [
f64::from(p.0) + 0.5,
f64::from(p.1) + y_off,
f64::from(p.2) + 0.5,
];
return self.spawn_dragon_spawned(at);
}
let t = self.res.mob_types().get(&name)?.clone();
let y_off = if try_move_down {
self.drop_offset(t.width, t.height, p)
@@ -956,6 +983,9 @@ impl Game<'_> {
}
/// After `EntityType.spawn` / `MobBucketItem.spawn`: `mob.playAmbientSound()`.
fn mob_ambient(&mut self, id: u32) -> Result<()> {
if self.dragon_spawn_ambient(id) {
return Ok(());
}
let Some(i) = self.mobs.iter().position(|m| m.id == id) else {
return Ok(());
};
Loaded 100 of 278 files, more files were not shown because too many files have changed in this diff. Show more