F3+I copies the server's data on servers and LAN

On a connection (a server, a LAN world, and single player, whose world
runs on its own server thread) F3+I copied the client's partial copy of
the block or mob and called it server-side: a chest lost its items, a mob
its tags and brain. Vanilla's KeyboardHandler.copyRecreateCommand asks the
server through DebugQueryHandler (ServerboundBlockEntityTagQueryPacket,
ServerboundEntityTagQueryPacket) and copies the ClientboundTagQueryPacket
answer to the newest query.

Add the three packets, the query state on the connection and its answer,
and the server side for guests (ServerGamePacketListenerImpl
.handleBlockEntityTagQuery and handleEntityTagQuery, for game masters
only). Part of #3520 (its handleBlockEntityTagQuery and
handleEntityTagQuery rows).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
d3grandoandClaude Opus 5.5 committed 2026-10-30 14:05:51 +07:00
1 parent b1dfde280b
commit ef024a082c
9 files changed
+350 -38

No files matched your search

@@ -719,6 +719,27 @@ impl Game<'_> {
});
dims
}
/// `Level.getEntity`: the entity of the level in place with a network
/// id, players aside.
pub(in crate::game) fn level_entity(&self, id: i32) -> Option<Target> {
let id = u32::try_from(id).ok()?;
[
Target::Mob(id),
Target::Item(id),
Target::Orb(id),
Target::Projectile(id),
Target::Stand(id),
Target::Frame(id),
Target::Painting(id),
Target::Boat(id),
Target::Cart(id),
Target::Other(id),
Target::Crystal(id),
Target::Dragon(id),
]
.into_iter()
.find(|t| self.level_holds(*t))
}
/// Whether the level in place holds the entity a target names.
fn level_holds(&self, t: Target) -> bool {
match t {
+196 -28
View File
@@ -14,6 +14,8 @@ use horizon_client::{
text::Text,
};
use horizon_core::geometry::{Axis, Vec3};
use horizon_protocol::packets::play::{BlockEntityTagQuery, EntityTagQuery, Serverbound};
use horizon_world::nbt::Tag;
use winit::keyboard::KeyCode;
/// `ChatFormatting.YELLOW` and `RED`.
@@ -217,7 +219,7 @@ impl Game<'_> {
self.debug_charts.log_packets([sent, received]);
}
if shown {
use horizon_protocol::packets::{play::Serverbound, status::Ping};
use horizon_protocol::packets::status::Ping;
let time = self.debug_millis();
self.send(Serverbound::PingRequest(Ping { time }));
}
@@ -682,7 +684,7 @@ impl Game<'_> {
/// `GameModeCommand.setGameMode` with its feedback.
pub(super) fn change_game_mode(&mut self, mode: GameMode) {
if self.remote.is_some() {
use horizon_protocol::packets::play::{ChangeGameMode, Serverbound};
use horizon_protocol::packets::play::ChangeGameMode;
let mode = match mode {
GameMode::Survival => 0,
GameMode::Creative => 1,
@@ -698,10 +700,11 @@ impl Game<'_> {
}
}
/// `KeyboardHandler.copyRecreateCommand`: the targeted block or mob as a
/// `/setblock` or `/summon` command. In a single-player world the
/// server's data (no Shift) is the game's own.
/// `/setblock` or `/summon` command. The server's data (no Shift) comes
/// from the server the game is connected to, the integrated one included
/// (`DebugQueryHandler`); without one, the game's own data is the server's.
fn copy_recreate_command(&mut self, add_nbt: bool, from_server: bool) {
use horizon_client::java_text::format_fixed2 as f2;
let query = add_nbt && from_server && self.remote.is_some();
let side = if add_nbt && from_server {
"server"
} else {
@@ -710,45 +713,135 @@ impl Game<'_> {
if self.entity_targeted()
&& let Some((index, _)) = self.mob_hit()
{
let (id, pos, mob) = {
let (kind, position, id) = {
let m = &self.mobs[index];
(m.kind.id.clone(), m.position, m.id)
};
let mut command = format!("/summon {id} {} {} {}", f2(pos.x), f2(pos.y), f2(pos.z));
if add_nbt && let Some(mut tag) = self.entity_nbt(super::commands::Target::Mob(mob)) {
if let horizon_world::nbt::Tag::Compound(c) = &mut tag {
for key in ["id", "UUID", "Pos"] {
c.remove(&key.into());
}
if query {
// DebugQueryHandler.queryEntityTag: the answer copies it.
let reply = QueryReply::Entity { id, kind, position };
if let Some(transaction) = self.remote.as_mut().map(|r| r.debug_query.start(reply))
{
self.send(Serverbound::EntityTagQuery(EntityTagQuery {
transaction,
entity: id as i32,
}));
}
command.push(' ');
command.push_str(&horizon_client::snbt::pretty(&tag).plain());
return;
}
self.set_clipboard(&command);
// Entity.saveWithoutId.
let tag = if add_nbt {
self.entity_nbt(super::commands::Target::Mob(id))
.map(|mut tag| {
if let Tag::Compound(c) = &mut tag {
c.remove(&"id".into());
}
tag
})
} else {
None
};
self.copy_create_entity_command(&kind, position, tag);
self.debug_translated(&format!("debug.inspect.{side}.entity"));
return;
}
let Some(hit) = self.pick() else {
return;
};
let p = hit.pos;
let mut description = self.state_string(self.world.block(p[0], p[1], p[2]));
if add_nbt && let Some(e) = self.world.block_entities.get(&(p[0], p[1], p[2])) {
// BlockEntity.saveWithoutMetadata: no id or position.
let name = self.data_block_name(p);
let mut tag = horizon_client::save::encode_block_entity((p[0], p[1], p[2]), e, &name);
if let horizon_world::nbt::Tag::Compound(c) = &mut tag {
for key in ["id", "x", "y", "z", "keepPacked"] {
c.remove(&key.into());
}
let pos = hit.pos;
let state = self.state_string(self.world.block(pos[0], pos[1], pos[2]));
if query {
// DebugQueryHandler.queryBlockEntityTag: the answer copies it.
let reply = QueryReply::Block { pos, state };
if let Some(transaction) = self.remote.as_mut().map(|r| r.debug_query.start(reply)) {
self.send(Serverbound::BlockEntityTagQuery(BlockEntityTagQuery {
transaction,
pos,
}));
}
description.push_str(&horizon_client::snbt::to_snbt(&tag));
return;
}
let tag = if add_nbt {
self.block_entity_tag(pos)
} else {
None
};
self.copy_create_block_command(pos, &state, tag.as_ref());
self.debug_translated(&format!("debug.inspect.{side}.block"));
}
/// `BlockEntity.saveWithoutMetadata` of the block entity at `pos`, if
/// there is one: its data without the id or the position.
pub(super) fn block_entity_tag(&self, pos: [i32; 3]) -> Option<Tag> {
let e = self.world.block_entities.get(&(pos[0], pos[1], pos[2]))?;
let name = self.data_block_name(pos);
let mut tag = horizon_client::save::encode_block_entity((pos[0], pos[1], pos[2]), e, &name);
if let Tag::Compound(c) = &mut tag {
for key in ["id", "x", "y", "z", "keepPacked"] {
c.remove(&key.into());
}
}
Some(tag)
}
/// `ClientPacketListener.handleTagQueryPacket`: the server's answer to
/// the last debug query (`DebugQueryHandler.handleResponse`) completes
/// it; an answer to an older query is ignored.
pub(super) fn answer_tag_query(&mut self, transaction: i32, tag: Tag) {
let Some(reply) = self
.remote
.as_mut()
.and_then(|r| r.debug_query.answer(transaction))
else {
return;
};
// FriendlyByteBuf.readNbt: an End tag is no compound.
let tag = match tag {
Tag::End => None,
tag => Some(tag),
};
match reply {
QueryReply::Block { pos, state } => {
self.copy_create_block_command(pos, &state, tag.as_ref());
self.debug_translated("debug.inspect.server.block");
}
QueryReply::Entity { id, kind, position } => {
// Entity.position() when the answer comes: where it is now,
// else where it was last.
let position = self
.mobs
.iter()
.find(|m| m.id == id)
.map_or(position, |m| m.position);
self.copy_create_entity_command(&kind, position, tag);
self.debug_translated("debug.inspect.server.entity");
}
}
}
/// `KeyboardHandler.copyCreateBlockCommand`.
fn copy_create_block_command(&mut self, pos: [i32; 3], state: &str, tag: Option<&Tag>) {
let mut description = state.to_owned();
if let Some(tag) = tag {
description.push_str(&horizon_client::snbt::to_snbt(tag));
}
self.set_clipboard(&format!(
"/setblock {} {} {} {description}",
p[0], p[1], p[2]
pos[0], pos[1], pos[2]
));
self.debug_translated(&format!("debug.inspect.{side}.block"));
}
/// `KeyboardHandler.copyCreateEntityCommand`: the data without its UUID
/// or position.
fn copy_create_entity_command(&mut self, kind: &str, pos: Vec3, tag: Option<Tag>) {
use horizon_client::java_text::format_fixed2 as f2;
let mut command = format!("/summon {kind} {} {} {}", f2(pos.x), f2(pos.y), f2(pos.z));
if let Some(mut tag) = tag {
if let Tag::Compound(c) = &mut tag {
for key in ["UUID", "Pos"] {
c.remove(&key.into());
}
}
command.push(' ');
command.push_str(&horizon_client::snbt::pretty(&tag).plain());
}
self.set_clipboard(&command);
}
/// `KeyboardHandler.setClipboard`.
fn set_clipboard(&mut self, text: &str) {
@@ -1010,3 +1103,78 @@ fn step_view(step: [i32; 3]) -> (f32, f32) {
(-x.atan2(z)).to_degrees() as f32,
)
}
/// `DebugQueryHandler`: the last debug NBT query (F3+I) sent to the server
/// and what its answer completes. Only the newest query is answered.
#[derive(Debug)]
pub(super) struct DebugQuery {
transaction: i32,
pending: Option<QueryReply>,
}
impl Default for DebugQuery {
fn default() -> Self {
Self {
transaction: -1,
pending: None,
}
}
}
/// What a debug query's answer is copied into: the block command
/// (`copyCreateBlockCommand`) or the entity's (`copyCreateEntityCommand`).
#[derive(Clone, Debug, PartialEq)]
pub(super) enum QueryReply {
Block {
pos: [i32; 3],
state: String,
},
Entity {
id: u32,
kind: String,
position: Vec3,
},
}
impl DebugQuery {
/// `DebugQueryHandler.startTransaction`: the next transaction id, which
/// replaces any query still waiting.
fn start(&mut self, reply: QueryReply) -> i32 {
self.pending = Some(reply);
self.transaction = self.transaction.wrapping_add(1);
self.transaction
}
/// `DebugQueryHandler.handleResponse`: what the answer to `transaction`
/// completes, once, when it answers the newest query.
fn answer(&mut self, transaction: i32) -> Option<QueryReply> {
if transaction == self.transaction {
self.pending.take()
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn block(x: i32) -> QueryReply {
QueryReply::Block {
pos: [x, 64, 0],
state: "minecraft:chest[facing=north,type=single,waterlogged=false]".into(),
}
}
#[test]
fn a_debug_query_is_answered_once_and_only_the_newest() {
let mut q = DebugQuery::default();
// The first transaction is 0 (the handler starts at -1).
assert_eq!(q.start(block(1)), 0);
assert_eq!(q.answer(0), Some(block(1)));
assert_eq!(q.answer(0), None);
// A second press before the first answer: the older answer is ignored
// and the newer one copies the newer target.
assert_eq!(q.start(block(2)), 1);
assert_eq!(q.start(block(3)), 2);
assert_eq!(q.answer(1), None);
assert_eq!(q.answer(2), Some(block(3)));
}
}
@@ -1578,6 +1578,33 @@ impl Game<'_> {
self.send_admin_feedback(host, Some(conn));
}
}
// handleEntityTagQuery: a game master's F3+I on an entity of their
// level gets its Entity.saveWithoutId.
S::EntityTagQuery(p) if host.hub.guests[gi].permission >= 2 => {
if let Some(tag) = self.level_entity_tag(host, p.entity) {
host.hub.send_to(
conn,
&C::TagQuery(play::TagQuery {
transaction: p.transaction,
tag,
}),
);
}
}
// handleBlockEntityTagQuery: BlockEntity.saveWithoutMetadata, or
// null where there is no block entity.
S::BlockEntityTagQuery(p) if host.hub.guests[gi].permission >= 2 => {
let tag = self
.block_entity_tag(p.pos)
.unwrap_or(horizon_world::nbt::Tag::End);
host.hub.send_to(
conn,
&C::TagQuery(play::TagQuery {
transaction: p.transaction,
tag,
}),
);
}
S::Unknown(id, payload)
if id == horizon_protocol::ids::play::serverbound::COMMAND_SUGGESTION =>
{
@@ -1631,6 +1658,31 @@ impl Game<'_> {
}
}
/// `ServerLevel.getEntity(id)`'s `Entity.saveWithoutId`: the player or
/// other entity of the level in place with that network id.
fn level_entity_tag(&mut self, host: &mut LanHost, id: i32) -> Option<horizon_world::nbt::Tag> {
use super::commands::Target;
let player = host
.hub
.guests
.iter()
.find(|g| g.entity_id == id && host.hub.here(g))
.map(|g| g.conn);
let mut tag = match player {
Some(player) => self
.as_guest(host, player, |g| g.entity_nbt(Target::Player))
.flatten()?,
None => {
let target = self.level_entity(id)?;
self.entity_nbt(target)?
}
};
if let horizon_world::nbt::Tag::Compound(c) = &mut tag {
c.remove(&"id".into());
}
Some(tag)
}
/// `ServerGamePacketListenerImpl.tryPickItem` for the guest: a matching stack
/// is selected, else a creative player gets the item; the held slot is sent
/// back and the changed slots follow with the tick's inventory changes.
+5
View File
@@ -250,6 +250,9 @@ pub(super) struct Remote {
/// `ClientCommonPacketListenerImpl.serverLinks`: the links "Server
/// Links..." in the pause menu lists (only `http`/`https` URIs kept).
pub(super) server_links: Vec<horizon_protocol::packets::configuration::ServerLink>,
/// `ClientPacketListener.getDebugQueryHandler`: the F3+I query waiting
/// for the server's data.
pub(super) debug_query: super::debug_keys::DebugQuery,
}
/// The time a frame spends on chunk packets before it leaves the rest of
@@ -551,6 +554,7 @@ impl Game<'_> {
backlog: VecDeque::new(),
backlog_since: None,
server_links: Vec::new(),
debug_query: Default::default(),
transfer: None,
});
// The level's TickRateManager follows the server's ticking packets.
@@ -3047,6 +3051,7 @@ impl Game<'_> {
self.capture(true);
}
}
C::TagQuery(p) => self.answer_tag_query(p.transaction, p.tag),
C::Unknown(id, data)
if id == horizon_protocol::ids::play::clientbound::COMMAND_SUGGESTIONS =>
{
@@ -838,6 +838,11 @@ packet!(Sound {
seed: i64 => Long,
});
packet!(SystemChat { content: Tag => Nbt, overlay: bool => Bool });
packet!(
/// ClientboundTagQueryPacket: the answer to a debug NBT query (F3+I), with
/// the transaction it answers; `Tag::End` when there was no block entity.
TagQuery { transaction: i32 => VarInt, tag: Tag => Nbt }
);
packet!(TakeItemEntity { item: i32 => VarInt, player: i32 => VarInt, amount: i32 => VarInt });
packet!(TeleportEntity {
id: i32 => VarInt,
@@ -1925,6 +1930,7 @@ packet_enum!(Clientbound, crate::ids::play::clientbound {
SwingAnimation(SwingAnimation) = SWING_ANIMATION,
SystemChat(SystemChat) = SYSTEM_CHAT,
TabList(TabList) = TAB_LIST,
TagQuery(TagQuery) = TAG_QUERY,
TakeItemEntity(TakeItemEntity) = TAKE_ITEM_ENTITY,
TeleportEntity(TeleportEntity) = TELEPORT_ENTITY,
TickingState(TickingState) = TICKING_STATE,
@@ -2162,6 +2168,16 @@ packet!(
}
);
packet!(SelectTrade { item: i32 => VarInt });
packet!(
/// ServerboundBlockEntityTagQueryPacket: an operator's F3+I asks for the
/// block entity's data at a position.
BlockEntityTagQuery { transaction: i32 => VarInt, pos: [i32; 3] => Pos }
);
packet!(
/// ServerboundEntityTagQueryPacket: an operator's F3+I asks for an
/// entity's data, by its network id.
EntityTagQuery { transaction: i32 => VarInt, entity: i32 => VarInt }
);
packet!(
/// ServerboundContainerButtonClickPacket: `AbstractContainerMenu.clickMenuButton`
/// (the loom's patterns, the stonecutter's recipes, the enchanting table's offers).
@@ -2310,6 +2326,8 @@ packet_enum!(Serverbound, crate::ids::play::serverbound {
SetGameRule(SetGameRule) = SET_GAME_RULE,
AcceptTeleportation(AcceptTeleportation) = ACCEPT_TELEPORTATION,
Attack(Attack) = ATTACK,
BlockEntityTagQuery(BlockEntityTagQuery) = BLOCK_ENTITY_TAG_QUERY,
EntityTagQuery(EntityTagQuery) = ENTITY_TAG_QUERY,
TeleportToEntity(TeleportToEntity) = TELEPORT_TO_ENTITY,
ChangeGameMode(ChangeGameMode) = CHANGE_GAME_MODE,
Chat(Chat) = CHAT,
@@ -3049,6 +3067,54 @@ mod tests {
assert_eq!(body[body.len() - 1], 0);
}
#[test]
fn debug_tag_queries_decode_as_their_packets() {
use crate::ids::play::{clientbound, serverbound};
// ServerboundBlockEntityTagQueryPacket: VarInt transaction, BlockPos.
let pos = [3, 64, -2];
let mut body = vec![7];
body.extend_from_slice(&crate::buf::pack_block_pos(pos).to_be_bytes());
let packet = Serverbound::decode(serverbound::BLOCK_ENTITY_TAG_QUERY, &body).unwrap();
assert_eq!(
packet,
Serverbound::BlockEntityTagQuery(BlockEntityTagQuery {
transaction: 7,
pos
})
);
round_trip(packet, Serverbound::encode, Serverbound::decode);
// ServerboundEntityTagQueryPacket: VarInt transaction, VarInt entity id.
let packet = Serverbound::decode(serverbound::ENTITY_TAG_QUERY, &[0, 42]).unwrap();
assert_eq!(
packet,
Serverbound::EntityTagQuery(EntityTagQuery {
transaction: 0,
entity: 42
})
);
round_trip(packet, Serverbound::encode, Serverbound::decode);
// ClientboundTagQueryPacket: VarInt transaction, then the nullable
// compound of FriendlyByteBuf.writeNbt (a bare End tag for null).
let packet = Clientbound::decode(clientbound::TAG_QUERY, &[1, 0]).unwrap();
assert_eq!(
packet,
Clientbound::TagQuery(TagQuery {
transaction: 1,
tag: Tag::End
})
);
let mut items = horizon_world::nbt::Compound::new();
items.insert("Lock".into(), Tag::String("key".into()));
round_trip(
Clientbound::TagQuery(TagQuery {
transaction: 2,
tag: Tag::Compound(items),
}),
Clientbound::encode,
Clientbound::decode,
);
}
#[test]
fn change_game_mode_uses_the_game_type_varint() {
for mode in 0..=3 {
+1 -1
View File
@@ -48,7 +48,7 @@ ClientPacketListener.handleCommands port crates/horizon-client/src/game/remote.r
ClientPacketListener.handleStopSoundEvent port crates/horizon-client/src/game/remote.rs::Game::handle_packet StopSound arm: audio stop_sounds
ClientPacketListener.handleUpdateRecipes skip:data Horizon reads recipes from its own tables; the server's list is not read
ClientPacketListener.handleLookAt missing player: a server command that turns the camera (/faceplayer, /tp with facing) does not turn this player's view
ClientPacketListener.handleTagQueryPacket skip:debug answer to a debug NBT query; Horizon sends none
ClientPacketListener.handleTagQueryPacket port crates/horizon-client/src/game/debug_keys.rs::Game::answer_tag_query reached from the TagQuery arm of remote.rs handle_packet
ClientPacketListener.handleAwardStats port crates/horizon-client/src/game/remote.rs::Game::handle_packet AwardStats arm: known stat keys into the stats values
ClientPacketListener.handleRecipeBookAdd port crates/horizon-client/src/game/recipe_book.rs::Game::remote_recipe_book_add
ClientPacketListener.handleRecipeBookRemove port crates/horizon-client/src/game/recipe_book.rs::Game::remote_recipe_book_remove
1 ClientPacketListener.<init> port crates/horizon-client/src/game/remote.rs::Game::join_server gap: minor: clears the chat on join; vanilla restores chat state kept across a reconfiguration
48 ClientPacketListener.handleStopSoundEvent port crates/horizon-client/src/game/remote.rs::Game::handle_packet StopSound arm: audio stop_sounds
49 ClientPacketListener.handleUpdateRecipes skip:data Horizon reads recipes from its own tables; the server's list is not read
50 ClientPacketListener.handleLookAt missing player: a server command that turns the camera (/faceplayer, /tp with facing) does not turn this player's view
51 ClientPacketListener.handleTagQueryPacket skip:debug port crates/horizon-client/src/game/debug_keys.rs::Game::answer_tag_query answer to a debug NBT query; Horizon sends none reached from the TagQuery arm of remote.rs handle_packet
52 ClientPacketListener.handleAwardStats port crates/horizon-client/src/game/remote.rs::Game::handle_packet AwardStats arm: known stat keys into the stats values
53 ClientPacketListener.handleRecipeBookAdd port crates/horizon-client/src/game/recipe_book.rs::Game::remote_recipe_book_add
54 ClientPacketListener.handleRecipeBookRemove port crates/horizon-client/src/game/recipe_book.rs::Game::remote_recipe_book_remove
+4 -4
View File
@@ -42,10 +42,10 @@ ClientRecipeBook.categorizeAndGroupRecipes port crates/horizon-client/src/recipe
CommandHistory.addCommand port crates/horizon-client-render/src/chat.rs::Chat::add_recent no repeats; keeps the last 50 saved commands
CommandHistory.save inline crates/horizon-client-render/src/chat.rs::Chat::add_recent writes command_history.txt there
ComponentCollector.getResult skip:java collector helper
DebugQueryHandler.handleResponse skip:debug F3 NBT query: packet ids exist in horizon-protocol but nothing sends them
DebugQueryHandler.startTransaction skip:debug F3 NBT query transaction; see handleResponse
DebugQueryHandler.queryEntityTag skip:debug F3 entity NBT query; see handleResponse
DebugQueryHandler.queryBlockEntityTag skip:debug F3 block entity NBT query; see handleResponse
DebugQueryHandler.handleResponse port crates/horizon-client/src/game/debug_keys.rs::DebugQuery::answer
DebugQueryHandler.startTransaction port crates/horizon-client/src/game/debug_keys.rs::DebugQuery::start
DebugQueryHandler.queryEntityTag inline crates/horizon-client/src/game/debug_keys.rs::Game::copy_recreate_command the query waits in Remote.debug_query
DebugQueryHandler.queryBlockEntityTag inline crates/horizon-client/src/game/debug_keys.rs::Game::copy_recreate_command the query waits in Remote.debug_query
DeltaTracker.Timer.pause port crates/horizon-client/src/clock.rs::Timer::update_pause_state residual kept while paused
DeltaTracker.Timer.unPause port crates/horizon-client/src/clock.rs::Timer::update_pause_state residual restored on resume
GameNarrator missing minor: no text-to-speech; the narrator option is stored but nothing is spoken
1 PlayerHurtEntityTrigger.TriggerInstance.playerHurtEntityWithDamage skip:data criterion factory; Horizon reads the criterion from the advancement JSON
42 CommandHistory.addCommand port crates/horizon-client-render/src/chat.rs::Chat::add_recent no repeats; keeps the last 50 saved commands
43 CommandHistory.save inline crates/horizon-client-render/src/chat.rs::Chat::add_recent writes command_history.txt there
44 ComponentCollector.getResult skip:java collector helper
45 DebugQueryHandler.handleResponse skip:debug port crates/horizon-client/src/game/debug_keys.rs::DebugQuery::answer F3 NBT query: packet ids exist in horizon-protocol but nothing sends them
46 DebugQueryHandler.startTransaction skip:debug port crates/horizon-client/src/game/debug_keys.rs::DebugQuery::start F3 NBT query transaction; see handleResponse
47 DebugQueryHandler.queryEntityTag skip:debug inline crates/horizon-client/src/game/debug_keys.rs::Game::copy_recreate_command F3 entity NBT query; see handleResponse the query waits in Remote.debug_query
48 DebugQueryHandler.queryBlockEntityTag skip:debug inline crates/horizon-client/src/game/debug_keys.rs::Game::copy_recreate_command F3 block entity NBT query; see handleResponse the query waits in Remote.debug_query
49 DeltaTracker.Timer.pause port crates/horizon-client/src/clock.rs::Timer::update_pause_state residual kept while paused
50 DeltaTracker.Timer.unPause port crates/horizon-client/src/clock.rs::Timer::update_pause_state residual restored on resume
51 GameNarrator missing minor: no text-to-speech; the narrator option is stored but nothing is spoken
+3 -3
View File
@@ -88,11 +88,11 @@ ClientboundSetPlayerTeamPacket.shouldHavePlayerList port crates/horizon-protocol
ClientboundSetTitlesAnimationPacket.write port crates/horizon-protocol/src/buf.rs::write packet! TitlesAnimation {fade_in, stay, fade_out Int}
ClientboundSoundEntityPacket.write port crates/horizon-protocol/src/buf.rs::write packet! SoundEntity with the sound holder
ClientboundSoundPacket.write port crates/horizon-protocol/src/buf.rs::write packet! Sound with the sound holder and fixed-point position
ClientboundTagQueryPacket.write missing minor: answers to F3+I NBT queries are not handled; Horizon has no tag_query packet (ids.rs only)
ClientboundTagQueryPacket.write port crates/horizon-protocol/src/buf.rs::write packet! TagQuery {transaction VarInt, tag Nbt}
ClientboundTakeItemEntityPacket.write port crates/horizon-protocol/src/buf.rs::write packet! TakeItemEntity {item, player, amount VarInt}
DebugEntityNameGenerator.getEntityName skip:debug debug name for entities
ServerboundBlockEntityTagQueryPacket.write missing minor: the F3+I block NBT query is not sent; Horizon has no such packet (ids.rs only)
ServerboundEntityTagQueryPacket.write missing minor: the F3+I entity NBT query is not sent; Horizon has no such packet (ids.rs only)
ServerboundBlockEntityTagQueryPacket.write port crates/horizon-protocol/src/buf.rs::write packet! BlockEntityTagQuery {transaction VarInt, pos Pos}
ServerboundEntityTagQueryPacket.write port crates/horizon-protocol/src/buf.rs::write packet! EntityTagQuery {transaction VarInt, entity VarInt}
ServerboundJigsawGeneratePacket.write missing minor: no jigsaw generate packet; the client has no jigsaw screen (only block_entity_nbt reads jigsaw data)
ServerboundMovePlayerPacket.packFlags inline crates/horizon-client/src/player_sync.rs::PositionSender::update on-ground and horizontal-collision bits set when the move is sent
ServerboundPlayerActionPacket.write port crates/horizon-protocol/src/buf.rs::write packet! PlayerAction {action VarInt, pos, direction UByte, sequence VarInt}
1 IdDispatchCodec.decode skip:java codec dispatch; packet_enum! matches the id in packets/mod.rs
88 ClientboundSetTitlesAnimationPacket.write port crates/horizon-protocol/src/buf.rs::write packet! TitlesAnimation {fade_in, stay, fade_out Int}
89 ClientboundSoundEntityPacket.write port crates/horizon-protocol/src/buf.rs::write packet! SoundEntity with the sound holder
90 ClientboundSoundPacket.write port crates/horizon-protocol/src/buf.rs::write packet! Sound with the sound holder and fixed-point position
91 ClientboundTagQueryPacket.write missing port crates/horizon-protocol/src/buf.rs::write minor: answers to F3+I NBT queries are not handled; Horizon has no tag_query packet (ids.rs only) packet! TagQuery {transaction VarInt, tag Nbt}
92 ClientboundTakeItemEntityPacket.write port crates/horizon-protocol/src/buf.rs::write packet! TakeItemEntity {item, player, amount VarInt}
93 DebugEntityNameGenerator.getEntityName skip:debug debug name for entities
94 ServerboundBlockEntityTagQueryPacket.write missing port crates/horizon-protocol/src/buf.rs::write minor: the F3+I block NBT query is not sent; Horizon has no such packet (ids.rs only) packet! BlockEntityTagQuery {transaction VarInt, pos Pos}
95 ServerboundEntityTagQueryPacket.write missing port crates/horizon-protocol/src/buf.rs::write minor: the F3+I entity NBT query is not sent; Horizon has no such packet (ids.rs only) packet! EntityTagQuery {transaction VarInt, entity VarInt}
96 ServerboundJigsawGeneratePacket.write missing minor: no jigsaw generate packet; the client has no jigsaw screen (only block_entity_nbt reads jigsaw data)
97 ServerboundMovePlayerPacket.packFlags inline crates/horizon-client/src/player_sync.rs::PositionSender::update on-ground and horizontal-collision bits set when the move is sent
98 ServerboundPlayerActionPacket.write port crates/horizon-protocol/src/buf.rs::write packet! PlayerAction {action VarInt, pos, direction UByte, sequence VarInt}
+2 -2
View File
@@ -24,8 +24,8 @@ ServerGamePacketListenerImpl.handleJigsawGenerate missing player: the jigsaw bl
ServerGamePacketListenerImpl.handleSelectTrade port crates/horizon-client/src/game/lan_trading.rs::Game::lan_select_trade
ServerGamePacketListenerImpl.updateBookContents port crates/horizon-client/src/game/books.rs::Game::write_book writes the pages into the writable book in the slot
ServerGamePacketListenerImpl.filterableFromOutgoing skip:service text filter pass-through: with no filter on the LAN host the page is the raw text
ServerGamePacketListenerImpl.handleEntityTagQuery missing player: an op's entity NBT query (ENTITY_TAG_QUERY) gets no reply
ServerGamePacketListenerImpl.handleBlockEntityTagQuery missing player: an op's block entity NBT query (BLOCK_ENTITY_TAG_QUERY) gets no reply
ServerGamePacketListenerImpl.handleEntityTagQuery port crates/horizon-client/src/game/lan_actions.rs::Game::level_entity_tag reached from the EntityTagQuery arm of lan_guest_packet, which checks permission 2 or more
ServerGamePacketListenerImpl.handleBlockEntityTagQuery inline crates/horizon-client/src/game/lan_actions.rs::Game::lan_guest_packet the BlockEntityTagQuery arm, with debug_keys.rs block_entity_tag
ServerGamePacketListenerImpl.shouldCheckPlayerMovement inline crates/horizon-client/src/lan/hub.rs::Hub::position_change gap: minor: the changing-dimension exemption is not checked; the single-player owner, movement and elytra gates are
ServerGamePacketListenerImpl.forceSendPlayerSupportBlocks missing minor: the blocks under a player standing on air are never sent again (vanilla's desync fix)
ServerGamePacketListenerImpl.updateAwaitingTeleport port crates/horizon-client/src/lan/hub.rs::Hub::move_player gap: minor: vanilla runs it every tick; here it runs only when a move packet arrives
1 ServerGamePacketListenerImpl.getMaximumFlyingTicks missing player: the flying kick (multiplayer.disconnect.flying) for hovering above ground is never checked; the tick limit is not computed
24 ServerGamePacketListenerImpl.handleSelectTrade port crates/horizon-client/src/game/lan_trading.rs::Game::lan_select_trade
25 ServerGamePacketListenerImpl.updateBookContents port crates/horizon-client/src/game/books.rs::Game::write_book writes the pages into the writable book in the slot
26 ServerGamePacketListenerImpl.filterableFromOutgoing skip:service text filter pass-through: with no filter on the LAN host the page is the raw text
27 ServerGamePacketListenerImpl.handleEntityTagQuery missing port crates/horizon-client/src/game/lan_actions.rs::Game::level_entity_tag player: an op's entity NBT query (ENTITY_TAG_QUERY) gets no reply reached from the EntityTagQuery arm of lan_guest_packet, which checks permission 2 or more
28 ServerGamePacketListenerImpl.handleBlockEntityTagQuery missing inline crates/horizon-client/src/game/lan_actions.rs::Game::lan_guest_packet player: an op's block entity NBT query (BLOCK_ENTITY_TAG_QUERY) gets no reply the BlockEntityTagQuery arm, with debug_keys.rs block_entity_tag
29 ServerGamePacketListenerImpl.shouldCheckPlayerMovement inline crates/horizon-client/src/lan/hub.rs::Hub::position_change gap: minor: the changing-dimension exemption is not checked; the single-player owner, movement and elytra gates are
30 ServerGamePacketListenerImpl.forceSendPlayerSupportBlocks missing minor: the blocks under a player standing on air are never sent again (vanilla's desync fix)
31 ServerGamePacketListenerImpl.updateAwaitingTeleport port crates/horizon-client/src/lan/hub.rs::Hub::move_player gap: minor: vanilla runs it every tick; here it runs only when a move packet arrives