From a8f48f5d7a0607e1f789e99267ee9cf1897a8473 Mon Sep 17 00:00:00 2001 From: Purdze <29306641+Purdze@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:54:07 +0000 Subject: [PATCH 1/2] send the main hand and skin parts from the options --- pomme-client/src/app/mod.rs | 5 ++- pomme-client/src/app/phases/in_game.rs | 60 ++++++++++++++++++-------- pomme-client/src/app/phases/in_menu.rs | 17 +++++--- pomme-client/src/headless.rs | 10 +++-- pomme-client/src/net/azalea_compat.rs | 14 ++++++ pomme-client/src/net/connection.rs | 22 +++++++++- pomme-client/src/net/mod.rs | 22 ++++++---- pomme-client/src/ui/hud.rs | 3 +- pomme-client/src/ui/menu/mod.rs | 42 ++++++++++++++++++ 9 files changed, 155 insertions(+), 40 deletions(-) diff --git a/pomme-client/src/app/mod.rs b/pomme-client/src/app/mod.rs index 94b4c8d1f..b2a7ca340 100644 --- a/pomme-client/src/app/mod.rs +++ b/pomme-client/src/app/mod.rs @@ -199,6 +199,7 @@ fn start_connecting( core.menu.render_distance, world.is_some(), core.menu.chat_options, + core.menu.skin_options(), block_definitions, protocol, ); @@ -557,7 +558,7 @@ impl ApplicationHandler for App { MenuUpdateResult::Connect { connect_args, world, - } => start_connecting(&mut self.core, gfx, panorama, connect_args, world), + } => start_connecting(&mut self.core, gfx, panorama, *connect_args, world), _ => AppPhase::InMenu { gfx, panorama }, } } @@ -1093,7 +1094,7 @@ impl ApplicationHandler for App { MenuUpdateResult::Connect { connect_args, world, - } => start_connecting(core, gfx, panorama, connect_args, world), + } => start_connecting(core, gfx, panorama, *connect_args, world), MenuUpdateResult::Quit => { event_loop.exit(); AppPhase::InMenu { gfx, panorama } diff --git a/pomme-client/src/app/phases/in_game.rs b/pomme-client/src/app/phases/in_game.rs index 1686b575f..da73a5f9a 100644 --- a/pomme-client/src/app/phases/in_game.rs +++ b/pomme-client/src/app/phases/in_game.rs @@ -276,6 +276,7 @@ pub struct GameState { pub last_render_distance: u32, pub last_chat_visibility: crate::ui::chat::ChatVisibilitySetting, pub last_chat_colors: bool, + pub last_skin_options: crate::ui::menu::SkinOptions, pub server_render_distance: u32, pub server_simulation_distance: u32, pub item_entity_store: ItemEntityStore, @@ -372,12 +373,14 @@ impl GameState { self.recent_sounds.push_back(sound.event_name().to_owned()); } + #[allow(clippy::too_many_arguments)] pub(crate) fn new( mesh_source: &impl MeshSource, resource_packs: Option<&ResourcePackManager>, render_distance: u32, singleplayer: bool, chat_options: crate::ui::chat::ChatOptions, + skin_options: crate::ui::menu::SkinOptions, block_definitions: Rc, protocol: i32, ) -> Self { @@ -415,6 +418,7 @@ impl GameState { last_render_distance: render_distance, last_chat_visibility: chat_options.visibility, last_chat_colors: chat_options.colors, + last_skin_options: skin_options, server_render_distance: 0, server_simulation_distance: 0, item_entity_store: ItemEntityStore::new(), @@ -1028,11 +1032,16 @@ impl GameState { ]); } - /// Whether the chat options `ClientInformation` carries differ from the - /// last ones sent. - fn chat_information_changed(&self, chat_options: crate::ui::chat::ChatOptions) -> bool { + /// Whether the chat or skin options `ClientInformation` carries differ + /// from the last ones sent. + fn client_options_changed( + &self, + chat_options: crate::ui::chat::ChatOptions, + skin_options: crate::ui::menu::SkinOptions, + ) -> bool { self.last_chat_visibility != chat_options.visibility || self.last_chat_colors != chat_options.colors + || self.last_skin_options != skin_options } pub fn sync_client_information( @@ -1040,20 +1049,23 @@ impl GameState { connection: &ConnectionHandle, render_distance: u32, chat_options: crate::ui::chat::ChatOptions, + skin_options: crate::ui::menu::SkinOptions, ) { let render_changed = self.last_render_distance != render_distance; - let chat_changed = self.chat_information_changed(chat_options); + let options_changed = self.client_options_changed(chat_options, skin_options); self.last_render_distance = render_distance; self.last_chat_visibility = chat_options.visibility; self.last_chat_colors = chat_options.colors; + self.last_skin_options = skin_options; if render_changed { tracing::info!("Render distance changed to {render_distance}"); } - if chat_changed { + if options_changed { tracing::info!( visibility = ?chat_options.visibility, colors = chat_options.colors, - "Chat client information changed" + skin = ?skin_options, + "Client information options changed" ); } @@ -1064,6 +1076,7 @@ impl GameState { client_information: crate::net::client_information( render_distance as u8, chat_options, + skin_options, ), }, )); @@ -1900,14 +1913,24 @@ fn apply_render_distance( rd: u32, ) { core.menu.set_bench_render_distance(rd); - game.sync_client_information(connection, rd, core.menu.chat_options); + game.sync_client_information( + connection, + rd, + core.menu.chat_options, + core.menu.skin_options(), + ); } /// Puts back the render distance the chunk-load benchmark replaced, on the /// server too. fn end_chunk_load(core: &mut AppCore, game: &mut GameState, connection: &ConnectionHandle) { if let Some(rd) = core.menu.restore_bench_render_distance() { - game.sync_client_information(connection, rd, core.menu.chat_options); + game.sync_client_information( + connection, + rd, + core.menu.chat_options, + core.menu.skin_options(), + ); } } @@ -4268,19 +4291,22 @@ pub fn update_game( PauseAction::None => {} } + // Vanilla `Options.save` broadcasts on every change, wherever it was made. + let (chat_options, skin_options) = (core.menu.chat_options, core.menu.skin_options()); + if core.menu.render_distance != game.last_render_distance + || game.client_options_changed(chat_options, skin_options) + { + game.sync_client_information( + connection, + core.menu.render_distance, + chat_options, + skin_options, + ); + } if game.options_from_game { // TODO: a resource-pack toggle's `menu.reload_assets` is only applied // once back on the title screen. core.apply_font_options(&mut gfx.renderer); - if core.menu.render_distance != game.last_render_distance - || game.chat_information_changed(core.menu.chat_options) - { - game.sync_client_information( - connection, - core.menu.render_distance, - core.menu.chat_options, - ); - } if !core.menu.is_options_screen() { game.options_from_game = false; // Chat Settings opened from chat return to it, unpaused. diff --git a/pomme-client/src/app/phases/in_menu.rs b/pomme-client/src/app/phases/in_menu.rs index 39bd74121..9eef3ba53 100644 --- a/pomme-client/src/app/phases/in_menu.rs +++ b/pomme-client/src/app/phases/in_menu.rs @@ -8,7 +8,7 @@ use crate::ui::menu::MenuAction; pub enum MenuUpdateResult { None, Connect { - connect_args: ConnectArgs, + connect_args: Box, world: Option, }, Quit, @@ -35,14 +35,14 @@ pub fn open_world(core: &mut AppCore, folder: &str) -> MenuUpdateResult { core.audio.stop_menu_music(); let username = core.user.username.clone(); MenuUpdateResult::Connect { - connect_args: connect_args( + connect_args: Box::new(connect_args( core, Transport::Memory { end: client_end, protocol: singleplayer::PROTOCOL, }, username, - ), + )), world: Some(world), } } @@ -53,14 +53,14 @@ pub fn quick_play(core: &mut AppCore, play: QuickPlay) -> MenuUpdateResult { QuickPlay::Multiplayer(server) => MenuUpdateResult::Connect { // TODO: read the saved server list's protocol for this address to // skip the join-time probe. - connect_args: connect_args( + connect_args: Box::new(connect_args( core, Transport::Remote { server, protocol: None, }, core.user.username.clone(), - ), + )), world: None, }, QuickPlay::Singleplayer(folder) => open_world(core, &folder), @@ -75,6 +75,7 @@ pub fn connect_args(core: &AppCore, transport: Transport, username: String) -> C access_token: core.user.access_token.clone(), view_distance: core.view_distance(), chat_options: core.menu.chat_options, + skin_options: core.menu.skin_options(), } } @@ -191,7 +192,11 @@ pub fn update_menu( core.audio.stop_menu_music(); return MenuUpdateResult::Connect { - connect_args: connect_args(core, Transport::Remote { server, protocol }, username), + connect_args: Box::new(connect_args( + core, + Transport::Remote { server, protocol }, + username, + )), world: None, }; } diff --git a/pomme-client/src/headless.rs b/pomme-client/src/headless.rs index ff0761dfc..e909f5707 100644 --- a/pomme-client/src/headless.rs +++ b/pomme-client/src/headless.rs @@ -23,7 +23,7 @@ use crate::renderer::RenderTimings; use crate::renderer::chunk::mesh_source::CpuMeshAssets; use crate::singleplayer::World; use crate::ui::chat::ChatOptions; -use crate::ui::menu::saved_chunk_detail; +use crate::ui::menu::{SkinOptions, saved_chunk_detail}; use crate::ui::world_list::WorldList; use crate::user::UserData; use crate::world::block::block_registry::BlockDefinitions; @@ -71,6 +71,7 @@ pub fn run( AssetIndex::load(&dirs.indexes_dir, &dirs.objects_dir, version), ); let chat_options = ChatOptions::default(); + let skin_options = SkinOptions::default(); let connection = spawn_connection( rt, ConnectArgs { @@ -80,6 +81,7 @@ pub fn run( access_token: user.access_token, view_distance: rd as u8, chat_options, + skin_options, }, ); let mut game = GameState::new( @@ -88,6 +90,7 @@ pub fn run( rd, world.is_some(), chat_options, + skin_options, Rc::new(BlockDefinitions::new()), crate::version::selected_protocol(), ); @@ -152,6 +155,7 @@ fn drive( dirs: &DataDirs, ) -> i32 { let chunk_detail = saved_chunk_detail(&dirs.game_dir); + let skin_options = game.last_skin_options; // Columns with at least one non-empty section, as the GPU buffers count // them. let mut drawn: HashMap = HashMap::new(); @@ -271,7 +275,7 @@ fn drive( BenchMode::Headless, *game.player.position, )); - game.sync_client_information(connection, CHUNK_LOAD_MIN_RD, chat_options); + game.sync_client_information(connection, CHUNK_LOAD_MIN_RD, chat_options, skin_options); } // Taken for the step, as in `update_game`, so a new distance can be // sent; `drain_meshes` records the mesh timings into it meanwhile. @@ -284,7 +288,7 @@ fn drive( ) { ChunkLoadStep::Wait => {} ChunkLoadStep::Load(rd) => { - game.sync_client_information(connection, rd, chat_options); + game.sync_client_information(connection, rd, chat_options, skin_options); } ChunkLoadStep::Done(result) => { result.save(&dirs.game_dir); diff --git a/pomme-client/src/net/azalea_compat.rs b/pomme-client/src/net/azalea_compat.rs index 92ca32f3f..447ff6f1e 100644 --- a/pomme-client/src/net/azalea_compat.rs +++ b/pomme-client/src/net/azalea_compat.rs @@ -6835,3 +6835,17 @@ fn lifecycle_packets_match_azalea() { }; assert_eq!(plain(&reason), "bye"); } + +#[test] +fn client_information_reports_the_skin_options() { + use azalea_entity::HumanoidArm; + let skin = crate::ui::menu::SkinOptions { + hat: false, + main_hand_right: false, + ..Default::default() + }; + let info = super::client_information(8, crate::ui::chat::ChatOptions::default(), skin); + assert_eq!(info.main_hand, HumanoidArm::Left); + assert!(!info.model_customization.hat); + assert!(info.model_customization.cape); +} diff --git a/pomme-client/src/net/connection.rs b/pomme-client/src/net/connection.rs index 5178b5caa..70cdff54e 100644 --- a/pomme-client/src/net/connection.rs +++ b/pomme-client/src/net/connection.rs @@ -85,6 +85,7 @@ pub struct ConnectArgs { pub access_token: Option, pub view_distance: u8, pub chat_options: crate::ui::chat::ChatOptions, + pub skin_options: crate::ui::menu::SkinOptions, } pub struct ConnectionHandle { @@ -136,6 +137,7 @@ pub async fn connect_to_server( access_token, view_distance, chat_options, + skin_options, } = args; let (mut conn, wire) = match transport { @@ -204,6 +206,7 @@ pub async fn connect_to_server( &mut conn, view_distance, chat_options, + skin_options, &event_tx, &mut game_packet_rx, None, @@ -234,6 +237,7 @@ pub async fn connect_to_server( joined, view_distance, chat_options, + skin_options, chat: ChatSender::new(profile_id, uuid, access_token, key_pair_tx), key_pair_rx, }, @@ -470,6 +474,7 @@ async fn config_sequence( conn: &mut Conn, view_distance: u8, chat_options: crate::ui::chat::ChatOptions, + skin_options: crate::ui::menu::SkinOptions, event_tx: &Sender, outbound_rx: &mut mpsc::UnboundedReceiver, // `Some` on a mid-session reconfiguration: the previous registries, @@ -502,7 +507,11 @@ async fn config_sequence( conn, ServerboundConfigPacket::ClientInformation( s_client_information::ServerboundClientInformation { - information: super::client_information(view_distance, chat_options), + information: super::client_information( + view_distance, + chat_options, + skin_options, + ), }, ), ) @@ -889,6 +898,7 @@ struct GameLoopArgs { joined: Joined, view_distance: u8, chat_options: crate::ui::chat::ChatOptions, + skin_options: crate::ui::menu::SkinOptions, chat: ChatSender, key_pair_rx: mpsc::UnboundedReceiver>>, } @@ -905,6 +915,7 @@ async fn game_loop( joined, view_distance, chat_options, + skin_options, mut chat, mut key_pair_rx, } = args; @@ -938,7 +949,11 @@ async fn game_loop( // them in the configuration phase (ClientPacketListener.handleLogin). let info = ServerboundGamePacket::ClientInformation( azalea_protocol::packets::game::s_client_information::ServerboundClientInformation { - client_information: super::client_information(view_distance, chat_options), + client_information: super::client_information( + view_distance, + chat_options, + skin_options, + ), }, ); write_game_frame(&mut conn.writer, translation, serialize_frame(&info)?).await?; @@ -1057,6 +1072,7 @@ async fn game_loop( &mut conn, view_distance, chat_options, + skin_options, event_tx, &mut outbound_rx, Some(&configured), @@ -1339,6 +1355,7 @@ mod tests { access_token: None, view_distance: 8, chat_options: crate::ui::chat::ChatOptions::default(), + skin_options: crate::ui::menu::SkinOptions::default(), }, event_tx, packet_tx, @@ -1391,6 +1408,7 @@ mod tests { access_token: None, view_distance: 8, chat_options: crate::ui::chat::ChatOptions::default(), + skin_options: crate::ui::menu::SkinOptions::default(), }, event_tx, packet_tx, diff --git a/pomme-client/src/net/mod.rs b/pomme-client/src/net/mod.rs index a3745b2aa..356c411d8 100644 --- a/pomme-client/src/net/mod.rs +++ b/pomme-client/src/net/mod.rs @@ -641,9 +641,11 @@ pub enum NetworkEvent { /// The client information pomme reports. The configuration and game phase /// packets wrap the same struct under different field names, and 1.20.1 sends /// it in the game phase only, so all three sites share this. +/// Vanilla `Options.buildPlayerInformation`. pub fn client_information( view_distance: u8, chat_options: crate::ui::chat::ChatOptions, + skin: crate::ui::menu::SkinOptions, ) -> azalea_protocol::common::client_information::ClientInformation { use azalea_entity::HumanoidArm; use azalea_protocol::common::client_information::*; @@ -658,15 +660,19 @@ pub fn client_information( chat_visibility, chat_colors: chat_options.colors, model_customization: ModelCustomization { - cape: true, - jacket: true, - left_sleeve: true, - right_sleeve: true, - left_pants: true, - right_pants: true, - hat: true, + cape: skin.cape, + jacket: skin.jacket, + left_sleeve: skin.left_sleeve, + right_sleeve: skin.right_sleeve, + left_pants: skin.left_pants, + right_pants: skin.right_pants, + hat: skin.hat, + }, + main_hand: if skin.main_hand_right { + HumanoidArm::Right + } else { + HumanoidArm::Left }, - main_hand: HumanoidArm::Right, text_filtering_enabled: false, allows_listing: true, particle_status: ParticleStatus::All, diff --git a/pomme-client/src/ui/hud.rs b/pomme-client/src/ui/hud.rs index cef6a3ed7..bfb0d4c23 100644 --- a/pomme-client/src/ui/hud.rs +++ b/pomme-client/src/ui/hud.rs @@ -695,8 +695,7 @@ pub fn build_hud( // Vanilla `Hud.extractItemHotbar`: 18x18 indicator on the main-hand side // of the hotbar, bottom-up fill, plain alpha (no invert, no full-charge // sprite). Spectators get the SpectatorGui hotbar instead - // (`extractHotbarAndDecorations`). TODO: `skin_main_hand_right` isn't - // sent in ClientInformation yet (hardcoded Right in net/connection.rs). + // (`extractHotbarAndDecorations`). if game_mode != 3 && attack.mode == AttackIndicatorMode::Hotbar && attack.scale < 1.0 { let y = (screen_h - 20.0 * gs).round(); let x = if main_hand_right { diff --git a/pomme-client/src/ui/menu/mod.rs b/pomme-client/src/ui/menu/mod.rs index 93caf7ed0..99213f1bf 100644 --- a/pomme-client/src/ui/menu/mod.rs +++ b/pomme-client/src/ui/menu/mod.rs @@ -568,6 +568,35 @@ fn atlas_dirty(count: usize, last: &mut usize, since: &mut Option) -> b } } +/// The skin customization options the server is told about: which model +/// parts show, and the main hand. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SkinOptions { + pub cape: bool, + pub jacket: bool, + pub left_sleeve: bool, + pub right_sleeve: bool, + pub left_pants: bool, + pub right_pants: bool, + pub hat: bool, + pub main_hand_right: bool, +} + +impl Default for SkinOptions { + fn default() -> Self { + Self { + cape: true, + jacket: true, + left_sleeve: true, + right_sleeve: true, + left_pants: true, + right_pants: true, + hat: true, + main_hand_right: true, + } + } +} + pub struct MainMenu { action_binding: Option, username: String, @@ -1058,6 +1087,19 @@ impl MainMenu { self.skin_main_hand_right } + pub fn skin_options(&self) -> SkinOptions { + SkinOptions { + cape: self.skin_cape, + jacket: self.skin_jacket, + left_sleeve: self.skin_left_sleeve, + right_sleeve: self.skin_right_sleeve, + left_pants: self.skin_left_pants, + right_pants: self.skin_right_pants, + hat: self.skin_hat, + main_hand_right: self.skin_main_hand_right, + } + } + pub fn set_main_hand_right(&mut self, right: bool, key_bindings: &KeyBindings) { self.skin_main_hand_right = right; self.save_settings(key_bindings); From e89d3c923d717892635d1b196392490368d2edda Mon Sep 17 00:00:00 2001 From: Purdze <29306641+Purdze@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:23:19 +0000 Subject: [PATCH 2/2] send client information when an options screen closes --- pomme-client/src/app/phases/in_game.rs | 7 ++--- pomme-client/src/harness/exec.rs | 2 ++ pomme-client/src/ui/menu/mod.rs | 37 +++++++++++++++++++++++++- tests/ingame/README.md | 2 +- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/pomme-client/src/app/phases/in_game.rs b/pomme-client/src/app/phases/in_game.rs index da73a5f9a..193efc860 100644 --- a/pomme-client/src/app/phases/in_game.rs +++ b/pomme-client/src/app/phases/in_game.rs @@ -4291,10 +4291,11 @@ pub fn update_game( PauseAction::None => {} } - // Vanilla `Options.save` broadcasts on every change, wherever it was made. + // Vanilla `Options.broadcastOptions`, which sends only what changed. let (chat_options, skin_options) = (core.menu.chat_options, core.menu.skin_options()); - if core.menu.render_distance != game.last_render_distance - || game.client_options_changed(chat_options, skin_options) + if core.menu.take_options_saved() + && (core.menu.render_distance != game.last_render_distance + || game.client_options_changed(chat_options, skin_options)) { game.sync_client_information( connection, diff --git a/pomme-client/src/harness/exec.rs b/pomme-client/src/harness/exec.rs index 4b11637af..ac2d35e12 100644 --- a/pomme-client/src/harness/exec.rs +++ b/pomme-client/src/harness/exec.rs @@ -180,11 +180,13 @@ fn bind_keys(core: &mut AppCore, bind: &protocol::Bind) -> Result Ok(Value::Null) } +/// Sets options as their screen would, then saves them as it does closing. fn set_options(core: &mut AppCore, options: &protocol::OptionSet) -> Result { if let Some(hand) = options.main_hand { core.menu .set_main_hand_right(hand == protocol::Hand::Right, &core.input.key_bindings); } + core.menu.save_options(&core.input.key_bindings); Ok(Value::Null) } diff --git a/pomme-client/src/ui/menu/mod.rs b/pomme-client/src/ui/menu/mod.rs index 99213f1bf..ae7fd8282 100644 --- a/pomme-client/src/ui/menu/mod.rs +++ b/pomme-client/src/ui/menu/mod.rs @@ -722,6 +722,8 @@ pub struct MainMenu { settings_dir: PathBuf, /// Set by slider drags, written by `flush_settings`. settings_dirty: bool, + /// The options were saved, so the game should broadcast them. + options_saved: bool, bench_render_distance: crate::benchmark::BenchRenderDistance, /// The title screen's splash line, rolled at launch and on every return /// from a world like vanilla's fresh `TitleScreen`. `None` renders nothing. @@ -868,6 +870,7 @@ impl MainMenu { active_slider: None, settings_dir: game_dir.to_path_buf(), settings_dirty: false, + options_saved: false, bench_render_distance: Default::default(), splash: None, menu_open_time: None, @@ -900,7 +903,12 @@ impl MainMenu { } fn set_screen(&mut self, screen: Screen, key_bindings: &KeyBindings) { - self.flush_settings(key_bindings); + // Vanilla's options screens save as they're removed. + if self.is_options_screen() { + self.save_options(key_bindings); + } else { + self.flush_settings(key_bindings); + } // Vanilla opens Key Binds with a fresh list and returns to Controls's // own; neither should inherit the other's offset. if matches!(screen, Screen::OptionsKeybinds) @@ -1013,6 +1021,17 @@ impl MainMenu { } /// Writes pending slider changes, vanilla `OptionsSubScreen.removed()`. + /// Vanilla `Options.save`: writes the settings, then the game broadcasts + /// the client information. + pub(crate) fn save_options(&mut self, key_bindings: &KeyBindings) { + self.flush_settings(key_bindings); + self.options_saved = true; + } + + pub fn take_options_saved(&mut self) -> bool { + std::mem::take(&mut self.options_saved) + } + pub fn flush_settings(&mut self, key_bindings: &KeyBindings) { if self.settings_dirty { self.save_settings(key_bindings); @@ -1469,6 +1488,22 @@ fn test_menu(dir: &str, key_bindings: &mut KeyBindings) -> MainMenu { mod tests { use super::*; + #[test] + fn options_save_as_their_screen_closes() { + let dir = std::env::temp_dir().join(format!("pomme-options-save-{}", std::process::id())); + let mut key_bindings = KeyBindings::default(); + let mut menu = test_menu(dir.to_str().unwrap(), &mut key_bindings); + menu.open_options(&key_bindings); + menu.set_screen(Screen::OptionsSkinCustomization, &key_bindings); + assert!(menu.take_options_saved(), "leaving Options saves"); + menu.set_main_hand_right(false, &key_bindings); + assert!(!menu.take_options_saved(), "a change alone doesn't"); + menu.set_screen(Screen::Options, &key_bindings); + assert!(menu.take_options_saved()); + assert!(!menu.take_options_saved()); + std::fs::remove_dir_all(&dir).ok(); + } + #[test] fn a_chunk_bench_saves_the_players_render_distance_and_reads_their_chunk_detail() { let dir = std::env::temp_dir().join(format!("pomme-bench-rd-{}", std::process::id())); diff --git a/tests/ingame/README.md b/tests/ingame/README.md index 4ee149651..66cb53c7e 100644 --- a/tests/ingame/README.md +++ b/tests/ingame/README.md @@ -139,4 +139,4 @@ echo '{"cmd":"inventory"}' | nc 127.0.0.1 25599 The commands are `status`, `chat` (`text`), `open_inventory`, `close_menu`, `click` (`slot`, `op`), `creative_set_slot` (`slot`, `from`), `inventory`, `entities`, `advancements`, `player`, `hud`, `sounds`, `block` (`at`), `look_at` (`at`), -`mouse_down` and `mouse_up` (`button`), `press` (`action`, a key binding id like `key.drop`, or `key`, an input id like `key.keyboard.j`; optional `ctrl`), `bind` (`action`, optional `keys`), `option` (`main_hand`: `left` or `right`), `hover` (`slot` 36-44 or `catalog` cell, in a creative item tab), `chat_log` and `screenshot` (`path`). +`mouse_down` and `mouse_up` (`button`), `press` (`action`, a key binding id like `key.drop`, or `key`, an input id like `key.keyboard.j`; optional `ctrl`), `bind` (`action`, optional `keys`), `option` (`main_hand`: `left` or `right`; saved as closing its options screen would, so the server is told), `hover` (`slot` 36-44 or `catalog` cell, in a creative item tab), `chat_log` and `screenshot` (`path`).