From d0d3e68ec483bfb0be9fedebcc79d852f732dba5 Mon Sep 17 00:00:00 2001 From: Ludovic Marie <136255825+ludovic111@users.noreply.github.com> Date: Thu, 5 Nov 2026 18:38:01 +0100 Subject: [PATCH] SMP: the /vanish reminder sits on the action bar, not mid-screen (#298) rdbt (operator, #mod-chat 11-05): the centred 'Vanished: only operators see you' line covers the view. It is now an action bar line refreshed once a second; unvanishing sends an empty action bar instead of clearing titles. proof.py checks the action bar and that nothing shows mid-screen. Co-authored-by: Horizon agent Co-authored-by: Claude Opus 5.5 --- native/smp/README.md | 4 ++-- native/smp/mixins/smp/glue.rs | 18 +++++++----------- native/smp/proof.py | 21 ++++++++++++--------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/native/smp/README.md b/native/smp/README.md index 4bc442a..de952d3 100644 --- a/native/smp/README.md +++ b/native/smp/README.md @@ -117,8 +117,8 @@ the other operators on. Needs horizon#6106 (`native_inventory`, `native_move_sta **Vanish.** `/vanish` takes an operator of level 3 out of the Tab list and the world of every player below level 3, whatever the game mode (survival and creative too), and those players don't read them joining or leaving; operators of -level 3 or more still see them, and are told who vanished. A vanished operator reads a subtitle the whole time -(*Vanished: only operators see you*), isn't counted online (the sidebar, the Tab list's footer), doesn't hold the night up, isn't +level 3 or more still see them, and are told who vanished. A vanished operator reads a line above the hotbar the +whole time (*Vanished: only operators see you*), isn't counted online (the sidebar, the Tab list's footer), doesn't hold the night up, isn't announced as AFK or on a first join, and can't be reached by name by those who don't see them (`/tpa`, `/trade`, `/coinflip`, `/playtime`). It lasts over a relog and a restart (`vanish.json`) until `/vanish` again. Needs horizon#7003 (`native_hide_player`). diff --git a/native/smp/mixins/smp/glue.rs b/native/smp/mixins/smp/glue.rs index 8a3be49..f3ecbf7 100644 --- a/native/smp/mixins/smp/glue.rs +++ b/native/smp/mixins/smp/glue.rs @@ -2682,22 +2682,18 @@ pub(in crate::game) fn tick(game: &mut Game<'_>) { game.native_sleep_ignored(uuid, ignored); } } - // A vanished player doesn't hold the night up; the subtitle lasts a - // little past the next second's (1.5 s), so it never blinks. + // A vanished player doesn't hold the night up. The reminder is on the + // action bar, above the hotbar, not in the middle of the screen: sent once + // a second, it stays up (an action bar line lasts about 3 s), so it never blinks. for (uuid, name) in subtitles { game.native_sleep_ignored(uuid, true); - let subtitle = serde_json::json!({"text": smp_vanish::SUBTITLE, "color": "aqua"}); - for line in [ - format!("title {name} times 0 30 5"), - format!("title {name} subtitle {subtitle}"), - format!("title {name} title \"\""), - ] { - game.native_run_command(&line); - } + let line = serde_json::json!({"text": smp_vanish::SUBTITLE, "color": "aqua"}); + game.native_run_command(&format!("title {name} actionbar {line}")); } for (uuid, name) in unvanished { game.native_sleep_ignored(uuid, false); - game.native_run_command(&format!("title {name} clear")); + // An empty line takes the reminder down at once, and leaves any title alone. + game.native_run_command(&format!("title {name} actionbar \"\"")); } for line in particles.into_iter().chain(casino) { game.native_run_command(&line); diff --git a/native/smp/proof.py b/native/smp/proof.py index 364c0c5..c57c576 100644 --- a/native/smp/proof.py +++ b/native/smp/proof.py @@ -1068,27 +1068,30 @@ def trades(server, alex, steve, port, ctl, resources, root): def vanish(server, alex, steve, out): - """/vanish: Alex (an operator) leaves Steve's world and Tab list, and reads a subtitle the whole time; Steve can't + """/vanish: Alex (an operator) leaves Steve's world and Tab list, and reads a line above the hotbar the whole time; Steve can't reach her by name; /vanish again brings her back. Steve can't vanish.""" r = {} players = lambda c: [e for e in c.run("entities", 64, "player")["entities"]] - subtitle = lambda c: c.run("messages", 1).get("subtitle") or "" + actionbar = lambda c: c.run("messages", 1).get("actionbar") or "" + centre = lambda c: (c.run("messages", 1).get("subtitle") or "") + (c.run("messages", 1).get("title") or "") r["seen_before"] = len(players(steve)) r["refused"] = steve.say("/vanish") assert any("Unknown" in l or "Only operators" in l or "permission" in l for l in r["refused"]), r r["vanish"] = alex.say("/vanish") assert any("You vanished" in l for l in r["vanish"]), r time.sleep(2.5) - # Over a few seconds, not just once: the subtitle never lapses. - r["subtitles"] = [] + # Over a few seconds, not just once: the line never lapses. + r["actionbar"] = [] for _ in range(4): - r["subtitles"].append(subtitle(alex)) + r["actionbar"].append(actionbar(alex)) time.sleep(1) - assert all(s == "Vanished: only operators see you" for s in r["subtitles"]), r - assert "Vanished" not in subtitle(steve), r + assert all(s == "Vanished: only operators see you" for s in r["actionbar"]), r + # On the action bar only: nothing in the middle of the screen. + assert "Vanished" not in centre(alex), r + assert "Vanished" not in actionbar(steve), r if r["seen_before"]: assert not players(steve), "Steve no longer sees Alex" - alex.run("screenshot", str(Path(out).resolve() / "vanish-subtitle.png")) + alex.run("screenshot", str(Path(out).resolve() / "vanish-actionbar.png")) steve.run("key", "tab", "down") time.sleep(1) steve.run("screenshot", str(Path(out).resolve() / "vanish-tab.png")) @@ -1100,7 +1103,7 @@ def vanish(server, alex, steve, out): r["back"] = alex.say("/vanish") assert any("You are visible again." in l for l in r["back"]), r time.sleep(3) - assert "Vanished" not in subtitle(alex), r + assert "Vanished" not in actionbar(alex), r if r["seen_before"]: assert players(steve), "Steve sees Alex again" time.sleep(6) # the spam guard's window, for the steps after