mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #3203 from senjinthedragon/hdr-screen
HDR: its settings get a screen of their own
This commit is contained in:
9 files changed
+151
-36
No files matched your search
@@ -1182,6 +1182,7 @@ impl Renderer {
|
||||
}
|
||||
surface.configure(&device, &config);
|
||||
let hdr_output = hdr::output(&caps);
|
||||
crate::graphics::offer_hdr(hdr_output.is_some());
|
||||
if previous.is_none() {
|
||||
log::info!(
|
||||
"Horizon renderer: HDR output {}",
|
||||
|
||||
@@ -285,6 +285,16 @@ pub fn reduced_far_detail_suggested() -> bool {
|
||||
REDUCED_SUGGESTED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
static HDR_OFFERED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
/// Whether the display path offers HDR output (HDR10 or scRGB): set when the renderer
|
||||
/// opens its surface; the settings open HDR's screen only then.
|
||||
pub fn offer_hdr(on: bool) {
|
||||
HDR_OFFERED.store(on, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
pub fn hdr_offered() -> bool {
|
||||
HDR_OFFERED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The farthest LOD Distance, in chunks, on integrated GPUs other than
|
||||
/// Apple's: they share the CPU's memory bandwidth, and past it far terrain
|
||||
/// costs them more frame time than it is worth.
|
||||
|
||||
@@ -130,8 +130,9 @@ impl Style {
|
||||
label: &Text,
|
||||
value: f32,
|
||||
hover: bool,
|
||||
active: bool,
|
||||
) {
|
||||
ui::slider(batch, atlas, x, y, w, label, value, hover, true);
|
||||
ui::slider(batch, atlas, x, y, w, label, value, hover && active, active);
|
||||
}
|
||||
/// The cost tag and the description beside a control, wrapped to two lines.
|
||||
pub fn description(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Horizon's graphics settings screens, Simple and Advanced: which settings
|
||||
//! Horizon's graphics settings screens, Simple, Advanced and HDR: which settings
|
||||
//! they show, in what groups, with what description and cost, and their
|
||||
//! input. How they look comes from `graphics_style`.
|
||||
use crate::{
|
||||
@@ -14,6 +14,8 @@ use crate::{
|
||||
pub enum Level {
|
||||
Simple,
|
||||
Advanced,
|
||||
/// HDR output's own settings, opened from Advanced while HDR is on.
|
||||
Hdr,
|
||||
}
|
||||
|
||||
/// Horizon's own settings (`graphics::Graphics`).
|
||||
@@ -38,6 +40,8 @@ pub enum H {
|
||||
Antialiasing,
|
||||
Upscaling,
|
||||
Hdr,
|
||||
/// Opens `Level::Hdr`.
|
||||
HdrSettings,
|
||||
HdrBlackLevel,
|
||||
HdrPeak,
|
||||
HdrPaperWhite,
|
||||
@@ -122,6 +126,34 @@ pub fn rows(level: Level) -> Vec<Row> {
|
||||
);
|
||||
let mut rows = match level {
|
||||
Level::Simple => vec![preset, render, lod, far, fov, fps, vsync, gui],
|
||||
Level::Hdr => vec![
|
||||
Row::Header("HDR"),
|
||||
h(
|
||||
H::Hdr,
|
||||
None,
|
||||
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.",
|
||||
),
|
||||
h(
|
||||
H::HdrBlackLevel,
|
||||
None,
|
||||
"Your display's darkest black. 0 for OLED; LCDs glow a little.",
|
||||
),
|
||||
h(
|
||||
H::HdrPeak,
|
||||
None,
|
||||
"How bright the brightest highlights get. Your display's peak.",
|
||||
),
|
||||
h(
|
||||
H::HdrPaperWhite,
|
||||
None,
|
||||
"How bright white paper and menus look. Match your desktop.",
|
||||
),
|
||||
h(
|
||||
H::HdrUiBrightness,
|
||||
None,
|
||||
"How bright the HUD and menus are, against paper white.",
|
||||
),
|
||||
],
|
||||
Level::Advanced => vec![
|
||||
Row::Header("Rendering"),
|
||||
render,
|
||||
@@ -232,30 +264,10 @@ pub fn rows(level: Level) -> Vec<Row> {
|
||||
"Renders the world smaller and rebuilds it with TAA. Faster, softer.",
|
||||
),
|
||||
h(
|
||||
H::Hdr,
|
||||
H::HdrSettings,
|
||||
None,
|
||||
"HDR output on an HDR display: lava, fire and sunlight shine brighter.",
|
||||
),
|
||||
h(
|
||||
H::HdrBlackLevel,
|
||||
None,
|
||||
"HDR: your display's darkest black. 0 for OLED; LCDs glow a little.",
|
||||
),
|
||||
h(
|
||||
H::HdrPeak,
|
||||
None,
|
||||
"HDR: how bright the brightest highlights get. Your display's peak.",
|
||||
),
|
||||
h(
|
||||
H::HdrPaperWhite,
|
||||
None,
|
||||
"HDR: how bright white paper and menus look. Match your desktop.",
|
||||
),
|
||||
h(
|
||||
H::HdrUiBrightness,
|
||||
None,
|
||||
"HDR: how bright the HUD and menus are, against paper white.",
|
||||
),
|
||||
h(
|
||||
H::PresentMode,
|
||||
None,
|
||||
@@ -417,6 +429,7 @@ impl H {
|
||||
),
|
||||
H::Upscaling => labelled("Upscaling", g.upscaling.label()),
|
||||
H::Hdr => labelled("HDR", on_off(g.hdr)),
|
||||
H::HdrSettings => Text::literal("HDR Settings..."),
|
||||
H::HdrPaperWhite => labelled("Paper White", format!("{} nits", g.hdr_paper_white)),
|
||||
H::HdrPeak => labelled("Peak Brightness", format!("{} nits", g.hdr_peak)),
|
||||
H::HdrBlackLevel => labelled(
|
||||
@@ -525,6 +538,25 @@ impl Item {
|
||||
fn available(self) -> bool {
|
||||
!matches!(self, Item::Vanilla(o) if !o.available())
|
||||
}
|
||||
/// Why a control can't be used now, when it can't: HDR's screen opens only where
|
||||
/// the display offers HDR output, whatever `horizon-graphics.txt` says (a copied game
|
||||
/// folder, a display swapped for an SDR one: the renderer leaves HDR off there, and
|
||||
/// the saved values wait for an HDR display), and its sliders do something only
|
||||
/// while it is on.
|
||||
fn blocked(self, s: &Settings) -> Option<&'static str> {
|
||||
if !self.available() {
|
||||
return Some("This feature is not yet available in Horizon.");
|
||||
}
|
||||
match self {
|
||||
Item::Horizon(H::HdrSettings | H::Hdr) if !crate::graphics::hdr_offered() => {
|
||||
Some("Your display or system doesn't offer HDR output.")
|
||||
}
|
||||
Item::Horizon(
|
||||
H::HdrBlackLevel | H::HdrPeak | H::HdrPaperWhite | H::HdrUiBrightness,
|
||||
) if !s.horizon.hdr => Some("Turn HDR on to use it."),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// On/off settings and their value.
|
||||
pub fn switch(self, s: &Settings) -> Option<bool> {
|
||||
let g = &s.horizon;
|
||||
@@ -626,6 +658,7 @@ pub enum Action {
|
||||
SliderDrag,
|
||||
OpenAdvanced,
|
||||
OpenSimple,
|
||||
OpenHdr,
|
||||
Done,
|
||||
}
|
||||
|
||||
@@ -709,6 +742,17 @@ impl GraphicsUi {
|
||||
let other = match level {
|
||||
Level::Simple => Action::OpenAdvanced,
|
||||
Level::Advanced => Action::OpenSimple,
|
||||
// One Done, back to Advanced.
|
||||
Level::Hdr => {
|
||||
widgets.push(Widget {
|
||||
x: cx - 100.0,
|
||||
y: by,
|
||||
w: 200.0,
|
||||
target: Target::Button(Action::Done),
|
||||
list: false,
|
||||
});
|
||||
return (widgets, headers, content);
|
||||
}
|
||||
};
|
||||
widgets.push(Widget {
|
||||
x: cx - 155.0,
|
||||
@@ -770,12 +814,10 @@ impl GraphicsUi {
|
||||
continue;
|
||||
}
|
||||
let label = item.label(s);
|
||||
let live = item.available();
|
||||
if hover && !live {
|
||||
tooltip = Some(atlas.font.split(
|
||||
&Text::literal("This feature is not yet available in Horizon."),
|
||||
170.0,
|
||||
));
|
||||
let blocked = item.blocked(s);
|
||||
let live = blocked.is_none();
|
||||
if hover && let Some(why) = blocked {
|
||||
tooltip = Some(atlas.font.split(&Text::literal(why), 170.0));
|
||||
}
|
||||
match item.slider(s) {
|
||||
Some(value) => STYLE.slider(
|
||||
@@ -785,6 +827,7 @@ impl GraphicsUi {
|
||||
&label,
|
||||
value,
|
||||
hover || self.dragging == Some(item),
|
||||
live,
|
||||
),
|
||||
None => STYLE.button(
|
||||
batch,
|
||||
@@ -834,6 +877,7 @@ impl GraphicsUi {
|
||||
let title = match level {
|
||||
Level::Simple => "Graphics",
|
||||
Level::Advanced => "Graphics: Advanced",
|
||||
Level::Hdr => "Graphics: HDR",
|
||||
};
|
||||
STYLE.title(batch, atlas, &Text::literal(title));
|
||||
if let Some(lines) = tooltip {
|
||||
@@ -889,7 +933,10 @@ impl GraphicsUi {
|
||||
return match wd.target {
|
||||
Target::Button(action) => vec![Action::ButtonClick, action],
|
||||
// A disabled control does nothing: no sound, no preset change.
|
||||
Target::Item(item) if !item.available() => Vec::new(),
|
||||
Target::Item(item) if item.blocked(s).is_some() => Vec::new(),
|
||||
Target::Item(Item::Horizon(H::HdrSettings)) => {
|
||||
vec![Action::ButtonClick, Action::OpenHdr]
|
||||
}
|
||||
Target::Item(item) => {
|
||||
if item.slider(s).is_some() {
|
||||
self.dragging = Some(item);
|
||||
@@ -921,6 +968,53 @@ impl GraphicsUi {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hdr_settings_open_where_hdr_is_offered() {
|
||||
let mut s = Settings::default();
|
||||
let mut ui = GraphicsUi::default();
|
||||
let size = (480.0, 2000.0);
|
||||
let at = |ui: &mut GraphicsUi, s: &Settings, level, item| {
|
||||
let (widgets, _, _) = ui.layout(level, s, size.0, size.1);
|
||||
let w = widgets
|
||||
.iter()
|
||||
.find(|w| matches!(w.target, Target::Item(i) if i == item))
|
||||
.unwrap();
|
||||
(w.x + 1.0, w.y + 1.0)
|
||||
};
|
||||
let button = at(&mut ui, &s, Level::Advanced, Item::Horizon(H::HdrSettings));
|
||||
let click = |ui: &mut GraphicsUi, s: &mut Settings, level, point| {
|
||||
ui.click(level, s, point, size, [480, 2000], false, false)
|
||||
};
|
||||
crate::graphics::offer_hdr(false);
|
||||
assert!(click(&mut ui, &mut s, Level::Advanced, button).is_empty());
|
||||
// Not even with HDR saved on, from another computer.
|
||||
s.horizon.hdr = true;
|
||||
assert!(click(&mut ui, &mut s, Level::Advanced, button).is_empty());
|
||||
s.horizon.hdr = false;
|
||||
crate::graphics::offer_hdr(true);
|
||||
assert_eq!(
|
||||
click(&mut ui, &mut s, Level::Advanced, button),
|
||||
[Action::ButtonClick, Action::OpenHdr]
|
||||
);
|
||||
// On HDR's screen, the sliders wait for the switch above them.
|
||||
let peak = at(&mut ui, &s, Level::Hdr, Item::Horizon(H::HdrPeak));
|
||||
assert!(click(&mut ui, &mut s, Level::Hdr, peak).is_empty());
|
||||
s.horizon.hdr = true;
|
||||
assert_eq!(
|
||||
click(&mut ui, &mut s, Level::Hdr, peak),
|
||||
[Action::SliderDrag]
|
||||
);
|
||||
let on = |level, item| {
|
||||
extra_rows(level, &s)
|
||||
.iter()
|
||||
.any(|r| matches!(r, Row::Setting(x) if x.item == Item::Horizon(item)))
|
||||
};
|
||||
assert!(on(Level::Hdr, H::Hdr) && !on(Level::Advanced, H::Hdr));
|
||||
assert!(on(Level::Hdr, H::HdrPeak) && !on(Level::Advanced, H::HdrPeak));
|
||||
for item in [H::HdrBlackLevel, H::HdrPaperWhite, H::HdrUiBrightness] {
|
||||
assert!(on(Level::Hdr, item) && !on(Level::Advanced, item));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn graphics_buttons_sound_on_press_and_sliders_on_release() {
|
||||
let mut s = Settings::default();
|
||||
|
||||
@@ -993,6 +993,7 @@ pub enum Page {
|
||||
/// Horizon's graphics settings (`graphics_ui`).
|
||||
Graphics,
|
||||
GraphicsAdvanced,
|
||||
GraphicsHdr,
|
||||
/// Horizon's controller settings and button binds.
|
||||
Controller,
|
||||
PadBinds,
|
||||
@@ -1367,7 +1368,10 @@ impl OptionsUi {
|
||||
if page == Page::Packs {
|
||||
self.pack_screen = None;
|
||||
}
|
||||
if matches!(page, Page::Graphics | Page::GraphicsAdvanced) {
|
||||
if matches!(
|
||||
page,
|
||||
Page::Graphics | Page::GraphicsAdvanced | Page::GraphicsHdr
|
||||
) {
|
||||
self.graphics.reset();
|
||||
}
|
||||
}
|
||||
@@ -1376,6 +1380,7 @@ impl OptionsUi {
|
||||
match page {
|
||||
Page::Graphics => Some(crate::graphics_ui::Level::Simple),
|
||||
Page::GraphicsAdvanced => Some(crate::graphics_ui::Level::Advanced),
|
||||
Page::GraphicsHdr => Some(crate::graphics_ui::Level::Hdr),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1397,7 +1402,7 @@ impl OptionsUi {
|
||||
Page::Mods => "Mods",
|
||||
Page::NativeMod(_) => "Mod Settings",
|
||||
Page::ShaderPacks => "Shader Packs",
|
||||
Page::Graphics | Page::GraphicsAdvanced => "options.videoTitle",
|
||||
Page::Graphics | Page::GraphicsAdvanced | Page::GraphicsHdr => "options.videoTitle",
|
||||
Page::Controller => "Controller Settings",
|
||||
Page::PadBinds => "Controller Binds",
|
||||
Page::Extras => "Horizon Extras",
|
||||
@@ -2656,6 +2661,7 @@ impl OptionsUi {
|
||||
Action::Changed => events.push(Event::Changed),
|
||||
Action::SliderDrag => events.push(Event::SliderDrag),
|
||||
Action::OpenAdvanced => self.push(Page::GraphicsAdvanced),
|
||||
Action::OpenHdr => self.push(Page::GraphicsHdr),
|
||||
Action::OpenSimple => {
|
||||
if self.stack.len() >= 2
|
||||
&& self.stack[self.stack.len() - 2] == Page::Graphics
|
||||
|
||||
@@ -146,6 +146,7 @@ const PAGES: Ty = Ty::Enum(&[
|
||||
"shader-packs",
|
||||
"graphics",
|
||||
"graphics-advanced",
|
||||
"graphics-hdr",
|
||||
"extras",
|
||||
"horizon",
|
||||
"agent",
|
||||
|
||||
@@ -34,7 +34,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)",
|
||||
"options [PAGE] open the options (video sound controls keybinds mouse chat accessibility language font packs mods graphics graphics-advanced graphics-hdr)",
|
||||
"stats [reset] frame-time summary since the last reset",
|
||||
"bench SECONDS reset stats, wait, then reply with the summary",
|
||||
"fly X1 Y1 Z1 X2 Y2 Z2 SECONDS [PITCH [raw]] fly the camera along a line at constant speed, then reply with the frame summary (raw: every frame time too)",
|
||||
@@ -685,6 +685,7 @@ impl Game<'_> {
|
||||
"shader-packs" => Page::ShaderPacks,
|
||||
"graphics" => Page::Graphics,
|
||||
"graphics-advanced" => Page::GraphicsAdvanced,
|
||||
"graphics-hdr" => Page::GraphicsHdr,
|
||||
"extras" => Page::Extras,
|
||||
"credits" => Page::Credits,
|
||||
"horizon" => Page::Horizon,
|
||||
|
||||
+3
-2
@@ -21,7 +21,8 @@ Every row has a one-line description and its cost in frame time (none, low, medi
|
||||
| Quality | smooth lighting, mipmap levels, biome blend, clouds, cloud distance, weather radius, particles, entity shadows, cutout leaves | `options.txt` |
|
||||
| | sky, fog, weather, animated textures | `horizon-graphics.txt` |
|
||||
| Display | FOV, max framerate, VSync, fullscreen, GUI scale | `options.txt` |
|
||||
| | resolution scale, anti-aliasing (FXAA or TAA), TAA upscaling, HDR, paper white, peak brightness, present mode, frame pacing | `horizon-graphics.txt` |
|
||||
| | resolution scale, anti-aliasing (FXAA or TAA), TAA upscaling, present mode, frame pacing | `horizon-graphics.txt` |
|
||||
| HDR (its own screen, from Display) | HDR, black level, peak brightness, paper white, UI brightness | `horizon-graphics.txt` |
|
||||
| Performance | battery mode, worker threads, upload budget per frame | `horizon-graphics.txt` |
|
||||
| Overlay | performance overlay: FPS, frame/CPU/GPU times, frame-time graph | `horizon-graphics.txt` |
|
||||
|
||||
@@ -29,7 +30,7 @@ Every row has a one-line description and its cost in frame time (none, low, medi
|
||||
|
||||
**Battery mode** (Performance, off by default) is for laptops and handhelds: at most 60 frames per second in a world, 30 in menus without a world or while another window has focus, and half the automatic worker threads (chunks load slower). It changes only when frames are drawn: what they show and the game's 20 ticks per second stay as they are.
|
||||
|
||||
**HDR** (Display, off by default) presents to an HDR10 swapchain (`Rgb10a2Unorm`, BT.2020 with the PQ curve) or, where only that is offered, an scRGB one (`Rgba16Float`, extended linear sRGB), on displays and compositors that offer either; `HORIZON_HDR_OUTPUT=pq` or `scrgb` allows only one. The frame is drawn exactly as in SDR, in vanilla's gamma space, and a last pass (`hdr.wgsl`) decodes it to linear light at the **Paper White** and lifts light sources (blocks that emit light, lava, the sun and the moon) towards the **Peak Brightness**; everything else stays as in SDR. The mesher puts the light a block's face emits in its vertex colour's alpha (`EMISSION_BITS`), the frame's alpha carries the mark, and only pixels that still show the world before the hand and HUD are lifted. On Vulkan, `horizon-hdr` sends HDR metadata (MaxCLL, mastering luminance), which wgpu doesn't: without it a compositor may clip everything above SDR white. KWin maps the content's reference white onto the desktop's SDR brightness, so on Linux the default Paper White matches SDR mode. **Black Level** (0 by default, for an OLED's true black) is the display's darkest black, in nits: the last pass lifts the shadows onto it with ITU-R BT.2390's black level lift in PQ, which leaves the peak where it is and fades out by the fourth power above black, and the metadata gives it as the minimum luminance; at 0 the picture is unchanged. **UI Brightness** (100% by default) sets the HUD and menus against paper white: once the world is kept for the last pass, the frame's alpha is zeroed, everything drawn after raises it where it covers the world, the first-person hand's is then scaled down to 1/255 (it stays at the world's brightness), and the last pass scales the rest by UI Brightness and lifts none of it (so text over the sun stays text). `HORIZON_HDR_DEBUG=1` paints lifted pixels magenta. Not lifted yet: glowing particles, distant LOD terrain.
|
||||
**HDR** (off by default) lives on its own screen, Graphics: HDR, behind **HDR Settings...** under Display, which is greyed where the display path offers no HDR output (so players without an HDR display don't take its settings for their display's); there the switch comes first and the sliders below it only while it is on. It presents to an HDR10 swapchain (`Rgb10a2Unorm`, BT.2020 with the PQ curve) or, where only that is offered, an scRGB one (`Rgba16Float`, extended linear sRGB), on displays and compositors that offer either; `HORIZON_HDR_OUTPUT=pq` or `scrgb` allows only one. The frame is drawn exactly as in SDR, in vanilla's gamma space, and a last pass (`hdr.wgsl`) decodes it to linear light at the **Paper White** and lifts light sources (blocks that emit light, lava, the sun and the moon) towards the **Peak Brightness**; everything else stays as in SDR. The mesher puts the light a block's face emits in its vertex colour's alpha (`EMISSION_BITS`), the frame's alpha carries the mark, and only pixels that still show the world before the hand and HUD are lifted. On Vulkan, `horizon-hdr` sends HDR metadata (MaxCLL, mastering luminance), which wgpu doesn't: without it a compositor may clip everything above SDR white. KWin maps the content's reference white onto the desktop's SDR brightness, so on Linux the default Paper White matches SDR mode. **Black Level** (0 by default, for an OLED's true black) is the display's darkest black, in nits: the last pass lifts the shadows onto it with ITU-R BT.2390's black level lift in PQ, which leaves the peak where it is and fades out by the fourth power above black, and the metadata gives it as the minimum luminance; at 0 the picture is unchanged. **UI Brightness** (100% by default) sets the HUD and menus against paper white: once the world is kept for the last pass, the frame's alpha is zeroed, everything drawn after raises it where it covers the world, the first-person hand's is then scaled down to 1/255 (it stays at the world's brightness), and the last pass scales the rest by UI Brightness and lifts none of it (so text over the sun stays text). `HORIZON_HDR_DEBUG=1` paints lifted pixels magenta. Not lifted yet: glowing particles, distant LOD terrain.
|
||||
|
||||
Which settings each screen shows, their descriptions and costs are in `crates/horizon-client-render/src/graphics_ui.rs` (`rows`); `graphics_style.rs` draws them.
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ Every command `horizon-ctl` takes, its MCP tool in `horizon-ctl mcp` (the launch
|
||||
| `respawn` | `respawn` | Respawn from the death screen | none |
|
||||
| `pause` | `pause` | Open the pause menu | none |
|
||||
| `resume` | `resume` | Leave the pause menu | none |
|
||||
| `options [PAGE]` | `options` | Open the options screen, or one of its pages (Horizon's too) | `page` (main, video, sound, controls, keybinds, controller, controller-binds, mouse, chat, accessibility, language, font, packs, mods, shader-packs, graphics, graphics-advanced, extras, horizon, agent): the page (default: the main options) |
|
||||
| `options [PAGE]` | `options` | Open the options screen, or one of its pages (Horizon's too) | `page` (main, video, sound, controls, keybinds, controller, controller-binds, mouse, chat, accessibility, language, font, packs, mods, shader-packs, graphics, graphics-advanced, graphics-hdr, extras, horizon, agent): the page (default: the main options) |
|
||||
| `book [next\|back\|take\|done]` | `book` | The open book or lectern: its page text, or press one of its buttons | `action` (next, back, take, done): a button to press |
|
||||
| `signedit X Y Z [front\|back]` | `signedit` | Open the sign editor of the sign at X Y Z (then type and close) | `x` (integer, required): block X<br>`y` (integer, required): block Y<br>`z` (integer, required): block Z<br>`side` (front, back): which side |
|
||||
| `mount-menu` | `mount_menu` | Open the inventory of the horse, donkey, mule or llama being ridden | none |
|
||||
|
||||
Reference in new issue
Block a user