mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge main and retain raid harness observations
Co-Authored-By: Codex <[email protected]>
This commit is contained in:
commit
de4029c1df
278 files changed
+6719
-980
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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, ¤t]));
|
||||
self.feedback_unbroadcast(Text::tr(
|
||||
"commands.gamerule.query",
|
||||
&[&rule.name, ¤t],
|
||||
));
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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], ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user