mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
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:
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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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}
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
Reference in new issue
Block a user