diff --git a/crates/window-core/src/authoring/fullscreen.rs b/crates/window-core/src/authoring/fullscreen.rs index 59b9b6f4..dd0a556d 100644 --- a/crates/window-core/src/authoring/fullscreen.rs +++ b/crates/window-core/src/authoring/fullscreen.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use super::element::{ElementDto, collect_handles, convert_children, intern_art}; -use crate::ir::{FullscreenMode, Rgb}; +use crate::ir::{Backdrop, FullscreenMode, Rgb}; use crate::model::{Art, Fullscreen}; use crate::{Error, Result}; @@ -35,14 +35,7 @@ impl FullscreenDto { ))); } }; - let backdrop = self - .backdrop - .map(|color| { - Rgb::parse_hex(&color).ok_or_else(|| { - Error::Validation(format!("{owner} has invalid backdrop `{color}`; expected #rrggbb")) - }) - }) - .transpose()?; + let backdrop = self.backdrop.map(|color| parse_backdrop(&color, &owner)).transpose()?; Ok(Fullscreen { name: self.name, mode, @@ -53,3 +46,28 @@ impl FullscreenDto { }) } } + +/// Parses `#rrggbb`, which hides the world, or `#rrggbbaa`, which dims it. The text shader drops fragments below 10% +/// opacity, so a dimming alpha starts at `1a`. +fn parse_backdrop(color: &str, owner: &str) -> Result { + let invalid = + || Error::Validation(format!("{owner} has invalid backdrop `{color}`; expected #rrggbb or #rrggbbaa")); + if !color.is_ascii() { + return Err(invalid()); + } + let (rgb, alpha) = match color.len() { + 7 => (color, 0xff), + 9 => (&color[..7], u8::from_str_radix(&color[7..], 16).map_err(|_| invalid())?), + _ => return Err(invalid()), + }; + let color_rgb = Rgb::parse_hex(rgb).ok_or_else(invalid)?; + if alpha < MIN_BACKDROP_ALPHA { + return Err(Error::Validation(format!( + "{owner} has backdrop `{color}` below the lowest alpha the text shader draws, `{MIN_BACKDROP_ALPHA:02x}`" + ))); + } + Ok(Backdrop { color: color_rgb, alpha }) +} + +/// The lowest backdrop alpha whose fragments pass the text shader's 0.1 alpha cutoff. +const MIN_BACKDROP_ALPHA: u8 = 0x1a; diff --git a/crates/window-core/src/fullscreen.rs b/crates/window-core/src/fullscreen.rs index 2f52164a..9a321406 100644 --- a/crates/window-core/src/fullscreen.rs +++ b/crates/window-core/src/fullscreen.rs @@ -240,9 +240,9 @@ pub(crate) fn backdrop_texture() -> Texture { /// The include: the contract's constants, the color table `colors`, the control rects `controls` in canvas pixels, /// and the functions decoding markers and mapping rotation to the cursor. -pub(crate) fn include(colors: &[Rgb], controls: &[Rect], cursor_size: Size) -> String { - let rgb = |color: &Rgb| format!("vec3({}.0, {}.0, {}.0)", color.r, color.g, color.b); - let colors_glsl = colors.iter().map(rgb).collect::>().join(", "); +pub(crate) fn include(colors: &[(Rgb, u8)], controls: &[Rect], cursor_size: Size) -> String { + let rgba = |(color, alpha): &(Rgb, u8)| format!("vec4({}.0, {}.0, {}.0, {alpha}.0)", color.r, color.g, color.b); + let colors_glsl = colors.iter().map(rgba).collect::>().join(", "); let rects: Vec = controls .iter() .map(|rect| format!("vec4({}.0, {}.0, {}.0, {}.0)", rect.x, rect.y, rect.width, rect.height)) @@ -277,9 +277,9 @@ const vec2 WINDOW_FS_CURSOR_SIZE = vec2({cursor_width}.0, {cursor_height}.0); // pixels. const vec2 WINDOW_FS_HUD_STRIP = vec2({strip_half_width}.0, {strip_height}.0); -// Marker colors, 0-255 per channel; index 0 is white. +// Marker colors with their alpha, 0-255 per channel; index 0 is white. Only backdrops are translucent. const int WINDOW_FS_COLOR_COUNT = {color_count}; -const vec3 WINDOW_FS_COLORS[{color_count}] = vec3[]({colors_glsl}); +const vec4 WINDOW_FS_COLORS[{color_count}] = vec4[]({colors_glsl}); // Every full-screen control rect (x, y, width, height) in canvas pixels, by the control's manifest index. const int WINDOW_FS_CONTROL_COUNT = {control_count}; @@ -305,7 +305,7 @@ int window_fs_layer(int payload) {{ }} // The color a marked glyph's texels are multiplied by. -vec3 window_fs_color(int payload) {{ +vec4 window_fs_color(int payload) {{ return WINDOW_FS_COLORS[min(payload & 1023, WINDOW_FS_COLOR_COUNT - 1)] / 255.0; }} diff --git a/crates/window-core/src/hud/renderer/fullscreen.rs b/crates/window-core/src/hud/renderer/fullscreen.rs index 0b464526..d1b67dec 100644 --- a/crates/window-core/src/hud/renderer/fullscreen.rs +++ b/crates/window-core/src/hud/renderer/fullscreen.rs @@ -82,7 +82,7 @@ void window_fs_apply() { gl_Position = vec4(ndc, window_fs_depth(window_fs_layer(payload)), 1.0); sphericalVertexDistance = WINDOW_FS_NO_FOG_DISTANCE; cylindricalVertexDistance = WINDOW_FS_NO_FOG_DISTANCE; - vertexColor = vec4(window_fs_color(payload), 1.0); + vertexColor = window_fs_color(payload); texCoord0 = UV0; #ifdef IS_GRAYSCALE windowHudTextureMode = 0; diff --git a/crates/window-core/src/ir.rs b/crates/window-core/src/ir.rs index 29274353..d2f7511b 100644 --- a/crates/window-core/src/ir.rs +++ b/crates/window-core/src/ir.rs @@ -131,6 +131,15 @@ impl Rgb { } } +/// What fills the whole screen behind a full-screen canvas, letterbox included. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Backdrop { + /// The fill color. + pub color: Rgb, + /// The fill's opacity: 255 hides the world, and anything lower dims it. + pub alpha: u8, +} + /// The runtime action closing the window. Action ids in the `window:` namespace are resolved by the runtime. pub const CLOSE_ACTION: &str = "window:close"; @@ -631,8 +640,8 @@ pub struct LaidOutFullscreen { pub name: String, /// How the cursor follows the mouse. pub mode: FullscreenMode, - /// The color filling the whole screen behind the canvas, if any. - pub backdrop: Option, + /// What fills the whole screen behind the canvas, if anything. + pub backdrop: Option, /// Static draws in paint order (back to front). pub draws: Vec, /// Text regions (dynamic slots and static labels), document order. diff --git a/crates/window-core/src/model.rs b/crates/window-core/src/model.rs index d9949d59..75731936 100644 --- a/crates/window-core/src/model.rs +++ b/crates/window-core/src/model.rs @@ -5,7 +5,7 @@ use std::collections::{BTreeMap, BTreeSet}; use crate::geometry::{Insets, Point, Size}; use crate::inventory::{InventorySlotRef, SlotPattern}; -use crate::ir::{Align, FullscreenMode, Handle, HudChannel, HudShader, Rgb}; +use crate::ir::{Align, Backdrop, FullscreenMode, Handle, HudChannel, HudShader, Rgb}; use crate::surface::ContainerKind; mod flex; @@ -130,8 +130,8 @@ pub struct Fullscreen { pub name: String, /// How the cursor follows the mouse. pub mode: FullscreenMode, - /// The color filling the whole screen behind the canvas, letterbox included; `None` shows the world. - pub backdrop: Option, + /// What fills the whole screen behind the canvas, letterbox included; `None` shows the world as is. + pub backdrop: Option, /// The root elements, laid out in the canvas rect. Inventory elements are rejected during layout. pub children: Vec, /// Typed handles by id; their elements carry the entry names of their uses. diff --git a/crates/window-core/src/pipeline/fullscreen.rs b/crates/window-core/src/pipeline/fullscreen.rs index 60456627..d7d6efde 100644 --- a/crates/window-core/src/pipeline/fullscreen.rs +++ b/crates/window-core/src/pipeline/fullscreen.rs @@ -28,6 +28,7 @@ use super::{CompileContext, OutputFile}; mod horse; const WHITE: Rgb = Rgb::new(0xff, 0xff, 0xff); +const OPAQUE: u8 = 0xff; /// The item model, under the pack namespace, that draws nothing: the runtime fills the hotbar with it while a /// full-screen window is open, which also hides the hand. @@ -35,7 +36,8 @@ const BLANK_ITEM: &str = "fullscreen/blank"; /// What the full-screen windows compiled so far put in the include: the color table and every control rect. pub(super) struct Contract { - colors: Vec, + /// Colors with their alpha; only backdrops are translucent. + colors: Vec<(Rgb, u8)>, controls: Vec, /// Whether a horse-mode window was compiled. horse: bool, @@ -43,13 +45,19 @@ pub(super) struct Contract { impl Default for Contract { fn default() -> Self { - Self { colors: vec![WHITE], controls: Vec::new(), horse: false } + Self { colors: vec![(WHITE, OPAQUE)], controls: Vec::new(), horse: false } } } impl Contract { - /// The marker color of a `kind` run at `layer` drawn in `color`, adding `color` to the color table. + /// The marker color of a `kind` run at `layer` drawn in opaque `color`, adding `color` to the color table. fn marker(&mut self, kind: MarkerKind, layer: u8, color: Rgb) -> Result { + self.translucent_marker(kind, layer, color, OPAQUE) + } + + /// Like [`Self::marker`], with the run drawn at `alpha`. + fn translucent_marker(&mut self, kind: MarkerKind, layer: u8, color: Rgb, alpha: u8) -> Result { + let color = (color, alpha); let index = match self.colors.iter().position(|known| *known == color) { Some(index) => index, None => { @@ -183,8 +191,10 @@ pub(super) fn compile_fullscreen( contract.controls.push(rect); controls.insert(region.name.clone(), entry); } - let backdrop_marker = - f.backdrop.map(|color| contract.marker(MarkerKind::Backdrop, BACKDROP_LAYER, color)).transpose()?; + let backdrop_marker = f + .backdrop + .map(|b| contract.translucent_marker(MarkerKind::Backdrop, BACKDROP_LAYER, b.color, b.alpha)) + .transpose()?; Ok(FullscreenEntry { surface: FullscreenSurfaceEntry { diff --git a/crates/window-core/src/pipeline/preview.rs b/crates/window-core/src/pipeline/preview.rs index 302238dd..6fdc411a 100644 --- a/crates/window-core/src/pipeline/preview.rs +++ b/crates/window-core/src/pipeline/preview.rs @@ -16,7 +16,7 @@ use crate::compose::{Composite, Texture, blit_sprite, multiply}; use crate::geometry::{Point, Rect, Size}; use crate::inventory::InventorySlotRef; use crate::ir::{ - Align, AnvilInputIr, ArtIr, CollectionIr, Draw, Handle, HandleKind, HandleRole, HandleUse, ItemIr, + Align, AnvilInputIr, ArtIr, Backdrop, CollectionIr, Draw, Handle, HandleKind, HandleRole, HandleUse, ItemIr, LaidOutFullscreen, LaidOutHud, LaidOutWindow, Layer, RegionIr, Rgb, SlotIr, SpriteSlotIr, SwitchIr, bar_columns, }; use crate::model::{GeneratedStyle, SpriteDef}; @@ -260,8 +260,8 @@ struct Pane<'a> { label: String, /// The surface rect in GUI space, or in canvas pixels for a full-screen window. rect: Rect, - /// The color filling the whole image behind everything else. - backdrop: Option, + /// What fills the whole image behind everything else. + backdrop: Option, /// The shadow of slot text. shadow: Shadow, container: Option, @@ -667,8 +667,8 @@ impl Scene<'_> { placement.include_in(&mut bounds); } let mut canvas = Canvas::new(bounds.into_rect(&request.name, request.scale.unwrap_or(1))?); - if let Some(Rgb { r, g, b }) = pane.backdrop { - canvas.texture.rgba.as_chunks_mut::<4>().0.fill([r, g, b, 0xff]); + if let Some(Backdrop { color: Rgb { r, g, b }, alpha }) = pane.backdrop { + canvas.texture.rgba.as_chunks_mut::<4>().0.fill([r, g, b, alpha]); } let field = ContainerKind::ANVIL_TEXT_FIELD; if pane.container == Some(ContainerKind::Anvil) diff --git a/crates/window-core/src/pipeline/tests/fullscreens.rs b/crates/window-core/src/pipeline/tests/fullscreens.rs index 15f05144..236da09e 100644 --- a/crates/window-core/src/pipeline/tests/fullscreens.rs +++ b/crates/window-core/src/pipeline/tests/fullscreens.rs @@ -32,9 +32,9 @@ fn include(out: &CompileOutput) -> &str { /// The include's color table, as `r, g, b` strings. fn color_table(include: &str) -> Vec { - let line = include.lines().find(|line| line.starts_with("const vec3 WINDOW_FS_COLORS")).unwrap(); - let list = line.split_once("vec3[](").unwrap().1.trim_end_matches(");"); - list.split("vec3(") + let line = include.lines().find(|line| line.starts_with("const vec4 WINDOW_FS_COLORS")).unwrap(); + let list = line.split_once("vec4[](").unwrap().1.trim_end_matches(");"); + list.split("vec4(") .filter(|entry| !entry.is_empty()) .map(|entry| entry.trim_end_matches([')', ',', ' ']).replace(".0", "")) .collect() @@ -49,7 +49,7 @@ fn panel() -> serde_json::Value { fn compiles_layers_controls_and_the_contract() { let tone = json!({ "kind": "tint", "id": "tone", "values": ["ok", "low"], "colors": ["#3ddc84", "#ff4d4d"] }); let out = compile(json!({ - "name": "menu", "mode": "horse", "backdrop": "#101010", + "name": "menu", "mode": "horse", "backdrop": "#10101080", "children": [ { "type": "sprite", "art": panel(), "x": 220, "y": 125 }, { "type": "slot", "handle": { "kind": "text", "id": "title" }, "width": 60, "x": 10, "y": 200, @@ -72,15 +72,15 @@ fn compiles_layers_controls_and_the_contract() { let (layer, index) = decode(marker, kind); (layer, table[index].as_str()) }; - assert_eq!(color(&menu.static_marker, 0), (1, "255, 255, 255")); - assert_eq!(color(&menu.markers["title"], 0), (2, "255, 215, 0")); + assert_eq!(color(&menu.static_marker, 0), (1, "255, 255, 255, 255")); + assert_eq!(color(&menu.markers["title"], 0), (2, "255, 215, 0, 255")); assert_eq!(color(&menu.markers["status"], 0).0, 3); let status = &menu.slots["status"].tint_markers; - assert_eq!(color(&status["ok"], 0), (3, "61, 220, 132")); - assert_eq!(color(&status["low"], 0), (3, "255, 77, 77")); - assert_eq!(color(menu.backdrop_marker.as_deref().unwrap(), 2), (0, "16, 16, 16")); - assert_eq!(color(&canvas.cursor.marker, 1), (255, "255, 255, 255")); - assert_eq!(color(&canvas.overlay_marker, 0), (fullscreen::OVERLAY_LAYER.into(), "255, 255, 255")); + assert_eq!(color(&status["ok"], 0), (3, "61, 220, 132, 255")); + assert_eq!(color(&status["low"], 0), (3, "255, 77, 77, 255")); + assert_eq!(color(menu.backdrop_marker.as_deref().unwrap(), 2), (0, "16, 16, 16, 128")); + assert_eq!(color(&canvas.cursor.marker, 1), (255, "255, 255, 255, 255")); + assert_eq!(color(&canvas.overlay_marker, 0), (fullscreen::OVERLAY_LAYER.into(), "255, 255, 255, 255")); let play = &menu.controls["play"]; assert_eq!((play.index, play.x, play.y, play.width, play.height), (0, 0, 0, 40, 20)); @@ -93,6 +93,16 @@ fn compiles_layers_controls_and_the_contract() { assert_eq!(&parsed, manifest); } +#[test] +fn a_dimming_backdrop_needs_an_alpha_the_text_shader_draws() { + let backdrop = |color: &str| compile(json!({ "name": "menu", "backdrop": color, "children": [] })); + assert!(backdrop("#0000001a").is_ok()); + let error = backdrop("#00000019").unwrap_err().to_string(); + assert!(error.contains("below the lowest alpha"), "{error}"); + assert!(backdrop("#0000").is_err()); + assert!(backdrop("#00000\u{e9}0").is_err()); +} + #[test] fn packs_without_fullscreens_emit_no_contract_or_shaders() { let project = json!({ "huds": [{ "name": "h", "children": [] }], "target": { "pack_format": 88 } }).to_string(); @@ -196,7 +206,7 @@ fn lays_tooltips_out_beside_their_controls_above_every_window_layer() { let [title] = top.lines.as_slice() else { panic!("{:?}", top.lines) }; assert_eq!((title.x, title.y, title.text.as_deref()), ((40 - (title.width as i32 + 8)) / 2 + 4, 36, Some("Top"))); let (layer, color) = decode(title.shader_marker.as_deref().unwrap(), 0); - assert_eq!((layer, table[color].as_str()), (u32::from(fullscreen::TOOLTIP_TEXT_LAYER), "255, 255, 255")); + assert_eq!((layer, table[color].as_str()), (u32::from(fullscreen::TOOLTIP_TEXT_LAYER), "255, 255, 255, 255")); // Above its control where the canvas ends below it, and kept on the canvas; the title is 2px further from its // lines than they are apart. @@ -207,7 +217,7 @@ fn lays_tooltips_out_beside_their_controls_above_every_window_layer() { let left = 480 - (bottom.lines.iter().map(|line| line.width).max().unwrap() as i32 + 8) + 4; assert_eq!(rows, [(left, top_y), (left, top_y + 12), (left, top_y + 22)]); let gray = decode(bottom.lines[1].shader_marker.as_deref().unwrap(), 0).1; - assert_eq!(table[gray], "170, 170, 170"); + assert_eq!(table[gray], "170, 170, 170, 255"); let file = out .files diff --git a/docs/AUTHORING.md b/docs/AUTHORING.md index 43c0bf40..3279d254 100644 --- a/docs/AUTHORING.md +++ b/docs/AUTHORING.md @@ -629,7 +629,10 @@ the core shaders that draw it, with no `coreShaders` option, and needs pack form - `mode`: `"unlocked"`, the default, keeps the camera on the player, so the world turns behind the canvas as they aim. `"horse"` locks the view in place and draws the cursor about a tick behind the mouse. -- `backdrop`: a hex color filling the screen behind the canvas, letterbox included. Omit it to show the world. +- `backdrop`: a hex color filling the screen behind the canvas, letterbox included. `#rrggbb` hides the world, and + `#rrggbbaa` dims it, such as `#00000099`; the alpha starts at `1a`, since the text shader drops fainter fragments. + Omit it to show the world. Translucent world layers drawn after the canvas, such as water, stained glass, and some + particles, can be hidden behind a dimming backdrop rather than dimmed. It takes the same elements as a window, except those that need a container: diff --git a/example/pack/src/window/storefront.tsx b/example/pack/src/window/storefront.tsx index 5aed5ae0..7277d832 100644 --- a/example/pack/src/window/storefront.tsx +++ b/example/pack/src/window/storefront.tsx @@ -358,12 +358,12 @@ const footer = ( ); /** - * A web-style storefront over the world, in horse mode so the view holds still behind it: no backdrop, translucent + * A web-style storefront over the world, in horse mode so the view holds still behind it: a backdrop dims it, translucent * glass panes, category tabs, a search box the player types into in place, a product grid and a cart that each scroll, * and a detail pane to buy from. */ export default ( - + {nav} {catalog} diff --git a/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenEntries.kt b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenEntries.kt index 937d4877..df2f0b99 100644 --- a/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenEntries.kt +++ b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenEntries.kt @@ -7277,6 +7277,7 @@ internal object WindowFullscreenEntries { "store_sort_label" to "#a05800", "store_toast" to "#a23c02", ), + backdropMarker = "#a80010", ), ) val canvas: FullscreenCanvasEntry = diff --git a/jvm/minestom/src/main/kotlin/com/chunkzero/window/minestom/internal/MinestomFullscreen.kt b/jvm/minestom/src/main/kotlin/com/chunkzero/window/minestom/internal/MinestomFullscreen.kt index 8aa1cfea..6c5d7d3e 100644 --- a/jvm/minestom/src/main/kotlin/com/chunkzero/window/minestom/internal/MinestomFullscreen.kt +++ b/jvm/minestom/src/main/kotlin/com/chunkzero/window/minestom/internal/MinestomFullscreen.kt @@ -39,11 +39,14 @@ import net.minestom.server.network.packet.client.play.ClientHeldItemChangePacket import net.minestom.server.network.packet.client.play.ClientInputPacket import net.minestom.server.network.packet.client.play.ClientInteractEntityPacket import net.minestom.server.network.packet.client.play.ClientPlayerActionPacket +import net.minestom.server.network.packet.client.play.ClientPlayerBlockPlacementPacket import net.minestom.server.network.packet.client.play.ClientPlayerPositionAndRotationPacket import net.minestom.server.network.packet.client.play.ClientPlayerRotationPacket +import net.minestom.server.network.packet.client.play.ClientUseItemPacket import net.minestom.server.network.packet.server.ServerPacket import net.minestom.server.network.packet.server.common.ClearDialogPacket import net.minestom.server.network.packet.server.common.ShowDialogPacket +import net.minestom.server.network.packet.server.play.AcknowledgeBlockChangePacket import net.minestom.server.network.packet.server.play.ChangeGameStatePacket import net.minestom.server.network.packet.server.play.CloseWindowPacket import net.minestom.server.network.packet.server.play.DestroyEntitiesPacket @@ -381,6 +384,17 @@ internal class MinestomFullscreen private constructor( event.isCancelled = true } + // A right click that misses the interaction entity would use the item the server's player really holds. + is ClientUseItemPacket -> { + event.isCancelled = true + rejectUse(packet.sequence()) + } + + is ClientPlayerBlockPlacementPacket -> { + event.isCancelled = true + rejectUse(packet.sequence()) + } + is ClientCloseWindowPacket -> { // Minestom ignores a close for any window but the open one, so a stale close must not hide the HUD. whileOpen { if (packet.windowId() == containerWindowId) containerClosed() } @@ -396,6 +410,15 @@ internal class MinestomFullscreen private constructor( } } + /** + * Undoes what the client predicted for a cancelled use: acknowledging [sequence] restores the blocks it placed, and + * the inventory resync restores the off-hand item it spent, while the hotbar stays blank. + */ + private fun rejectUse(sequence: Int) { + player.sendPacket(AcknowledgeBlockChangePacket(sequence)) + player.inventory.update(player) + } + /** Reports the client selecting [slot] as steps the shortest way around the hotbar from the slot it had. */ private fun scroll(slot: Int) { if (slot !in 0 until HOTBAR_SIZE) return diff --git a/jvm/minestom/src/test/kotlin/com/chunkzero/window/minestom/MinestomFullscreenTest.kt b/jvm/minestom/src/test/kotlin/com/chunkzero/window/minestom/MinestomFullscreenTest.kt index 4bfabe68..0b27de4d 100644 --- a/jvm/minestom/src/test/kotlin/com/chunkzero/window/minestom/MinestomFullscreenTest.kt +++ b/jvm/minestom/src/test/kotlin/com/chunkzero/window/minestom/MinestomFullscreenTest.kt @@ -47,6 +47,7 @@ import net.minestom.server.event.player.PlayerDisconnectEvent import net.minestom.server.event.player.PlayerPacketEvent import net.minestom.server.event.player.PlayerPacketOutEvent import net.minestom.server.event.player.PlayerSpawnEvent +import net.minestom.server.instance.block.BlockFace import net.minestom.server.inventory.Inventory import net.minestom.server.inventory.InventoryType import net.minestom.server.item.ItemStack @@ -62,12 +63,15 @@ import net.minestom.server.network.packet.client.play.ClientHeldItemChangePacket import net.minestom.server.network.packet.client.play.ClientInputPacket import net.minestom.server.network.packet.client.play.ClientInteractEntityPacket import net.minestom.server.network.packet.client.play.ClientNameItemPacket +import net.minestom.server.network.packet.client.play.ClientPlayerBlockPlacementPacket import net.minestom.server.network.packet.client.play.ClientPlayerRotationPacket +import net.minestom.server.network.packet.client.play.ClientUseItemPacket import net.minestom.server.network.packet.server.SendablePacket import net.minestom.server.network.packet.server.ServerPacket import net.minestom.server.network.packet.server.common.ClearDialogPacket import net.minestom.server.network.packet.server.common.PingPacket import net.minestom.server.network.packet.server.common.ShowDialogPacket +import net.minestom.server.network.packet.server.play.AcknowledgeBlockChangePacket import net.minestom.server.network.packet.server.play.CameraPacket import net.minestom.server.network.packet.server.play.ChangeGameStatePacket import net.minestom.server.network.packet.server.play.CloseWindowPacket @@ -194,6 +198,12 @@ class MinestomFullscreenTest : fixture.receive(ClientInteractEntityPacket(0, PlayerHand.MAIN, Vec.ZERO, false)).isCancelled shouldBe true fixture.receive(ClientInteractEntityPacket(0, PlayerHand.OFF, Vec.ZERO, false)) + fixture.receive(ClientUseItemPacket(PlayerHand.MAIN, 7, 0f, 0f)).isCancelled shouldBe true + val placement = + ClientPlayerBlockPlacementPacket(PlayerHand.OFF, Vec.ZERO, BlockFace.TOP, 0f, 0f, 0f, false, false, 8) + fixture.receive(placement).isCancelled shouldBe true + fixture.sent().map { it.sequence() } shouldContainExactly listOf(7, 8) + fixture.hotbar() shouldBe List(9) { Material.PAPER } fixture.receive(ClientPlayerRotationPacket(-44f, -24.375f, 0.toByte())) fixture.player.scheduler().processTickEnd() diff --git a/plugin/src/ui/components.ts b/plugin/src/ui/components.ts index c1da57c0..454fb52b 100644 --- a/plugin/src/ui/components.ts +++ b/plugin/src/ui/components.ts @@ -299,7 +299,7 @@ export interface FullscreenProps extends Omit< name: string; /** Defaults to `"unlocked"`. */ mode?: FullscreenMode; - /** Hex color filling the whole screen behind the canvas, letterbox included; omit to show the world. */ + /** `#rrggbb` filling the whole screen behind the canvas, letterbox included, or `#rrggbbaa` dimming the world behind it; omit to show the world. */ backdrop?: string; } diff --git a/plugin/src/ui/document.ts b/plugin/src/ui/document.ts index 63a02398..43a5ebb7 100644 --- a/plugin/src/ui/document.ts +++ b/plugin/src/ui/document.ts @@ -387,7 +387,7 @@ export interface Fullscreen { name: string; /** Defaults to `"unlocked"`. */ mode?: FullscreenMode; - /** Hex color filling the whole screen behind the canvas, letterbox included; omit to show the world. */ + /** `#rrggbb` filling the whole screen behind the canvas, letterbox included, or `#rrggbbaa` dimming the world behind it; omit to show the world. */ backdrop?: string; /** Overrides vars for the whole full-screen window. */ theme?: VarTheme; diff --git a/plugin/test/golden/fullscreen/expected.sha256 b/plugin/test/golden/fullscreen/expected.sha256 index 49030222..c223de68 100644 --- a/plugin/test/golden/fullscreen/expected.sha256 +++ b/plugin/test/golden/fullscreen/expected.sha256 @@ -4,8 +4,8 @@ de99b339c5bc4b28a85d9fe06066c528fa7c10497037c51dca18652360f14aa3 dist/assets/mi 96913f57883c88fa694c0ba08eeeda4b9d8973313fa1adf4326c58cadab77aa3 dist/assets/minecraft/shaders/core/position_tex_color.fsh fce31a4ee7abc60a5676275134fba3200604442b2e9031cdaa4cedc8d4c4d2c4 dist/assets/minecraft/shaders/core/position_tex_color.vsh 56bb409c4026de938f0a2f77bd3ac9131658aad33facbb9b92145e9095ba0fdd dist/assets/minecraft/shaders/core/text.fsh -f6a980f98a5850b6530119b72ef064d6e3365f2176c2ba9c50b7e3ce6524173b dist/assets/minecraft/shaders/core/text.vsh -d8c8dc63512f18a6f85b86ce57d473cc2b2b76453ba1c0fd3ba97b47bbb5b26f dist/assets/minecraft/shaders/include/window_fullscreen.glsl +ee48e982da11b8ad6097a36ee74f10a8602fe7c0bad7bce2ea31bd02f426e52a dist/assets/minecraft/shaders/core/text.vsh +069c77a5ab12615f00a8d71fd5e24f837cdabecc206757dd33eda02ceaaa7d66 dist/assets/minecraft/shaders/include/window_fullscreen.glsl 8f0a28c175d2c93ff86aa1377f34c9e8fbc76e922ca34ae2f6db19dd557d285e dist/assets/minecraft/textures/gui/sprites/hud/crosshair.png 69beab9c90d9a84fa45fd48c393db16ea354338990a7955935cf8518f6fbcf74 dist/assets/window/equipment/fullscreen/horse_cursor.json 36544f87434a58ca80349fb16fbbe42b24133f26bb83bab37e684d1cb7a10c73 dist/assets/window/font/ui.json @@ -35,7 +35,7 @@ a962b9aee37baf82e29390badf062f445b50a8e577de3731aa0947fcdbbd9222 dist/assets/wi 922d405eb87e7d345085f0d93e32a111d1e4bf6ddcc4102f0b1bb0e754ce06be dist/assets/window/textures/font/fullscreen/window/menu_tooltip_220_160_41x16_0_0.png 922d405eb87e7d345085f0d93e32a111d1e4bf6ddcc4102f0b1bb0e754ce06be dist/assets/window/textures/font/fullscreen/window/menu_tooltip_256_160_41x16_0_0.png eacb1a012fc0820bb358ea06380857fd97d62a420932142014ac89bcc4afbbc3 dist/assets/window/textures/gui/hitbox.png -748f9f2749c182fc80ef6f030e0f0719743db138268d617b3005cd0d2e25f75a dist/assets/window/window/debug.json +9708e2312c71397a6082ff8d95813c23bb14ee2bd15ae35390e51d9d6aea24d7 dist/assets/window/window/debug.json 65dd34ebb2d97b0c2f9ba3e310bc02fe652f4690a069e47d8b3aa99f76a94bfc dist/pack.mcmeta 23bf284dd31ac909286bdae4abd75e24f127fe9c720c9bd0cac4872c6f130a46 kotlin-out/LookoutFullscreen.kt c7112fbdeac4c678c3fdc1eacdd4fbccedbc28c3b0ff8baf44ef5af2d1d104a2 kotlin-out/MenuFullscreen.kt