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 <[email protected]>
This commit is contained in:
[email protected]andClaude Sonnet 5.5 committed 2026-11-01 17:16:10 +01:00
1 parent fcbfd000ee
commit 74a8a866f8
4 files changed
+70 -52

No files matched your search

+1
View File
@@ -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.
@@ -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<u32> = host
@@ -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;
+1 -1
View File
@@ -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")