mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Native mods: hide a player from lower-permission players (native_hide_player)
A Host helper for a vanish: the hub drops the player from the Tab lists of players below the level, the entity tracker stops pairing them with those players (as for a spectator), and their join and leave messages are skipped for them. Kept by UUID for a player who is off. Helper-only fragment. Closes #6996 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
26b9c0c9ba
commit
eacdc5b519
11 files changed
+234
-27
No files matched your search
@@ -3266,6 +3266,7 @@ impl Game<'_> {
|
||||
leash: None,
|
||||
spectator: false,
|
||||
watching: false,
|
||||
hidden: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ fn state(id: u32, kind: &str, position: [f64; 3], yaw: f32, on_ground: bool) ->
|
||||
leash: None,
|
||||
spectator: false,
|
||||
watching: false,
|
||||
hidden: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -450,6 +450,9 @@ impl Host for super::Game<'_> {
|
||||
) -> u32 {
|
||||
self.native_move_items(dimension, from, from_face, to, to_face, item, max)
|
||||
}
|
||||
fn hide_player(&mut self, uuid: u128, level: Option<u8>) -> bool {
|
||||
self.native_hide_player(uuid, level)
|
||||
}
|
||||
fn item_stack(&self, batch: &mut HudBatch, stack: &ItemStack, at: [f32; 2], size: f32) -> bool {
|
||||
self.native_item_stack(batch, stack, at, size)
|
||||
}
|
||||
|
||||
@@ -343,6 +343,26 @@ impl Game<'_> {
|
||||
}
|
||||
true
|
||||
}
|
||||
/// Hides that player (a vanish) from the players below permission
|
||||
/// `level`, or shows them again (None): from the next tick those players
|
||||
/// lose them from the Tab list and the world (their client stops
|
||||
/// tracking the player, as it does a spectator), and don't read their
|
||||
/// joining or leaving; players of `level` or more still see them, and
|
||||
/// a player made an operator meanwhile sees them again. By UUID: kept
|
||||
/// for a player who is off (who joins hidden), until a mod sets it again
|
||||
/// or the server stops. Works anywhere on a server, inside a player's
|
||||
/// hook too; false on a client and in a world nobody else can join.
|
||||
/// API @API:hide_player@.
|
||||
pub(super) fn native_hide_player(&mut self, uuid: u128, level: Option<u8>) -> bool {
|
||||
let Some(host) = &mut self.lan else {
|
||||
return false;
|
||||
};
|
||||
match level {
|
||||
Some(level) => host.hub.hidden.insert(uuid, level),
|
||||
None => host.hub.hidden.remove(&uuid),
|
||||
};
|
||||
true
|
||||
}
|
||||
/// Shows `nickname` in place of that player's name in chat (their
|
||||
/// messages, `/me`, `/msg`) and in the Tab list; None shows their name
|
||||
/// again. Hovering the shown name gives the real one, which stays in the
|
||||
|
||||
@@ -80,6 +80,9 @@ pub struct EntityState {
|
||||
/// `ServerPlayer.getCamera() != this`: a spectator watching through
|
||||
/// another entity is shown to nobody (`ServerPlayer.broadcastToPlayer`).
|
||||
pub watching: bool,
|
||||
/// A player a native mod hid (`native_hide_player`): only clients whose
|
||||
/// player has at least this permission level track it.
|
||||
pub hidden: Option<u8>,
|
||||
}
|
||||
|
||||
impl EntityState {
|
||||
@@ -107,6 +110,7 @@ impl EntityState {
|
||||
self.leash = None;
|
||||
self.spectator = false;
|
||||
self.watching = false;
|
||||
self.hidden = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +263,8 @@ struct ServerEntity {
|
||||
spectator: bool,
|
||||
/// Likewise for a spectator who watches through a camera.
|
||||
watching: bool,
|
||||
/// Likewise for a player a native mod hid.
|
||||
hidden: Option<u8>,
|
||||
}
|
||||
|
||||
/// SectionPos.of(entity).
|
||||
@@ -322,6 +328,7 @@ impl ServerEntity {
|
||||
section: section_of(e.position),
|
||||
spectator: e.spectator,
|
||||
watching: e.watching,
|
||||
hidden: e.hidden,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,6 +698,9 @@ pub struct Viewer<'a> {
|
||||
pub entity_id: i32,
|
||||
/// Its player is in spectator mode: it tracks the other spectators too.
|
||||
pub spectator: bool,
|
||||
/// Its player's permission level: players a native mod hid from lower
|
||||
/// levels are not tracked by it.
|
||||
pub permission: u8,
|
||||
/// It accepted a move since the last tick (ChunkMap.move): every pair
|
||||
/// of it is checked again.
|
||||
pub moved: bool,
|
||||
@@ -717,17 +727,20 @@ struct Probe {
|
||||
chunk: u32,
|
||||
spectator: bool,
|
||||
watching: bool,
|
||||
hidden: Option<u8>,
|
||||
}
|
||||
|
||||
/// `ServerPlayer.broadcastToPlayer` (`Entity`'s is `true`): to a spectator,
|
||||
/// a player is shown only while its camera is itself; to anyone else, a
|
||||
/// spectator is hidden.
|
||||
fn broadcast_to_player(viewer_spectator: bool, p: &Probe) -> bool {
|
||||
if viewer_spectator {
|
||||
/// spectator is hidden. A player a native mod hid is shown only to players
|
||||
/// of at least its level.
|
||||
fn broadcast_to_player(v: &Viewer, p: &Probe) -> bool {
|
||||
let shown = if v.spectator {
|
||||
!p.watching
|
||||
} else {
|
||||
!p.spectator
|
||||
}
|
||||
};
|
||||
shown && p.hidden.is_none_or(|level| v.permission >= level)
|
||||
}
|
||||
|
||||
/// TrackedEntity.updatePlayer: within the type's range (capped by the view
|
||||
@@ -740,7 +753,7 @@ fn broadcast_to_player(viewer_spectator: bool, p: &Probe) -> bool {
|
||||
fn sees(v: &Viewer, p: &Probe, chunks: &[(i32, i32)], held: &mut [u8]) -> bool {
|
||||
let range = f64::from(p.range.min(v.view_distance * 16));
|
||||
let (dx, dz) = (v.position[0] - p.x, v.position[2] - p.z);
|
||||
dx * dx + dz * dz <= range * range && broadcast_to_player(v.spectator, p) && {
|
||||
dx * dx + dz * dz <= range * range && broadcast_to_player(v, p) && {
|
||||
let held = &mut held[p.chunk as usize];
|
||||
if *held == 0 {
|
||||
*held = 1 + u8::from(v.chunks.contains(&chunks[p.chunk as usize]));
|
||||
@@ -840,10 +853,12 @@ impl Entities {
|
||||
|| section != s.section
|
||||
|| e.spectator != s.spectator
|
||||
|| e.watching != s.watching
|
||||
|| e.hidden != s.hidden
|
||||
{
|
||||
s.section = section;
|
||||
s.spectator = e.spectator;
|
||||
s.watching = e.watching;
|
||||
s.hidden = e.hidden;
|
||||
self.moved.push(index);
|
||||
self.is_moved[index] = true;
|
||||
}
|
||||
@@ -870,6 +885,7 @@ impl Entities {
|
||||
chunk,
|
||||
spectator: e.spectator,
|
||||
watching: e.watching,
|
||||
hidden: e.hidden,
|
||||
});
|
||||
}
|
||||
// TrackedEntity.getEffectiveRange: a vehicle is tracked as far as its
|
||||
@@ -1089,6 +1105,7 @@ mod tests {
|
||||
leash: None,
|
||||
spectator: false,
|
||||
watching: false,
|
||||
hidden: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1531,6 +1548,7 @@ mod tests {
|
||||
positions: [[f64; 3]; 2],
|
||||
moved: [bool; 2],
|
||||
spectators: [bool; 2],
|
||||
permissions: [u8; 2],
|
||||
/// The first client's entities gone, where they were last sent.
|
||||
gone: Vec<[f64; 3]>,
|
||||
}
|
||||
@@ -1544,6 +1562,7 @@ mod tests {
|
||||
positions: [[0.0, 64.0, 0.0]; 2],
|
||||
moved: [false; 2],
|
||||
spectators: [false; 2],
|
||||
permissions: [0; 2],
|
||||
gone: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1557,6 +1576,7 @@ mod tests {
|
||||
Viewer {
|
||||
entity_id: 1,
|
||||
spectator: self.spectators[0],
|
||||
permission: self.permissions[0],
|
||||
moved: self.moved[0],
|
||||
position: self.positions[0],
|
||||
view_distance: 10,
|
||||
@@ -1568,6 +1588,7 @@ mod tests {
|
||||
Viewer {
|
||||
entity_id: 2,
|
||||
spectator: self.spectators[1],
|
||||
permission: self.permissions[1],
|
||||
moved: self.moved[1],
|
||||
position: self.positions[1],
|
||||
view_distance: 10,
|
||||
@@ -1782,6 +1803,37 @@ mod tests {
|
||||
assert!(c.trackers[0].is_tracking(2));
|
||||
}
|
||||
|
||||
/// A player a native mod hid is tracked only by clients whose player has
|
||||
/// at least its level, and by everyone again once shown.
|
||||
#[test]
|
||||
fn a_hidden_player_is_tracked_only_from_its_level_up() {
|
||||
let mut level = Entities::default();
|
||||
let mut c = Clients::new();
|
||||
c.permissions = [0, 3];
|
||||
let mut op = state(0.0);
|
||||
(op.id, op.uuid, op.kind) = (9, 9, "minecraft:player".into());
|
||||
op.hidden = Some(3);
|
||||
c.tick(&mut level, &[&op]);
|
||||
c.tick(&mut level, &[&op]);
|
||||
assert!(
|
||||
!c.trackers[0].is_tracking(9),
|
||||
"a player of level 0 doesn't see it"
|
||||
);
|
||||
assert!(c.trackers[1].is_tracking(9), "an operator of level 3 does");
|
||||
// Hidden after it was seen: the client below the level loses it.
|
||||
op.hidden = None;
|
||||
c.tick(&mut level, &[&op]);
|
||||
assert!(c.trackers[0].is_tracking(9));
|
||||
op.hidden = Some(4);
|
||||
c.tick(&mut level, &[&op]);
|
||||
assert!(!c.trackers[0].is_tracking(9) && !c.trackers[1].is_tracking(9));
|
||||
// A client whose player was made an operator (its pairs checked again).
|
||||
c.permissions = [4, 3];
|
||||
c.moved = [true, false];
|
||||
c.tick(&mut level, &[&op]);
|
||||
assert!(c.trackers[0].is_tracking(9) && !c.trackers[1].is_tracking(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pairing_and_unpairing_are_kept_for_the_game() {
|
||||
let mut level = Entities::default();
|
||||
|
||||
@@ -184,6 +184,9 @@ pub struct Guest {
|
||||
/// It accepted a move since the last entity range checks
|
||||
/// (ServerChunkCache.move), which then check all of its pairs again.
|
||||
pub moved: bool,
|
||||
/// The players a native mod hid from it (`Hub::hidden`) that its player
|
||||
/// list no longer has.
|
||||
pub tab_hidden: crate::fast_hash::FxHashSet<u128>,
|
||||
/// The client finished loading terrain (ServerboundPlayerLoadedPacket).
|
||||
pub loaded: bool,
|
||||
/// Since it joined or respawned, the client hasn't said it loaded the level
|
||||
@@ -404,6 +407,7 @@ impl Guest {
|
||||
replay: None,
|
||||
changes: Default::default(),
|
||||
moved: false,
|
||||
tab_hidden: Default::default(),
|
||||
loaded: false,
|
||||
loading: true,
|
||||
load_timeout: CLIENT_LOADED_TIMEOUT,
|
||||
|
||||
@@ -234,6 +234,10 @@ pub struct Hub {
|
||||
/// The entities players started and stopped tracking, collected by
|
||||
/// `collect_tracking`, for the game to tell the mods.
|
||||
pub native_tracking: Vec<TrackingBatch>,
|
||||
/// Players a native mod hid (`native_hide_player`), by UUID, on or off:
|
||||
/// only players of at least that permission level see them, in the
|
||||
/// player list, the world and their joining and leaving.
|
||||
pub hidden: crate::fast_hash::FxHashMap<u128, u8>,
|
||||
pub lan: Lan,
|
||||
pub config: HubConfig,
|
||||
pub encoder: ChunkEncoder,
|
||||
@@ -576,6 +580,7 @@ impl Hub {
|
||||
mod_commands: (serde_json::Value::Null, None),
|
||||
native_menu_events: Vec::new(),
|
||||
native_tracking: Vec::new(),
|
||||
hidden: Default::default(),
|
||||
teams: Vec::new(),
|
||||
nicknames: std::collections::HashMap::new(),
|
||||
}
|
||||
@@ -846,6 +851,72 @@ impl Hub {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a player of `permission` sees the player `uuid`: not one a
|
||||
/// native mod hid from players below a higher level.
|
||||
pub fn sees(&self, permission: u8, uuid: u128) -> bool {
|
||||
self.hidden
|
||||
.get(&uuid)
|
||||
.is_none_or(|&level| permission >= level)
|
||||
}
|
||||
|
||||
/// The player lists after `hidden` or a permission changed: each placed
|
||||
/// player loses the players hidden from it (ClientboundPlayerInfoRemove)
|
||||
/// and gets back those it sees again (an ADD_PLAYER entry). A change
|
||||
/// checks the client's entity pairs again, so the tracker follows too.
|
||||
/// Nothing runs while nobody is hidden.
|
||||
fn sync_hidden(&mut self) {
|
||||
if self.hidden.is_empty() && self.guests.iter().all(|g| g.tab_hidden.is_empty()) {
|
||||
return;
|
||||
}
|
||||
let candidates: Vec<usize> = (0..self.guests.len())
|
||||
.filter(|&j| {
|
||||
let u = self.guests[j].uuid;
|
||||
self.guests[j].placed
|
||||
&& (self.hidden.contains_key(&u)
|
||||
|| self.guests.iter().any(|g| g.tab_hidden.contains(&u)))
|
||||
})
|
||||
.collect();
|
||||
for i in 0..self.guests.len() {
|
||||
if !self.guests[i].placed {
|
||||
continue;
|
||||
}
|
||||
for &j in &candidates {
|
||||
let (viewer, other) = (&self.guests[i], &self.guests[j]);
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
let hide = !self.sees(viewer.permission, other.uuid);
|
||||
if hide == viewer.tab_hidden.contains(&other.uuid) {
|
||||
continue;
|
||||
}
|
||||
let packet = if hide {
|
||||
C::PlayerInfoRemove(play::PlayerInfoRemove {
|
||||
uuids: vec![other.uuid],
|
||||
})
|
||||
} else {
|
||||
C::PlayerInfoUpdate(play::PlayerInfoUpdate {
|
||||
actions: Self::INFO_ACTIONS,
|
||||
entries: vec![self.player_info(
|
||||
other.uuid,
|
||||
&other.name,
|
||||
other.game_mode,
|
||||
other.latency.load(Ordering::Relaxed),
|
||||
)],
|
||||
})
|
||||
};
|
||||
let uuid = other.uuid;
|
||||
let viewer = &mut self.guests[i];
|
||||
viewer.send(&packet);
|
||||
if hide {
|
||||
viewer.tab_hidden.insert(uuid);
|
||||
} else {
|
||||
viewer.tab_hidden.remove(&uuid);
|
||||
}
|
||||
viewer.moved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn player_info(&self, uuid: u128, name: &str, mode: i32, latency: i32) -> play::PlayerInfo {
|
||||
let guest = self.guests.iter().find(|g| g.uuid == uuid);
|
||||
play::PlayerInfo {
|
||||
@@ -985,17 +1056,21 @@ impl Hub {
|
||||
self.send_permission_level(i);
|
||||
self.send_initial_recipe_book(i);
|
||||
// PlayerList.placeNewPlayer broadcasts the join line before
|
||||
// players.add: the others read it, the newcomer doesn't.
|
||||
self.broadcast_except(
|
||||
conn,
|
||||
&C::SystemChat(play::SystemChat {
|
||||
content: yellow_names(
|
||||
"multiplayer.player.joined",
|
||||
vec![display_name(self.guests[i].uuid, &name)],
|
||||
),
|
||||
overlay: false,
|
||||
}),
|
||||
);
|
||||
// players.add: the others read it, the newcomer doesn't (nor those
|
||||
// a native mod hides it from).
|
||||
let joined = C::SystemChat(play::SystemChat {
|
||||
content: yellow_names(
|
||||
"multiplayer.player.joined",
|
||||
vec![display_name(self.guests[i].uuid, &name)],
|
||||
),
|
||||
overlay: false,
|
||||
})
|
||||
.encode();
|
||||
for g in self.guests.iter().filter(|g| g.placed && g.conn != conn) {
|
||||
if self.sees(g.permission, uuid) {
|
||||
g.sender.send(joined.clone());
|
||||
}
|
||||
}
|
||||
self.guests[i].placed = true;
|
||||
self.resend_teleport(i, None);
|
||||
let g = &self.guests[i];
|
||||
@@ -1008,7 +1083,15 @@ impl Hub {
|
||||
if let Some(host) = ctx.host {
|
||||
entries.push(self.player_info(host.uuid, &host.name, host.game_mode, 0));
|
||||
}
|
||||
// Players a native mod hid from the newcomer stay out of its list,
|
||||
// and a hidden newcomer out of theirs.
|
||||
let permission = self.guests[i].permission;
|
||||
let mut hidden = Vec::new();
|
||||
for o in self.guests.iter().filter(|o| o.conn != conn && o.placed) {
|
||||
if !self.sees(permission, o.uuid) {
|
||||
hidden.push(o.uuid);
|
||||
continue;
|
||||
}
|
||||
entries.push(self.player_info(
|
||||
o.uuid,
|
||||
&o.name,
|
||||
@@ -1016,15 +1099,24 @@ impl Hub {
|
||||
o.latency.load(Ordering::Relaxed),
|
||||
));
|
||||
}
|
||||
self.guests[i].tab_hidden.extend(hidden);
|
||||
self.guests[i].send(&C::PlayerInfoUpdate(play::PlayerInfoUpdate {
|
||||
actions: Self::INFO_ACTIONS,
|
||||
entries,
|
||||
}));
|
||||
let newcomer = play::PlayerInfoUpdate {
|
||||
let newcomer = C::PlayerInfoUpdate(play::PlayerInfoUpdate {
|
||||
actions: Self::INFO_ACTIONS,
|
||||
entries: vec![self.player_info(uuid, &name, mode, 0)],
|
||||
};
|
||||
self.broadcast(&C::PlayerInfoUpdate(newcomer));
|
||||
})
|
||||
.encode();
|
||||
let level = self.hidden.get(&uuid).copied();
|
||||
for g in self.guests.iter_mut().filter(|g| g.placed) {
|
||||
if level.is_some_and(|l| g.permission < l) {
|
||||
g.tab_hidden.insert(uuid);
|
||||
} else {
|
||||
g.sender.send(newcomer.clone());
|
||||
}
|
||||
}
|
||||
self.send_level_info(i, ctx, true);
|
||||
// ServerLevel.addNewPlayer: the chunk tracking starts at the player.
|
||||
let g = &mut self.guests[i];
|
||||
@@ -1600,6 +1692,7 @@ impl Hub {
|
||||
}
|
||||
}
|
||||
self.sync_containers(ctx.world);
|
||||
self.sync_hidden();
|
||||
self.track_entities(ctx);
|
||||
for i in 0..self.guests.len() {
|
||||
if self.here(&self.guests[i]) {
|
||||
@@ -1674,10 +1767,20 @@ impl Hub {
|
||||
self.broadcast(&C::RemoveEntities(play::RemoveEntities {
|
||||
ids: vec![g.entity_id],
|
||||
}));
|
||||
self.system_message(yellow_names(
|
||||
"multiplayer.player.left",
|
||||
vec![display_name(g.uuid, &g.name)],
|
||||
));
|
||||
let left = C::SystemChat(play::SystemChat {
|
||||
content: yellow_names(
|
||||
"multiplayer.player.left",
|
||||
vec![display_name(g.uuid, &g.name)],
|
||||
),
|
||||
overlay: false,
|
||||
})
|
||||
.encode();
|
||||
for o in self.guests.iter_mut().filter(|o| o.placed) {
|
||||
if o.tab_hidden.remove(&g.uuid) {
|
||||
continue;
|
||||
}
|
||||
o.sender.send(left.clone());
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
@@ -2329,6 +2432,7 @@ impl Hub {
|
||||
leash: None,
|
||||
spectator: false,
|
||||
watching: false,
|
||||
hidden: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2424,6 +2528,7 @@ impl Hub {
|
||||
);
|
||||
state.spectator = g.spectator();
|
||||
state.watching = g.watching;
|
||||
state.hidden = self.hidden.get(&g.uuid).copied();
|
||||
stuck_meta(&mut state, g.stuck);
|
||||
shoulder_meta(&mut state, g.shoulders);
|
||||
state
|
||||
@@ -2452,6 +2557,7 @@ impl Hub {
|
||||
.map(|(g, (out, gone))| Viewer {
|
||||
entity_id: g.entity_id,
|
||||
spectator: g.spectator(),
|
||||
permission: g.permission,
|
||||
moved: std::mem::take(&mut g.moved),
|
||||
position: g.position,
|
||||
view_distance: g.view_distance,
|
||||
|
||||
+3
-4
@@ -2013,10 +2013,9 @@ pub trait Hooks: Send + Sync {
|
||||
/// does not happen: no damage, no knockback, no sound, no block reaction, the
|
||||
/// arrow does not stick, a pearl does not teleport, a potion or a fireball
|
||||
/// does not burst. The projectile flies on and is not discarded, so a mod
|
||||
/// that cancels also decides what becomes of it: it hits again the next tick
|
||||
/// unless the mod moves, turns or removes it. An arrow stays where it would
|
||||
/// have hit, and the other projectiles go on through the whole of their
|
||||
/// motion. Covers what `tick_projectiles` flies: arrows, tridents, snowballs,
|
||||
/// that cancels also decides what becomes of it. It continues through its
|
||||
/// whole motion and may hit something on a later tick. Covers what
|
||||
/// `tick_projectiles` flies: arrows, tridents, snowballs,
|
||||
/// eggs, pearls, potions, fireballs, wither skulls, wind charges, llama spit and
|
||||
/// shulker bullets, from players, mobs and dispensers. It does not fire for
|
||||
/// a breeze's deflection, for a hit on a wind charge, for fireworks or for
|
||||
|
||||
+11
@@ -664,6 +664,17 @@ pub trait Host {
|
||||
item: Option<&str>,
|
||||
max: u32,
|
||||
) -> u32;
|
||||
/// Hides that player (a vanish) from the players below permission
|
||||
/// `level`, or shows them again (None): from the next tick those players
|
||||
/// lose them from the Tab list and the world (their client stops
|
||||
/// tracking the player, as it does a spectator), and don't read their
|
||||
/// joining or leaving; players of `level` or more still see them, and
|
||||
/// a player made an operator meanwhile sees them again. By UUID: kept
|
||||
/// for a player who is off (who joins hidden), until a mod sets it again
|
||||
/// or the server stops. Works anywhere on a server, inside a player's
|
||||
/// hook too; false on a client and in a world nobody else can join.
|
||||
/// API @API:hide_player@.
|
||||
fn hide_player(&mut self, uuid: u128, level: Option<u8>) -> bool;
|
||||
/// Draw a stack and its slot decorations, except the count, at `at`,
|
||||
/// `size` GUI pixels square. False for an unknown item. API @API:item_stack@.
|
||||
fn item_stack(&self, batch: &mut HudBatch, stack: &ItemStack, at: [f32; 2], size: f32) -> bool;
|
||||
|
||||
@@ -1042,6 +1042,7 @@ fn guests_see_each_other_and_the_world_entities() {
|
||||
leash: None,
|
||||
spectator: false,
|
||||
watching: false,
|
||||
hidden: None,
|
||||
};
|
||||
let first = thread::spawn(move || {
|
||||
let mut bot = connect(port, "Alex", false)?;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"hooks": [],
|
||||
"helpers": [
|
||||
{"name": "hide_player", "target": "horizon_client::game::native_server::Game<'_>::native_hide_player"}
|
||||
],
|
||||
"rust_summary": "hiding a player from the players below a permission level (`native_hide_player`).",
|
||||
"doc_summary": "hiding a player (a vanish): `native_hide_player(uuid, level)` hides that player, from the next tick, from the players below permission `level` (None shows them again): they leave those players' Tab list and world (their clients stop tracking them, as they do a spectator) and those players don't read them joining or leaving. Players of `level` or more still see them; a player made an operator sees them again. It is kept by UUID, for a player who is off too, until a mod sets it again or the server stops. Server only; false on a client. Bundles need \"hooks\": @API:hide_player@.",
|
||||
"comments": ["crates/horizon-client/src/game/native_server.rs"]
|
||||
}
|
||||
Reference in new issue
Block a user