From 74a8a866f89f1a2257cfed6b3910fba03ace34e3 Mon Sep 17 00:00:00 2001 From: "lepacris08@gmail.com" Date: Sun, 1 Nov 2026 17:02:28 +0100 Subject: [PATCH] fix(mods): count the host's own player and golems as container viewers The count now runs in tick_world, so it also runs in-frame and without LAN. Fixes #6077 Co-Authored-By: Claude Sonnet 5.5 --- crates/horizon-client/src/game.rs | 1 + crates/horizon-client/src/game/lan_actions.rs | 51 -------------- .../horizon-client/src/game/native_hooks.rs | 68 +++++++++++++++++++ tests/test_mod_container_viewers.py | 2 +- 4 files changed, 70 insertions(+), 52 deletions(-) diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs index d0155af905..9775fa2b0e 100644 --- a/crates/horizon-client/src/game.rs +++ b/crates/horizon-client/src/game.rs @@ -7317,6 +7317,7 @@ impl Game<'_> { if running { horizon_client::spans::time("tick_mobs", || self.tick_mobs())?; horizon_client::spans::time("recheck_golem_openers", || self.recheck_golem_openers()); + self.tick_mod_container_viewers(); let before = self.orb_ids(); horizon_client::spans::time("tick_experience", || self.tick_experience()); // Merged, despawned or picked up. diff --git a/crates/horizon-client/src/game/lan_actions.rs b/crates/horizon-client/src/game/lan_actions.rs index 9631def1d7..4458586ebf 100644 --- a/crates/horizon-client/src/game/lan_actions.rs +++ b/crates/horizon-client/src/game/lan_actions.rs @@ -32,37 +32,8 @@ use horizon_protocol::{ text::literal, }; use horizon_world::entity_move::EntityMoveWorld; -use std::collections::HashMap; use winit::keyboard::KeyCode; -/// Sets the viewer counts `known` holds for `dim` to `now` and returns the -/// changes as `(pos, previous, viewers)`, in position order. A container that -/// is no longer counted goes to 0 and leaves `known`. -pub(super) fn viewer_changes( - known: &mut HashMap<(&'static str, (i32, i32, i32)), u32>, - dim: &'static str, - mut now: HashMap<(i32, i32, i32), u32>, -) -> Vec<((i32, i32, i32), u32, u32)> { - let mut changes = Vec::new(); - known.retain(|&(d, pos), previous| { - if d != dim { - return true; - } - let viewers = now.remove(&pos).unwrap_or(0); - if viewers != *previous { - changes.push((pos, *previous, viewers)); - *previous = viewers; - } - viewers > 0 - }); - for (pos, viewers) in now { - known.insert((dim, pos), viewers); - changes.push((pos, 0, viewers)); - } - changes.sort_unstable(); - changes -} - /// Per-guest state of the gameplay code that lives outside `Guest`. #[derive(Default)] pub(super) struct GuestPlay { @@ -1395,27 +1366,6 @@ impl Game<'_> { } } - /// `ContainerOpenersCounter` for the mod containers of the level in place: - /// the players with each one open, recounted from their menus, so a - /// change is heard whichever way a player left (`native_mod_container_viewers`). - fn tick_mod_container_viewers(&mut self, host: &LanHost) { - let mut now: HashMap<(i32, i32, i32), u32> = HashMap::new(); - for g in host.hub.guests.iter().filter(|g| host.hub.here(g)) { - // `BarrelBlockEntity.startOpen`: a spectator does not open it. - if !g.spectator() - && let Some(m) = &g.menu - && let Slots::Block(pos) = m.slots - && self.mod_menu_at(pos).is_some() - { - *now.entry(pos).or_default() += 1; - } - } - let dim = self.dim.id(); - for (pos, previous, viewers) in viewer_changes(&mut self.mod_container_viewers, dim, now) { - self.native_mod_container_viewers(dim, [pos.0, pos.1, pos.2], viewers, previous); - } - } - /// `stillValid` of the guest's open container: its blocks are still there /// and in reach, or its vehicle exists and is in reach. True with none open. fn container_menu_valid(&self, g: &horizon_client::lan::guest::Guest) -> bool { @@ -3305,7 +3255,6 @@ impl Game<'_> { self.lan_stations_tick(host); self.lan_tables_tick(host); self.lan_containers_tick(host); - self.tick_mod_container_viewers(host); self.tick_lan_mount_menus(host); self.announce_guest_advancements(host); let conns: Vec = host diff --git a/crates/horizon-client/src/game/native_hooks.rs b/crates/horizon-client/src/game/native_hooks.rs index 3139a5e293..c796185948 100644 --- a/crates/horizon-client/src/game/native_hooks.rs +++ b/crates/horizon-client/src/game/native_hooks.rs @@ -296,6 +296,7 @@ use horizon_client::{ hud::{HudBatch, rgba}, items::ItemStack, keybinds, + lan::menu::Slots, mobs::{Damage, Who}, mod_list::{self, Installed, NativePage}, music::{Music, MusicLayer}, @@ -315,6 +316,7 @@ use horizon_world::{ registry::{BlockRegistry, BlockStateId}, }; use std::{ + collections::HashMap, fs, path::{Path, PathBuf}, sync::Arc, @@ -2905,6 +2907,34 @@ fn sound_id(id: &str) -> bool { && path.chars().all(|c| allowed(c, true)) } +/// Sets the viewer counts `known` holds for `dim` to `now` and returns the +/// changes as `(pos, previous, viewers)`, in position order. A container that +/// is no longer counted goes to 0 and leaves `known`. +pub(super) fn viewer_changes( + known: &mut HashMap<(&'static str, (i32, i32, i32)), u32>, + dim: &'static str, + mut now: HashMap<(i32, i32, i32), u32>, +) -> Vec<((i32, i32, i32), u32, u32)> { + let mut changes = Vec::new(); + known.retain(|&(d, pos), previous| { + if d != dim { + return true; + } + let viewers = now.remove(&pos).unwrap_or(0); + if viewers != *previous { + changes.push((pos, *previous, viewers)); + *previous = viewers; + } + viewers > 0 + }); + for (pos, viewers) in now { + known.insert((dim, pos), viewers); + changes.push((pos, 0, viewers)); + } + changes.sort_unstable(); + changes +} + fn dimension_id(dimension: LevelDimension) -> &'static str { match dimension { LevelDimension::Overworld => "minecraft:overworld", @@ -4311,6 +4341,44 @@ impl Game<'_> { } true } + /// `ContainerOpenersCounter` for the mod containers of the level in place: + /// everyone with one open, recounted every tick, so a change is heard + /// whichever way an opener left (`native_mod_container_viewers`). The + /// openers are the guests (but not a spectator, `BarrelBlockEntity.startOpen`), + /// the host's own player, and the copper golems that opened it + /// (`chest_openers`). + pub(super) fn tick_mod_container_viewers(&mut self) { + let mut now: HashMap<(i32, i32, i32), u32> = HashMap::new(); + let mut open = |game: &Self, pos: (i32, i32, i32), n: u32| { + if n > 0 && game.mod_menu_at(pos).is_some() { + *now.entry(pos).or_default() += n; + } + }; + if let Some(host) = &self.lan { + for g in host + .hub + .guests + .iter() + .filter(|g| host.hub.here(g) && !g.spectator()) + { + if let Some(m) = &g.menu + && let Slots::Block(pos) = m.slots + { + open(self, pos, 1); + } + } + } + if let Some(pos) = self.container.filter(|_| !self.spectator()) { + open(self, pos, 1); + } + for (&pos, &(golems, _)) in &self.world.chest_openers { + open(self, pos, golems.max(0) as u32); + } + let dim = self.dim.id(); + for (pos, previous, viewers) in viewer_changes(&mut self.mod_container_viewers, dim, now) { + self.native_mod_container_viewers(dim, [pos.0, pos.1, pos.2], viewers, previous); + } + } /// The menu type of the mod container at `key`, if it has one. pub(super) fn mod_menu_at(&self, key: Pos) -> Option<&'static str> { let state = self.world.block(key.0, key.1, key.2).0; diff --git a/tests/test_mod_container_viewers.py b/tests/test_mod_container_viewers.py index f47fe8707b..0d9e48c10b 100644 --- a/tests/test_mod_container_viewers.py +++ b/tests/test_mod_container_viewers.py @@ -11,7 +11,7 @@ ROOT = Path(__file__).resolve().parents[1] class ModContainerViewersTests(unittest.TestCase): @unittest.skipUnless(shutil.which("rustc"), "requires Rust") def test_opens_second_viewer_close_and_leaving_without_closing(self): - source = (ROOT / "crates/horizon-client/src/game/lan_actions.rs").read_text(encoding="utf-8") + source = (ROOT / "crates/horizon-client/src/game/native_hooks.rs").read_text(encoding="utf-8") start = source.index("pub(super) fn viewer_changes(") end = source.index("\n}\n", start) + 3 function = source[start:end].replace("pub(super) fn", "fn")