Merge pull request #2970 from kacpherek/fix/multiplayer-safety-notice

Show the multiplayer safety notice on the first Multiplayer click
This commit is contained in:
levzzz authored and GitHub committed 2026-10-29 22:20:02 +05:00
commit 1b38710e92
7 files changed
+163 -7

No files matched your search

+2
View File
@@ -9716,6 +9716,7 @@ impl Game<'_> {
));
if let Some(title) = &mut self.title {
title.last_server = self.settings.file.string("lastServer");
title.skip_multiplayer_warning = self.settings.file.bool("skipMultiplayerWarning");
}
self.capture(false);
Ok(())
@@ -15421,6 +15422,7 @@ fn start_game<'a>(
&game.options.resources,
);
title.last_server = game.settings.file.string("lastServer");
title.skip_multiplayer_warning = game.settings.file.bool("skipMultiplayerWarning");
game.title = Some(title);
}
}
+3
View File
@@ -611,6 +611,9 @@ impl Game<'_> {
json!({ "mode": "multiplayer", "disconnected": reason })
}
Mode::Multiplayer(_) => json!({ "mode": "multiplayer" }),
Mode::MultiplayerWarning { check } => {
json!({ "mode": "multiplayer-warning", "check": check })
}
}
}
+7 -1
View File
@@ -19,12 +19,18 @@ pub(super) const CONTROL_HELP: &[&str] = &[
impl Game<'_> {
pub(super) fn save_title_options(&mut self) {
if let Some(title) = &self.title
&& title.last_server != self.settings.file.string("lastServer")
&& (title.last_server != self.settings.file.string("lastServer")
|| title.skip_multiplayer_warning
!= self.settings.file.bool("skipMultiplayerWarning"))
{
self.settings.file.set(
"lastServer",
horizon_client::options_file::V::Str(title.last_server.clone()),
);
self.settings.file.set(
"skipMultiplayerWarning",
horizon_client::options_file::V::Bool(title.skip_multiplayer_warning),
);
self.settings.save(&Self::options_path());
}
}
+3
View File
@@ -325,7 +325,9 @@ impl Game<'_> {
&self.options.resources,
));
}
let skip = self.settings.file.bool("skipMultiplayerWarning");
if let Some(title) = &mut self.title {
title.skip_multiplayer_warning = skip;
title.failed(error);
}
self.capture(false);
@@ -608,6 +610,7 @@ impl Game<'_> {
let saves = std::env::current_dir()?.join("saves");
let mut title = horizon_client::title::Title::new(saves.clone(), &self.options.resources);
title.last_server = self.settings.file.string("lastServer");
title.skip_multiplayer_warning = self.settings.file.bool("skipMultiplayerWarning");
if single_player && reason.is_none() {
self.title = Some(title);
self.capture(false);
+10 -6
View File
@@ -76,6 +76,7 @@ impl StartupMenu {
&self.root,
);
title.last_server = settings.file.string("lastServer");
title.skip_multiplayer_warning = settings.file.bool("skipMultiplayerWarning");
self.title = Some(title);
}
}
@@ -320,12 +321,11 @@ impl Loading {
}
fn save_menu_address(&mut self) {
if self
.menu
.title
.as_ref()
.is_some_and(|title| title.last_server != self.settings.file.string("lastServer"))
{
if self.menu.title.as_ref().is_some_and(|title| {
title.last_server != self.settings.file.string("lastServer")
|| title.skip_multiplayer_warning
!= self.settings.file.bool("skipMultiplayerWarning")
}) {
self.save_menu_settings();
}
}
@@ -336,6 +336,10 @@ impl Loading {
"lastServer",
horizon_client::options_file::V::Str(title.last_server.clone()),
);
self.settings.file.set(
"skipMultiplayerWarning",
horizon_client::options_file::V::Bool(title.skip_multiplayer_warning),
);
}
self.settings.save(&Game::options_path());
}
+133
View File
@@ -1,6 +1,7 @@
//! Title, world selection and world creation screens (TitleScreen,
//! SelectWorldScreen, CreateWorldScreen layouts), drawn as HUD quads.
use crate::{
dialog_screen::BOX,
game_rules_screen::{Exit, RulesScreen},
hud::{HudAtlas, HudBatch, rgba},
lang,
@@ -33,6 +34,12 @@ fn edit_top(h: f32) -> f32 {
pub(crate) fn confirm_top(h: f32, lines: usize) -> f32 {
((h.ceil() - 61.0 - lines as f32 * 9.0) / 2.0).floor()
}
/// The SafetyScreen's column (WarningScreen: title, 12 px around the message
/// box of its lines and 4 px padding, the checkbox, then the buttons, 8 px
/// apart): the title's top.
fn warning_top(h: f32, lines: usize) -> f32 {
((h.ceil() - 102.0 - lines as f32 * 9.0) / 2.0).floor()
}
fn translate(key: &str, fallback: &str, args: &[&dyn std::fmt::Display]) -> String {
let args: Vec<Text> = args
@@ -119,6 +126,11 @@ pub enum Mode {
return_to: Box<Mode>,
},
Multiplayer(crate::multiplayer::Screen),
/// SafetyScreen: the warning before the first Multiplayer click, with its
/// "Do not show this screen again" box.
MultiplayerWarning {
check: bool,
},
/// CreateFlatWorldScreen over Create New World: the fields that screen
/// keeps and the superflat editor on top of them.
CreateFlat {
@@ -160,6 +172,8 @@ pub struct Title {
pub mode: Mode,
/// Direct Connection's address, synchronized with `options.txt`'s `lastServer`.
pub last_server: String,
/// `skipMultiplayerWarning`, synchronized with `options.txt` the same way.
pub skip_multiplayer_warning: bool,
pub saves: PathBuf,
/// The prepared resource root: the flat preset list and block registry.
resources: PathBuf,
@@ -171,6 +185,8 @@ pub struct Title {
scroll: Cell<crate::ui::Scroll>,
create_return_to_main: bool,
message_lines: Cell<usize>,
/// The SafetyScreen's checkbox as last drawn: x, y, width, height.
warning_check: Cell<[f32; 4]>,
backup: Option<mpsc::Receiver<Result<u64, String>>>,
backup_shown: Cell<bool>,
/// The backup a `BackupBeforeUpgrade` asked for opens its world when done.
@@ -297,6 +313,7 @@ impl Title {
Self {
mode,
last_server: String::new(),
skip_multiplayer_warning: false,
saves,
resources: resources.to_owned(),
started: now,
@@ -306,6 +323,7 @@ impl Title {
scroll: Cell::default(),
create_return_to_main: false,
message_lines: Cell::new(1),
warning_check: Cell::new([0.0; 4]),
backup: None,
backup_shown: Cell::new(false),
backup_then_open: false,
@@ -1034,6 +1052,28 @@ impl Title {
),
]
}
Mode::MultiplayerWarning { .. } => {
let lines = self.message_lines.get();
let y = warning_top(h, lines) + 82.0 + lines as f32 * 9.0;
vec![
b(
(w / 2.0).floor() - 154.0,
y,
150.0,
&lang::get_or("gui.proceed", "Proceed"),
true,
70,
),
b(
(w / 2.0).floor() + 4.0,
y,
150.0,
&lang::get_or("gui.back", "Back"),
true,
71,
),
]
}
Mode::Backup { .. } => Vec::new(),
Mode::Multiplayer(screen) => screen
.buttons(w, h)
@@ -1365,6 +1405,74 @@ impl Title {
centered(atlas, batch, line, top + 17.0 + i as f32 * 9.0, u32::MAX);
}
}
Mode::MultiplayerWarning { check } => {
crate::ui::menu_background(batch, atlas, 0.0, 0.0, w, h, false);
let message = lang::get_or(
"multiplayerWarning.message",
"Caution: Online play is offered by third-party servers that are not owned, operated, or supervised by Mojang Studios or Microsoft. During online play, you may be exposed to unmoderated chat messages or other types of user-generated content that may not be suitable for everyone.",
);
// The message sits in a text box `width - 100` wide with 4 px
// inside, left aligned (FittingMultiLineTextWidget).
let box_w = w - 100.0;
let lines = wrapped(atlas, &message, box_w - 8.0);
self.message_lines.set(lines.len());
let top = warning_top(h, lines.len());
let header = Text::translate("multiplayerWarning.header", &[]).bold();
batch.text_styled(
atlas,
&header,
(w / 2.0).floor() - (atlas.font.width_text(&header) / 2.0).floor(),
top,
u32::MAX,
true,
);
let box_x = ((w - box_w) / 2.0).floor();
let box_y = top + 29.0;
let box_h = lines.len() as f32 * 9.0 + 8.0;
crate::ui::nine_slice(
batch,
atlas,
"gui/sprites/widget/text_field",
box_x,
box_y,
box_w,
box_h,
u32::MAX,
);
for (i, line) in lines.iter().enumerate() {
batch.text(
atlas,
line,
box_x + 4.0,
box_y + 4.0 + i as f32 * 9.0,
u32::MAX,
true,
);
}
// Checkbox: the box, its label right of it (as the dialogs draw theirs).
let label =
lang::get_or("multiplayerWarning.check", "Do not show this screen again");
let width = BOX + 4.0 + HudBatch::text_width(atlas, &label).ceil();
let x = (w / 2.0).floor() - (width / 2.0).floor();
let y = top + 57.0 + lines.len() as f32 * 9.0;
self.warning_check.set([x, y, width, BOX]);
let hover = (x..x + width).contains(&cursor.0) && (y..y + BOX).contains(&cursor.1);
let sprite = match (*check, hover) {
(true, true) => "gui/sprites/widget/checkbox_selected_highlighted",
(true, false) => "gui/sprites/widget/checkbox_selected",
(false, true) => "gui/sprites/widget/checkbox_highlighted",
(false, false) => "gui/sprites/widget/checkbox",
};
batch.sprite_scaled(atlas, sprite, x, y, BOX, BOX, u32::MAX);
batch.text(
atlas,
&label,
x + BOX + 4.0,
y + 4.0,
rgba(224, 224, 224, 255),
true,
);
}
Mode::Multiplayer(screen) => screen.draw(
atlas,
batch,
@@ -1471,6 +1579,14 @@ impl Title {
}
return (Action::None, changed);
}
// The SafetyScreen's box toggles where it was last drawn.
if let Mode::MultiplayerWarning { check } = &mut self.mode {
let [bx, by, bw, bh] = self.warning_check.get();
if (bx..bx + bw).contains(&x) && (by..by + bh).contains(&y) {
*check = !*check;
return (Action::None, true);
}
}
let hit = self
.buttons(w, h)
.into_iter()
@@ -1493,9 +1609,26 @@ impl Title {
}
(Mode::Main, Some(2)) => Action::Quit,
(Mode::Main, Some(4)) => {
// TitleScreen.init: SafetyScreen first, unless it was turned off.
self.mode = if self.skip_multiplayer_warning {
Mode::Multiplayer(crate::multiplayer::Screen::list(&self.saves))
} else {
Mode::MultiplayerWarning { check: false }
};
Action::None
}
(Mode::MultiplayerWarning { check }, Some(70)) => {
// SafetyScreen Proceed: the box saves the choice.
if *check {
self.skip_multiplayer_warning = true;
}
self.mode = Mode::Multiplayer(crate::multiplayer::Screen::list(&self.saves));
Action::None
}
(Mode::MultiplayerWarning { .. }, Some(71)) => {
self.mode = Mode::Main;
Action::None
}
(Mode::Multiplayer(_), hit) => match self.multiplayer_click(hit, x, y, (w, h)) {
crate::multiplayer::Outcome::Back => {
self.mode = Mode::Main;
+5
View File
@@ -88,6 +88,9 @@ fn direct_connection_remembers_the_address_on_cancel_escape_and_join() {
let mut f = Fixture::new();
f.title.last_server = "localhost:25566".into();
f.click("Multiplayer");
// SafetyScreen comes first while its box was never ticked.
assert!(matches!(f.title.mode, Mode::MultiplayerWarning { .. }));
f.click("Proceed");
f.click("Direct Connection");
assert_eq!(f.title.focused_field().unwrap().1, "localhost:25566");
@@ -100,6 +103,7 @@ fn direct_connection_remembers_the_address_on_cancel_escape_and_join() {
f.title.escape();
f.title.escape();
f.click("Multiplayer");
f.click("Proceed");
f.click("Direct Connection");
assert_eq!(f.title.focused_field().unwrap().1, "[::1]:25568");
@@ -122,6 +126,7 @@ fn direct_connection_remembers_the_address_on_cancel_escape_and_join() {
fn f5_reloads_the_server_list() {
let mut f = Fixture::new();
f.click("Multiplayer");
f.click("Proceed");
let file = crate::multiplayer::servers_file(&f.title.saves);
horizon_protocol::servers::save(
&file,