From 8106337f7ce3d1e2314e0d71282153ba5cd7178a Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:57:21 +0200 Subject: [PATCH 1/6] Add rotation support for /tp yaw and pitch parameters can be relative or absolute New UpdateType added to update player rotation Player rotation update packet is seperate from teleport packet to prevent repeated automated teleportations from messing with camera rotations. Might be useful in cases where modded servers want to prevent players from walking into a certain area or moving entirely. Update tp.zig Don't send a teleport packet if the target is already at the appropriate coordinates. It just seems right to not send a teleport packet if you are only changing the player rotation since the same is done the other way around as well. For consistency's sake. Is only intended to stop teleport packet sending in case of `/tp ~ ~ ~ `, but works in case of `/tp ~ ~ ~` as well. Does nothing to stop packet sending in case of something like `/tp 0 ~ ~`x2. Adapt to command source changes Adapt to command source changes in c4cf9ec8decaf19408b99e7f17afd367169ad81d Make requested changes --- src/network/protocols.zig | 20 +++++++++++++++++++- src/server/command.zig | 34 ++++++++++++++++++++++++++++++++++ src/server/command/tp.zig | 11 ++++++++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/network/protocols.zig b/src/network/protocols.zig index 50a7350942..bdb4877d4c 100644 --- a/src/network/protocols.zig +++ b/src/network/protocols.zig @@ -606,6 +606,7 @@ pub const genericUpdate = struct { // MARK: genericUpdate biome = 4, particles = 5, clear = 6, + setRotation = 7, }; const WorldEditPosition = enum(u2) { @@ -626,6 +627,13 @@ pub const genericUpdate = struct { // MARK: genericUpdate .teleport => { game.Player.setPosBlocking(try reader.readVec(Vec3d)); }, + .setRotation => { + var rot: Vec3f = try reader.readVec(Vec3f); + const bound = std.math.pi/2.0 - 0.001; + rot[0] = std.math.clamp(rot[0], -bound, bound); + game.camera.rotation[0] = rot[0]; + game.camera.rotation[2] = rot[2]; + }, .worldEditPos => { const typ = try reader.readEnum(WorldEditPosition); const pos: ?Vec3i = switch (typ) { @@ -704,7 +712,7 @@ pub const genericUpdate = struct { // MARK: genericUpdate fn serverReceive(conn: *Connection, reader: *utils.BinaryReader) !void { switch (try reader.readEnum(UpdateType)) { - .gamemode, .teleport, .time, .biome, .particles, .clear => return error.InvalidSide, + .gamemode, .teleport, .setRotation, .time, .biome, .particles, .clear => return error.InvalidSide, .worldEditPos => { const typ = try reader.readEnum(WorldEditPosition); const pos: ?Vec3i = switch (typ) { @@ -737,6 +745,16 @@ pub const genericUpdate = struct { // MARK: genericUpdate conn.send(.secure, id, writer.data.items); } + pub fn sendTPRotation(conn: *Connection, rot: Vec3f) void { + var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 20); + defer writer.deinit(); + + writer.writeEnum(UpdateType, .setRotation); + writer.writeVec(Vec3f, rot); + + conn.send(.secure, id, writer.data.items); + } + pub fn sendWorldEditPos(conn: *Connection, posType: WorldEditPosition, maybePos: ?Vec3i) void { var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 25); defer writer.deinit(); diff --git a/src/server/command.zig b/src/server/command.zig index 1acdc5472e..5eb21b313a 100644 --- a/src/server/command.zig +++ b/src/server/command.zig @@ -108,6 +108,27 @@ pub const Coordinate = union(enum) { } }; +pub const Rotation = union(enum) { + relative: f32, // Relative rotations are indicated by leading `~`. + absolute: f32, + + pub fn parse(_: NeverFailingAllocator, name: []const u8, arg: []const u8, errorMessage: *ListManaged(u8)) error{ParseError}!Rotation { + const isRelative = arg[0] == '~'; + const numberSlice = if (isRelative) arg[1..] else arg; + if (isRelative and numberSlice.len == 0) return .{.relative = 0}; + if (isRelative) { + return .{.relative = std.fmt.parseFloat(f32, numberSlice) catch { + errorMessage.print("Expected number for <{s}>, found \"{s}\"", .{name, numberSlice}); + return error.ParseError; + }}; + } + return .{.absolute = std.fmt.parseFloat(f32, numberSlice) catch { + errorMessage.print("Expected number or \"~\" for <{s}>, found \"{s}\"", .{name, arg}); + return error.ParseError; + }}; + } +}; + pub fn resolveCoordinates(x: Coordinate, y: Coordinate, z: Coordinate, source: Source) error{InvalidArg}!main.vec.Vec3d { if (source != .user and (x == .relative or y == .relative or z == .relative)) { source.sendMessage("Command was run without a user; unable to interpret relative coordinates.", .{}); @@ -121,6 +142,19 @@ pub fn resolveCoordinates(x: Coordinate, y: Coordinate, z: Coordinate, source: S }; } +pub fn resolveRotation(yaw: Rotation, pitch: Rotation, source: Source) error{InvalidArg}!main.vec.Vec3f { + if (source != .user and (yaw == .relative or pitch == .relative)) { + source.sendMessage("Command was run without a user; unable to interpret relative rotation.", .{}); + return error.InvalidArg; + } + const bound = std.math.pi/2.0 - 0.001; + return .{ + std.math.clamp(if (yaw == .relative) source.user.player().rot[0] + yaw.relative*std.math.pi/180 else yaw.absolute*std.math.pi/180, -bound, bound), + 0, + if (pitch == .relative) source.user.player().rot[2] + @mod(pitch.relative, 360)*std.math.pi/180 else @mod(pitch.absolute, 360)*std.math.pi/180, + }; +} + pub const Target = struct { user: *User, diff --git a/src/server/command/tp.zig b/src/server/command/tp.zig index 45d0e94e4c..c30915a90c 100644 --- a/src/server/command/tp.zig +++ b/src/server/command/tp.zig @@ -32,6 +32,14 @@ pub const Args = union(enum) { sourcePlayerIndex: command.PlayerIndex, destinationPlayerIndex: command.PlayerIndex, }, + @"/tp ": struct { + sourcePlayerIndex: ?command.PlayerIndex, + x: command.Coordinate, + y: command.Coordinate, + z: command.Coordinate, + yaw: command.Rotation, + pitch: command.Rotation, + }, }; pub fn execute(args: Args, source: Source) void { @@ -104,5 +112,6 @@ pub fn execute(args: Args, source: Source) void { break :blk dest.user.player().pos; }, }; - main.network.protocols.genericUpdate.sendTPCoordinates(target.user.conn, pos); + + if (!std.meta.eql(target.user.player().pos, pos)) main.network.protocols.genericUpdate.sendTPCoordinates(target.conn, pos); } From 5188d36543a4daeb4ac3a534ed66cf26ee64d284 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:05:57 +0200 Subject: [PATCH 2/6] Fix Will do thorough testing once I've migrated to SyncOperations --- src/server/command/tp.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/server/command/tp.zig b/src/server/command/tp.zig index c30915a90c..b5ee7747c4 100644 --- a/src/server/command/tp.zig +++ b/src/server/command/tp.zig @@ -107,11 +107,15 @@ pub fn execute(args: Args, source: Source) void { .@"/tp " => |pos| { break :blk command.resolveCoordinates(pos.x, pos.y, pos.z, source) catch return; }, + .@"/tp " => |pos| { + main.network.protocols.genericUpdate.sendTPRotation(target.user.conn, command.resolveRotation(pos.yaw, pos.pitch, source) catch return); + break :blk command.resolveCoordinates(pos.x, pos.y, pos.z, source) catch return; + }, inline .@"/tp ", .@"/tp " => |index| { const dest = command.Target.fromPlayerIndex(index.destinationPlayerIndex, source) catch return; break :blk dest.user.player().pos; }, }; - if (!std.meta.eql(target.user.player().pos, pos)) main.network.protocols.genericUpdate.sendTPCoordinates(target.conn, pos); + if (!std.meta.eql(target.user.player().pos, pos)) main.network.protocols.genericUpdate.sendTPCoordinates(target.user.conn, pos); } From 1a203be27d0aebe0b8a7c671750c55304fa14ce7 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Thu, 6 Aug 2026 18:00:41 +0200 Subject: [PATCH 3/6] Add setRotation SyncOperation The operation gets executed on the server, so it doesn't work. --- src/server/command/tp.zig | 2 +- src/sync.zig | 73 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/server/command/tp.zig b/src/server/command/tp.zig index b5ee7747c4..1a029aa51d 100644 --- a/src/server/command/tp.zig +++ b/src/server/command/tp.zig @@ -108,7 +108,7 @@ pub fn execute(args: Args, source: Source) void { break :blk command.resolveCoordinates(pos.x, pos.y, pos.z, source) catch return; }, .@"/tp " => |pos| { - main.network.protocols.genericUpdate.sendTPRotation(target.user.conn, command.resolveRotation(pos.yaw, pos.pitch, source) catch return); + main.sync.server.executeCommand(.{.setRotation = .{.target = target.user.id, .rotation = command.resolveRotation(pos.yaw, pos.pitch, source) catch return}}, source.user); break :blk command.resolveCoordinates(pos.x, pos.y, pos.z, source) catch return; }, inline .@"/tp ", .@"/tp " => |index| { diff --git a/src/sync.zig b/src/sync.zig index 6ec9a7e7c2..96360c5801 100644 --- a/src/sync.zig +++ b/src/sync.zig @@ -241,6 +241,7 @@ pub const Command = struct { // MARK: Command updateBlock = 9, addHealth = 10, chatCommand = 12, + setRotation = 18, }; pub const Payload = union(PayloadType) { open: Open, @@ -261,6 +262,7 @@ pub const Command = struct { // MARK: Command updateBlock: UpdateBlock, addHealth: AddHealth, chatCommand: ChatCommand, + setRotation: SetRotation, }; const BaseOperationType = enum(u8) { @@ -273,6 +275,7 @@ pub const Command = struct { // MARK: Command useDurability = 4, addHealth = 5, addEnergy = 6, + setRotation = 9, }; pub const BaseOperation = union(BaseOperationType) { @@ -322,6 +325,11 @@ pub const Command = struct { // MARK: Command energy: f32, previous: f32, }, + setRotation: struct { + target: ?*main.server.User, + rotation: Vec3f, + previous: Vec3f, + }, }; const SyncOperationType = enum(u8) { @@ -618,6 +626,9 @@ pub const Command = struct { // MARK: Command .addEnergy => |info| { main.game.Player.super.energy = info.previous; }, + .setRotation => |info| { + main.game.camera.rotation = info.previous; + }, } } } @@ -625,7 +636,7 @@ pub const Command = struct { // MARK: Command fn finalize(self: Command, allocator: NeverFailingAllocator, side: Side, reader: *BinaryReader) !void { for (self.baseOperations.items) |step| { switch (step) { - .move, .swap, .create, .moveToBag, .takeFromBag, .addHealth, .addEnergy => {}, + .move, .swap, .create, .moveToBag, .takeFromBag, .addHealth, .addEnergy, .setRotation => {}, .delete => |info| { info.item.deinit(); }, @@ -814,6 +825,23 @@ pub const Command = struct { // MARK: Command main.game.Player.super.energy = std.math.clamp(main.game.Player.super.energy + info.energy, 0, main.game.Player.super.maxEnergy); } }, + .setRotation => |*info| { + if (side == .server) { + info.previous = info.target.?.player().rot; + std.log.debug("SetRotation executed on server; target=TRUNCATED, rotation={}", .{info.rotation}); + + info.target.?.player().rot = info.rotation; + self.baseOperations.append(allocator, .{.setRotation = .{ + .target = info.target.?, + .rotation = info.rotation, + .previous = info.previous, + }}); + } else { + std.log.debug("SetRotation executed on client; target=TRUNCATED, rotation={}", .{info.rotation}); + info.previous = main.game.camera.rotation; + main.game.camera.rotation = info.rotation; + } + }, } self.baseOperations.append(allocator, op); } @@ -1730,6 +1758,49 @@ pub const Command = struct { // MARK: Command } }; + const SetRotation = struct { // MARK: SetRotation + target: main.entity.Entity, + rotation: Vec3f, + + fn run(self: SetRotation, ctx: Context) error{serverFailure}!void { + std.log.debug("SetRotation ran; target={}, rotation={}", .{self.target, self.rotation}); + var target: ?*main.server.User = null; + + if (ctx.side == .server) { + const userList = main.server.getUserList(main.stackAllocator); + defer main.stackAllocator.free(userList); + for (userList) |user| { + if (user.id == self.target) { + target = user; + break; + } + } + + if (target == null) return error.serverFailure; + } + + ctx.execute(.{.setRotation = .{ + .target = target, + .rotation = self.rotation, + .previous = if (ctx.side == .server) target.?.player().rot else main.game.camera.rotation, + }}); + } + + fn serialize(self: SetRotation, writer: *BinaryWriter) void { + writer.writeEnum(main.entity.Entity, self.target); + writer.writeVec(Vec3f, self.rotation); + } + + fn deserialize(reader: *BinaryReader, _: Side, user: ?*main.server.User) !SetRotation { + const result: SetRotation = .{ + .target = try reader.readEnum(main.entity.Entity), + .rotation = try reader.readVec(Vec3f), + }; + if (user.?.id != result.target) return error.Invalid; + return result; + } + }; + const ChatCommand = struct { // MARK: ChatCommand message: []const u8, From eb39ad5d4b0994e0820ba1529329283275a4c123 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:47:58 +0200 Subject: [PATCH 4/6] Update sync.zig Try to fix it --- src/sync.zig | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/sync.zig b/src/sync.zig index 96360c5801..805c84a261 100644 --- a/src/sync.zig +++ b/src/sync.zig @@ -339,6 +339,7 @@ pub const Command = struct { // MARK: Command health = 3, kill = 4, energy = 5, + rotation = 6, }; const SyncOperation = union(SyncOperationType) { // MARK: SyncOperation @@ -368,6 +369,10 @@ pub const Command = struct { // MARK: Command target: ?*main.server.User, energy: f32, }, + rotation: struct { + target: ?*main.server.User, + rotation: Vec3f, + }, pub fn executeFromData(reader: *BinaryReader) !void { switch (try deserialize(reader)) { @@ -414,6 +419,9 @@ pub const Command = struct { // MARK: Command .energy => |energy| { main.game.Player.super.energy = std.math.clamp(main.game.Player.super.energy + energy.energy, 0, main.game.Player.super.maxEnergy); }, + .rotation => |rotation| { + main.game.camera.rotation = rotation.rotation; + }, } } @@ -427,7 +435,7 @@ pub const Command = struct { // MARK: Command } return result; }, - inline .health, .kill, .energy => |data| { + inline .health, .kill, .energy, .rotation => |data| { const out = allocator.alloc(*main.server.User, 1); out[0] = data.target.?; return out; @@ -437,7 +445,7 @@ pub const Command = struct { // MARK: Command pub fn ignoreSource(self: SyncOperation) bool { return switch (self) { - .create, .delete, .useDurability, .health, .energy => true, + .create, .delete, .useDurability, .health, .energy, .rotation => true, .kill => false, }; } @@ -488,6 +496,12 @@ pub const Command = struct { // MARK: Command .energy = try reader.readFloat(f32), }}; }, + .rotation => { + return .{.rotation = .{ + .target = null, + .rotation = try reader.readVec(Vec3f), + }}; + }, } } @@ -519,6 +533,9 @@ pub const Command = struct { // MARK: Command .energy => |energy| { writer.writeFloat(f32, energy.energy); }, + .rotation => |rotation| { + writer.writeVec(Vec3f, rotation.rotation); + }, } return writer.data.toOwnedSlice(); } @@ -831,10 +848,9 @@ pub const Command = struct { // MARK: Command std.log.debug("SetRotation executed on server; target=TRUNCATED, rotation={}", .{info.rotation}); info.target.?.player().rot = info.rotation; - self.baseOperations.append(allocator, .{.setRotation = .{ + self.syncOperations.append(allocator, .{.rotation = .{ .target = info.target.?, .rotation = info.rotation, - .previous = info.previous, }}); } else { std.log.debug("SetRotation executed on client; target=TRUNCATED, rotation={}", .{info.rotation}); From 7b1f71a5a62df483e58638094040efd0abcafc79 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:34:28 +0200 Subject: [PATCH 5/6] Fixed! Thanks to Wunka --- src/server/command/tp.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/command/tp.zig b/src/server/command/tp.zig index 1a029aa51d..a87858cd66 100644 --- a/src/server/command/tp.zig +++ b/src/server/command/tp.zig @@ -108,7 +108,7 @@ pub fn execute(args: Args, source: Source) void { break :blk command.resolveCoordinates(pos.x, pos.y, pos.z, source) catch return; }, .@"/tp " => |pos| { - main.sync.server.executeCommand(.{.setRotation = .{.target = target.user.id, .rotation = command.resolveRotation(pos.yaw, pos.pitch, source) catch return}}, source.user); + main.sync.server.executeCommand(.{.setRotation = .{.target = target.user.id, .rotation = command.resolveRotation(pos.yaw, pos.pitch, source) catch return}}, null); break :blk command.resolveCoordinates(pos.x, pos.y, pos.z, source) catch return; }, inline .@"/tp ", .@"/tp " => |index| { From 5aafe7323633fed7ee6fbbe563fd5d39d4a0609d Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:41:14 +0200 Subject: [PATCH 6/6] Rip out unused code We don't need it anymore --- src/network/protocols.zig | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/network/protocols.zig b/src/network/protocols.zig index bdb4877d4c..50a7350942 100644 --- a/src/network/protocols.zig +++ b/src/network/protocols.zig @@ -606,7 +606,6 @@ pub const genericUpdate = struct { // MARK: genericUpdate biome = 4, particles = 5, clear = 6, - setRotation = 7, }; const WorldEditPosition = enum(u2) { @@ -627,13 +626,6 @@ pub const genericUpdate = struct { // MARK: genericUpdate .teleport => { game.Player.setPosBlocking(try reader.readVec(Vec3d)); }, - .setRotation => { - var rot: Vec3f = try reader.readVec(Vec3f); - const bound = std.math.pi/2.0 - 0.001; - rot[0] = std.math.clamp(rot[0], -bound, bound); - game.camera.rotation[0] = rot[0]; - game.camera.rotation[2] = rot[2]; - }, .worldEditPos => { const typ = try reader.readEnum(WorldEditPosition); const pos: ?Vec3i = switch (typ) { @@ -712,7 +704,7 @@ pub const genericUpdate = struct { // MARK: genericUpdate fn serverReceive(conn: *Connection, reader: *utils.BinaryReader) !void { switch (try reader.readEnum(UpdateType)) { - .gamemode, .teleport, .setRotation, .time, .biome, .particles, .clear => return error.InvalidSide, + .gamemode, .teleport, .time, .biome, .particles, .clear => return error.InvalidSide, .worldEditPos => { const typ = try reader.readEnum(WorldEditPosition); const pos: ?Vec3i = switch (typ) { @@ -745,16 +737,6 @@ pub const genericUpdate = struct { // MARK: genericUpdate conn.send(.secure, id, writer.data.items); } - pub fn sendTPRotation(conn: *Connection, rot: Vec3f) void { - var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 20); - defer writer.deinit(); - - writer.writeEnum(UpdateType, .setRotation); - writer.writeVec(Vec3f, rot); - - conn.send(.secure, id, writer.data.items); - } - pub fn sendWorldEditPos(conn: *Connection, posType: WorldEditPosition, maybePos: ?Vec3i) void { var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 25); defer writer.deinit();