From 21b5accc69453fe82eb48c50ce9f17f981b15307 Mon Sep 17 00:00:00 2001 From: DrPerkyLegit <116128211+DrPerkyLegit@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:21:22 -0400 Subject: [PATCH] Feature/plugin api experimental (#12) * added a null check to fix crash, expose internal latency value (its buggy) * fix latency calculations * sending packets from c# * world save event, move shutdown def, move called location of shutdown, expose FourKit.FireEvent * add docs --------- Co-authored-by: sylvessa <225480449+sylvessa@users.noreply.github.com> --- Doxyfile | 2 +- Minecraft.Client/PlayerConnection.cpp | 11 +- Minecraft.Server.FourKit/Entity/Player.cs | 30 + .../Event/World/WorldEvent.cs | 2 +- .../Event/World/WorldSaveEvent.cs | 11 + .../Experimental/PlayerConnection.cs | 28 +- Minecraft.Server.FourKit/FourKit.cs | 3 + .../FourKitHost.Callbacks.cs | 17 +- .../FourKitHost.Events.cs | 15 +- Minecraft.Server.FourKit/NativeBridge.cs | 16 +- .../docs/sending-packets.md | 1189 +++++++++++++++++ Minecraft.Server/FourKitBridge.cpp | 38 +- Minecraft.Server/FourKitBridge.h | 1 + Minecraft.Server/FourKitNatives.cpp | 19 + Minecraft.Server/FourKitNatives.h | 6 +- Minecraft.Server/Windows64/ServerMain.cpp | 4 +- Minecraft.World/Connection.cpp | 45 + Minecraft.World/Connection.h | 3 + 18 files changed, 1420 insertions(+), 20 deletions(-) create mode 100644 Minecraft.Server.FourKit/Event/World/WorldSaveEvent.cs create mode 100644 Minecraft.Server.FourKit/docs/sending-packets.md diff --git a/Doxyfile b/Doxyfile index aa660a7e7..8bf19403c 100644 --- a/Doxyfile +++ b/Doxyfile @@ -572,7 +572,7 @@ EXTRACT_PACKAGE = NO # included in the documentation. # The default value is: NO. -EXTRACT_STATIC = NO +EXTRACT_STATIC = YES # If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined # locally in source files will be included in the documentation. If set to NO, diff --git a/Minecraft.Client/PlayerConnection.cpp b/Minecraft.Client/PlayerConnection.cpp index 7d9734910..92ada058b 100644 --- a/Minecraft.Client/PlayerConnection.cpp +++ b/Minecraft.Client/PlayerConnection.cpp @@ -350,7 +350,7 @@ void PlayerConnection::handleMovePlayer(shared_ptr packet) // Anti-cheat: reject movement packets that exceed server-authoritative bounds. double velocitySq = player->xd * player->xd + player->yd * player->yd + player->zd * player->zd; double maxAllowedSq = kMoveBaseAllowanceSq + (velocitySq * kMoveVelocityAllowanceScale); - if (player->isAllowedToFly() || player->gameMode->isCreative()) + if (player->isAllowedToFly() || (player->gameMode != nullptr && player->gameMode->isCreative())) { // Creative / flight-allowed players can move farther legitimately per tick. maxAllowedSq *= 1.5; @@ -1717,8 +1717,13 @@ void PlayerConnection::handleKeepAlive(shared_ptr packet) { if (packet->id == lastKeepAliveId) { - int time = static_cast(System::nanoTime() / 1000000 - lastKeepAliveTime); - player->latency = (player->latency * 3 + time) / 4; + int64_t now = (System::nanoTime() / 1000000); + if (lastKeepAliveTime == 0) lastKeepAliveTime = now; + int64_t delta = static_cast(now - lastKeepAliveTime); + + player->latency = (player->latency * 3 + delta) / 4; + + lastKeepAliveTime = now; } } diff --git a/Minecraft.Server.FourKit/Entity/Player.cs b/Minecraft.Server.FourKit/Entity/Player.cs index 49b93d4bf..4499502b9 100644 --- a/Minecraft.Server.FourKit/Entity/Player.cs +++ b/Minecraft.Server.FourKit/Entity/Player.cs @@ -38,6 +38,7 @@ public class Player : HumanEntity, OfflinePlayer, CommandSender SetNameInternal(name); IsOnline = true; _playerInventory._holder = this; + _connection = new PlayerConnection(this); } /// @@ -55,6 +56,13 @@ public class Player : HumanEntity, OfflinePlayer, CommandSender return true; } + /// + /// Experimental. Gets the player's , which can be used + /// to send raw packet data directly to the client. + /// + /// The player's connection. + public PlayerConnection getConnection() => _connection; + /// public Player? getPlayer() => IsOnline ? this : null; @@ -86,10 +94,32 @@ public class Player : HumanEntity, OfflinePlayer, CommandSender public new Guid getUniqueId() => _playerUniqueId; + /// + /// Experimental. Gets the raw online XUID (Xbox User ID) for this player. + /// The online XUID is used for guests. + /// + /// The raw online XUID value. public ulong getRawOnlineXUID() => _playerRawOnlineXUID; + /// + /// Experimental. Gets the raw offline XUID (Xbox User ID) for this player. + /// The offline XUID is the main XUID used by the client. + /// + /// The raw offline XUID value. public ulong getRawOfflineXUID() => _playerRawOfflineXUID; + /// + /// Gets the player's estimated ping in milliseconds. + /// This value represents a weighted average of the response time to application layer ping packets sent. This value does not represent the network round trip time and as such may have less granularity and be impacted by other sources. For these reasons it should not be used for anti-cheat purposes. Its recommended use is only as a qualitative indicator of connection quality. + /// + /// The player's estimated ping in milliseconds. + public int getPing() + { + if (NativeBridge.GetPlayerLatency == null) + return -1; + + return NativeBridge.GetPlayerLatency(getEntityId()); + } /// /// Gets the player's current saturation level. diff --git a/Minecraft.Server.FourKit/Event/World/WorldEvent.cs b/Minecraft.Server.FourKit/Event/World/WorldEvent.cs index 4d30e8835..ddbd4f970 100644 --- a/Minecraft.Server.FourKit/Event/World/WorldEvent.cs +++ b/Minecraft.Server.FourKit/Event/World/WorldEvent.cs @@ -6,7 +6,7 @@ public class WorldEvent : Event { internal World? _world; - public WorldEvent(World? world) : base() + internal WorldEvent(World? world) : base() { _world = world; } diff --git a/Minecraft.Server.FourKit/Event/World/WorldSaveEvent.cs b/Minecraft.Server.FourKit/Event/World/WorldSaveEvent.cs new file mode 100644 index 000000000..37c6be467 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/World/WorldSaveEvent.cs @@ -0,0 +1,11 @@ +namespace Minecraft.Server.FourKit.Event.World; + +using Minecraft.Server.FourKit; + +public class WorldSaveEvent : Event +{ + + internal WorldSaveEvent() : base() + { + } +} diff --git a/Minecraft.Server.FourKit/Experimental/PlayerConnection.cs b/Minecraft.Server.FourKit/Experimental/PlayerConnection.cs index 5a56ed453..76b39aa1d 100644 --- a/Minecraft.Server.FourKit/Experimental/PlayerConnection.cs +++ b/Minecraft.Server.FourKit/Experimental/PlayerConnection.cs @@ -1,9 +1,35 @@ -using System; +using Minecraft.Server.FourKit.Entity; +using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Text; namespace Minecraft.Server.FourKit.Experimental; + public class PlayerConnection { + private Player _player; + internal PlayerConnection(Player player) + { + this._player = player; + } + + /// + /// Sends raw packet data directly to the client over the player's connection. + /// The byte array must contain the complete packet including the packet ID as the first byte. The server automatically prepends the 4-byte big-endian size header before transmitting. + /// + /// The raw packet bytes to send, where data[0] is the packet ID. + public void send(byte[] data) + { + var gh = GCHandle.Alloc(data, GCHandleType.Pinned); + try + { + NativeBridge.SendRaw?.Invoke(_player.getEntityId(), gh.AddrOfPinnedObject(), data.Length); + } + finally + { + gh.Free(); + } + } } diff --git a/Minecraft.Server.FourKit/FourKit.cs b/Minecraft.Server.FourKit/FourKit.cs index 71ed26aa3..cc8051254 100644 --- a/Minecraft.Server.FourKit/FourKit.cs +++ b/Minecraft.Server.FourKit/FourKit.cs @@ -6,6 +6,9 @@ using Minecraft.Server.FourKit.Event; using Minecraft.Server.FourKit.Inventory; using Minecraft.Server.FourKit.Plugin; +/// +/// The main entry point for the FourKit plugin API. +/// public static class FourKit { private static readonly EventDispatcher _dispatcher = new(); diff --git a/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs b/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs index ac52ad74f..f4e17ed1c 100644 --- a/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs +++ b/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs @@ -32,11 +32,11 @@ public static partial class FourKitHost } [UnmanagedCallersOnly] - public static void SetPlayerCallbacks(IntPtr kickPlayer, IntPtr banPlayer, IntPtr banPlayerIp, IntPtr getPlayerAddress) + public static void SetPlayerCallbacks(IntPtr kickPlayer, IntPtr banPlayer, IntPtr banPlayerIp, IntPtr getPlayerAddress, IntPtr getPlayerLatency) { try { - NativeBridge.SetPlayerCallbacks(kickPlayer, banPlayer, banPlayerIp, getPlayerAddress); + NativeBridge.SetPlayerCallbacks(kickPlayer, banPlayer, banPlayerIp, getPlayerAddress, getPlayerLatency); } catch (Exception ex) { @@ -44,6 +44,19 @@ public static partial class FourKitHost } } + [UnmanagedCallersOnly] + public static void SetPlayerConnectionCallbacks(IntPtr sendRaw) + { + try + { + NativeBridge.SetPlayerConnectionCallbacks(sendRaw); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetPlayerConnectionCallbacks error: {ex}"); + } + } + [UnmanagedCallersOnly] public static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer, IntPtr getItemMeta, IntPtr setItemMeta, IntPtr setHeldItemSlot) { diff --git a/Minecraft.Server.FourKit/FourKitHost.Events.cs b/Minecraft.Server.FourKit/FourKitHost.Events.cs index 3d2f3c6cb..165986681 100644 --- a/Minecraft.Server.FourKit/FourKitHost.Events.cs +++ b/Minecraft.Server.FourKit/FourKitHost.Events.cs @@ -15,6 +15,19 @@ namespace Minecraft.Server.FourKit; public static partial class FourKitHost { + [UnmanagedCallersOnly] + public static void FireWorldSave() + { + try + { + FourKit.FireEvent(new WorldSaveEvent()); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireWorldSave error: {ex}"); + } + } + [UnmanagedCallersOnly] public static int FirePlayerPreLogin(IntPtr namePtr, int nameByteLen, IntPtr ipPtr, int ipByteLen, int port) { @@ -34,7 +47,7 @@ public static partial class FourKitHost } catch (Exception ex) { - ServerLog.Error("fourkit", $"FirePlayerJoin error: {ex}"); + ServerLog.Error("fourkit", $"FirePlayerPreLogin error: {ex}"); return 0; } } diff --git a/Minecraft.Server.FourKit/NativeBridge.cs b/Minecraft.Server.FourKit/NativeBridge.cs index 1ec577872..2a38c788d 100644 --- a/Minecraft.Server.FourKit/NativeBridge.cs +++ b/Minecraft.Server.FourKit/NativeBridge.cs @@ -87,6 +87,12 @@ internal static class NativeBridge [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int NativeGetPlayerAddressDelegate(int entityId, IntPtr outIpBuf, int outIpBufSize, IntPtr outPort); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetPlayerLatencyDelegate(int entityId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeSendRawDelegate(int entityId, IntPtr dataBuf, int dataBufSize); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void NativeGetPlayerInventoryDelegate(int entityId, IntPtr outBuffer); @@ -203,6 +209,8 @@ internal static class NativeBridge internal static NativeBanPlayerDelegate? BanPlayer; internal static NativeBanPlayerIpDelegate? BanPlayerIp; internal static NativeGetPlayerAddressDelegate? GetPlayerAddress; + internal static NativeGetPlayerLatencyDelegate? GetPlayerLatency; + internal static NativeSendRawDelegate? SendRaw; internal static NativeGetPlayerInventoryDelegate? GetPlayerInventory; internal static NativeSetPlayerInventorySlotDelegate? SetPlayerInventorySlot; internal static NativeGetContainerContentsDelegate? GetContainerContents; @@ -264,12 +272,18 @@ internal static class NativeBridge DropItem = Marshal.GetDelegateForFunctionPointer(dropItem); } - internal static void SetPlayerCallbacks(IntPtr kickPlayer, IntPtr banPlayer, IntPtr banPlayerIp, IntPtr getPlayerAddress) + internal static void SetPlayerCallbacks(IntPtr kickPlayer, IntPtr banPlayer, IntPtr banPlayerIp, IntPtr getPlayerAddress, IntPtr getPlayerLatency) { KickPlayer = Marshal.GetDelegateForFunctionPointer(kickPlayer); BanPlayer = Marshal.GetDelegateForFunctionPointer(banPlayer); BanPlayerIp = Marshal.GetDelegateForFunctionPointer(banPlayerIp); GetPlayerAddress = Marshal.GetDelegateForFunctionPointer(getPlayerAddress); + GetPlayerLatency = Marshal.GetDelegateForFunctionPointer(getPlayerLatency); + } + + internal static void SetPlayerConnectionCallbacks(IntPtr sendRaw) + { + SendRaw = Marshal.GetDelegateForFunctionPointer(sendRaw); } internal static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer, IntPtr getItemMeta, IntPtr setItemMeta, IntPtr setHeldItemSlot) diff --git a/Minecraft.Server.FourKit/docs/sending-packets.md b/Minecraft.Server.FourKit/docs/sending-packets.md new file mode 100644 index 000000000..ec1d83e49 --- /dev/null +++ b/Minecraft.Server.FourKit/docs/sending-packets.md @@ -0,0 +1,1189 @@ +@page sending-packets Sending Packets + +This page covers how to manually send raw packets to clients using the \ref Minecraft.Server.FourKit.Experimental.PlayerConnection "PlayerConnection" API. This is an **experimental** feature for advanced use cases where you need to send data that FourKit doesnt yet wrap in its API. + +> **This API is experimental and may change. You are responsible for constructing valid packets!!! malformed data can crash or disconnect the client.** + +Also please keep in mind, some of this info may not be accurate! Feel free to improve this by contributing on the Github. + +--- + +@section packet_overview Overview + +Every Minecraft packet on the wire looks like this: + +| Field | Size | Description | +|-------|------|-------------| +| Size | 4 bytes (big-endian) | Total length of the remaining data (packet ID + payload). **Written automatically by the server.. you do NOT include it.** | +| Packet ID | 1 byte | Which packet this is. Only the low byte matters on the wire even though IDs are `int` in source. | +| Payload | variable | The rest of the packet data, format depends on the packet ID. | + +When you call `PlayerConnection.send(byte[] data)`, the server prepends the 4-byte big-endian size header for you. Your byte array should start with the packet ID byte followed by the payload. + +--- + +@section wire_data_types Wire Data Types + +All multi-byte values are **big-endian** (most significant byte first). + +| Type | Size | Description | +|------|------|-------------| +| `byte` | 1 | Unsigned 8-bit integer (0-255). | +| `bool` | 1 | 0 = false, non-zero = true. | +| `short` | 2 | Signed 16-bit integer. | +| `int` | 4 | Signed 32-bit integer. | +| `long` | 8 | Signed 64-bit integer. | +| `float` | 4 | IEEE 754 single-precision float. | +| `utf` | 2 + n | Modified UTF-8 string: `short` byte-length prefix followed by that many bytes of data. See \ref string_encoding "String Encoding". | +| `item` | variable | Item data (item ID, count, damage). See \ref item_data "Item Data". | +| `metadata` | variable | Entity metadata (SynchedEntityData). See \ref metadata_encoding "Metadata Encoding". | + +--- + +@section string_encoding String Encoding + +Strings use modified UTF-8 encoding: + +1. A `short` (2 bytes, big-endian) containing the byte length of the string data. +2. That many bytes of modified-UTF8 characters. + +For most ASCII text (chat messages, names, etc.) this is identical to standard UTF-8. A helper to write a string: + +```csharp +static int WriteUTF(byte[] buffer, int offset, string text) +{ + byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(text); + WriteShort(buffer, offset, (short)utf8.Length); + Buffer.BlockCopy(utf8, 0, buffer, offset + 2, utf8.Length); + return 2 + utf8.Length; // total bytes written +} +``` + +--- + +@section item_data Item Data + +Several container packets include serialized item data. The wire format for a single item slot is: + +| Field | Type | Description | +|-------|------|-------------| +| id | `short` | Item ID. `-1` means the slot is empty (no further fields follow). | +| count | `byte` | Stack size. | +| damage | `short` | Damage/metadata value. | + +If the item ID is `-1`, only the `short` is written (2 bytes for an empty slot). Otherwise all three fields are written (5 bytes). + +--- + +@section metadata_encoding Metadata Encoding (SynchedEntityData) + +Entity metadata is used in packets like AddMobPacket (24), AddPlayerPacket (20), and SetEntityDataPacket (40). It's a list of typed key-value entries terminated by `0x7F`. + +Each entry starts with a 1-byte header: +- **Bits 5-7** (mask `0xE0`): Type ID, shifted right by 5. +- **Bits 0-4** (mask `0x1F`): Data slot ID (max 31). + +So the header byte is `(type << 5) | (id & 0x1F)`. + +The value immediately follows, sized based on the type: + +| Type ID | Name | Value Size | +|---------|------|------------| +| 0 | Byte | 1 byte | +| 1 | Short | 2 bytes | +| 2 | Int | 4 bytes | +| 3 | Float | 4 bytes | +| 4 | String | `utf` (2-byte length prefix + string data) | +| 5 | ItemInstance | `item` (see \ref item_data "Item Data") | + +After all entries, write `0x7F` as the EOF marker. + +@subsubsection metadata_data_ids Common Data Slot IDs + +These are the data slot IDs used by the entity class hierarchy. When constructing metadata, define these in order and use the correct type. + +**Entity (base):** +| ID | Type | Name | Description | +|----|------|------|-------------| +| 0 | Byte | DATA_SHARED_FLAGS | Bitfield: bit 0=on fire, 1=sneaking, 3=sprinting, 4=using item, 5=invisible, 6=idle anim | +| 1 | Short | DATA_AIR_SUPPLY | Air supply (default 300, max 300) | + +**LivingEntity:** +| ID | Type | Name | Description | +|----|------|------|-------------| +| 6 | Float | DATA_HEALTH | Health value (default 1.0) | +| 7 | Int | DATA_EFFECT_COLOR | Potion effect color (default 0) | +| 8 | Byte | DATA_EFFECT_AMBIENCE | Potion effect ambience (default 0) | +| 9 | Byte | DATA_ARROW_COUNT | Number of arrows stuck in entity (default 0) | + +**Mob:** +| ID | Type | Name | Description | +|----|------|------|-------------| +| 10 | String | DATA_CUSTOM_NAME | Custom name tag (default "") | +| 11 | Byte | DATA_CUSTOM_NAME_VISIBLE | Show name tag (0 or 1, default 0) | + +For example, to write metadata for a basic mob with a custom name: + +```csharp +// helper to write a metadata entry header +static void WriteMetaHeader(byte[] buffer, int offset, int type, int id) +{ + buffer[offset] = (byte)((type << 5) | (id & 0x1F)); +} + +// build metadata for a mob with a visible custom name "booty" +// enity base: flags=0, air=300 +// livingentity: health=20.0, effectColor=0, effectAmbience=0, arrowCount=0 +// mob: customName="booty", customNameVisible=1 + +byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes("booty"); +int metaSize = (1+1) + (1+2) + (1+4) + (1+4) + (1+1) + (1+1) + + (1 + 2 + nameBytes.Length) + (1+1) + 1; // +1 for EOF +byte[] meta = new byte[metaSize]; +int pos = 0; + +// Entity +WriteMetaHeader(meta, pos, 0, 0); pos++; meta[pos++] = 0; // flags = 0 +WriteMetaHeader(meta, pos, 1, 1); pos++; WriteShort(meta, pos, 300); pos += 2; // air supply + +// LivingEntity +WriteMetaHeader(meta, pos, 3, 6); pos++; WriteFloat(meta, pos, 20.0f); pos += 4; // health +WriteMetaHeader(meta, pos, 2, 7); pos++; WriteInt(meta, pos, 0); pos += 4; // effect color +WriteMetaHeader(meta, pos, 0, 8); pos++; meta[pos++] = 0; // effect ambience +WriteMetaHeader(meta, pos, 0, 9); pos++; meta[pos++] = 0; // arrow count + +// Mob +WriteMetaHeader(meta, pos, 4, 10); pos++; +pos += WriteUTF(meta, pos, "booty"); // custom name +WriteMetaHeader(meta, pos, 0, 11); pos++; meta[pos++] = 1; // name visible + +meta[pos] = 0x7F; // EOF marker +``` + +--- + +@section writing_data Writing Packet Data + +Helper methods for writing big-endian values into a `byte[]` buffer: + +```csharp +static void WriteByte(byte[] buffer, int offset, int value) +{ + buffer[offset] = (byte)(value & 0xFF); +} + +static void WriteBool(byte[] buffer, int offset, bool value) +{ + buffer[offset] = (byte)(value ? 1 : 0); +} + +static void WriteShort(byte[] buffer, int offset, short value) +{ + buffer[offset] = (byte)((value >> 8) & 0xFF); + buffer[offset + 1] = (byte)(value & 0xFF); +} + +static void WriteInt(byte[] buffer, int offset, int value) +{ + buffer[offset] = (byte)((value >> 24) & 0xFF); + buffer[offset + 1] = (byte)((value >> 16) & 0xFF); + buffer[offset + 2] = (byte)((value >> 8) & 0xFF); + buffer[offset + 3] = (byte)(value & 0xFF); +} + +static void WriteFloat(byte[] buffer, int offset, float value) +{ + byte[] bytes = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian) + Array.Reverse(bytes); + Buffer.BlockCopy(bytes, 0, buffer, offset, 4); +} + +static void WriteLong(byte[] buffer, int offset, long value) +{ + buffer[offset] = (byte)((value >> 56) & 0xFF); + buffer[offset + 1] = (byte)((value >> 48) & 0xFF); + buffer[offset + 2] = (byte)((value >> 40) & 0xFF); + buffer[offset + 3] = (byte)((value >> 32) & 0xFF); + buffer[offset + 4] = (byte)((value >> 24) & 0xFF); + buffer[offset + 5] = (byte)((value >> 16) & 0xFF); + buffer[offset + 6] = (byte)((value >> 8) & 0xFF); + buffer[offset + 7] = (byte)(value & 0xFF); +} + +static void WriteEmptyItem(byte[] buffer, int offset) +{ + WriteShort(buffer, offset, (short)-1); +} + +static void WriteItem(byte[] buffer, int offset, short id, byte count, short damage) +{ + WriteShort(buffer, offset, id); + buffer[offset + 2] = count; + WriteShort(buffer, offset + 3, damage); +} +``` + +--- + +@section precision_scaling Precision Scaling + +Some fields use fixed-point encoding integers on the wire representing floats/doubles with a multiplier applied: + +| Data Type | Multiplier | Wire Type | Description | +|-----------|------------|-----------|-------------| +| Entity position | x 32 | `int` | `(int)(position * 32)`. 1/32 block precision. | +| Sound position | x 8 | `int` | `(int)(position * 8)`. 1/8 block precision. | +| Rotation | x 256 / 360 | `byte` | `(byte)(angle * 256.0 / 360.0)`. | +| Velocity | x 8000 | `short` | `(short)(velocity * 8000.0)`, clamped to +/-3.9 blocks/tick before encoding. | + +```csharp +int wireX = (int)(player.getLocation().getX() * 32); +int wireY = (int)(player.getLocation().getY() * 32); +int wireZ = (int)(player.getLocation().getZ() * 32); + +byte wireYaw = (byte)(player.getLocation().getYaw() * 256.0 / 360.0); +byte wirePitch = (byte)(player.getLocation().getPitch() * 256.0 / 360.0); +``` + +--- + +@section example_sound Example: Playing a Sound Effect + +Sends a LevelSoundPacket (ID 62) to play a ghast scream at the player's location whenever they chat. + +```csharp +[EventHandler] +public void onPlayerChat(PlayerChatEvent e) +{ + // LevelSoundPacket layout: + // [0] byte packet ID (62) + // [1..4] int sound type + // [5..8] int x * 8 + // [9..12] int y * 8 + // [13..16] int z * 8 + // [17..20] float volume + // [21..24] float pitch + byte[] buffer = new byte[25]; + buffer[0] = (byte)62; // packet id + + WriteInt(buffer, 1, (int)19); // eSoundType_MOB_GHAST_SCREAM (Sound.GHAST_SCREAM) + + WriteInt(buffer, 5, (int)(e.getPlayer().getLocation().getX() * 8)); + WriteInt(buffer, 9, (int)(e.getPlayer().getLocation().getY() * 8)); + WriteInt(buffer, 13, (int)(e.getPlayer().getLocation().getZ() * 8)); + + WriteFloat(buffer, 17, 10); // volume + WriteFloat(buffer, 21, 1); // pitch + + e.getPlayer().getConnection().send(buffer); +} +``` + +Sound type IDs correspond to the \ref Minecraft.Server.FourKit.Sound "Sound" enum. Cast any `Sound` value to `int` to get the wire value, for example `(int)Sound.GHAST_SCREAM` is `19`. + +--- + +@section example_gamee Example: Changing the Game Mode + +GameEventPacket (ID 70) notifies the client of game state changes. + +```csharp +[EventHandler] +public void onJoin(PlayerJoinEvent e) +{ + // layout: + // [0] byte packet ID (70) + // [1] byte event type + // [2] byte parameter + byte[] buffer = new byte[3]; + buffer[0] = (byte)70; // packet id + buffer[1] = (byte)3; // CHANGE_GAME_MODE + buffer[2] = (byte)1; // 0 = Survival, 1 = Creative, 2 = Adventure + e.getPlayer().getConnection().send(buffer); +} +``` + +--- + +@section example_entity_teleport Example: Teleporting an Entity (Client-Side) + +TeleportEntityPacket (ID 34) moves an entity to an absolute position on the client. + +```csharp +// Teleport entity 42 to (100.5, 64.0, -200.25) +byte[] buffer = new byte[17] +buffer[0] = (byte)34; // packet id + +WriteShort(buffer, 1, (short)42); // entity id +WriteInt(buffer, 3, (int)(100.5 * 32)); // x * 32 +WriteInt(buffer, 7, (int)(64.0 * 32)); // y * 32 +WriteInt(buffer, 11, (int)(-200.25 * 32)); // z * 32 +buffer[15] = (byte)(90.0 * 256.0 / 360.0); // yRot +buffer[16] = (byte)(0.0 * 256.0 / 360.0); // xRot + +player.getConnection().send(buffer); +``` + +--- + +@section example_set_time Example: Setting the World Time + +SetTimePacket (ID 4) updates the client's world time. + +```csharp +// set time to noon (6000 ticks) +byte[] buffer = new byte[17] +buffer[0] = (byte)4; // packet id + +WriteLong(buffer, 1, 6000L); // game time (total ticks elapsed) +WriteLong(buffer, 9, 6000L); // day time (time of day, 0-24000) + +player.getConnection().send(buffer); +``` + +--- + +@section example_health Example: Updating Health Display + +SetHealthPacket (ID 8) updates the client's health, food, and saturation display. + +```csharp +// set health to 20 (full), food to 20 (full), saturation to 5.0 +byte[] buffer = new byte[12]; +buffer[0] = (byte)8; // packet id + +WriteFloat(buffer, 1, 20.0f); // health +WriteShort(buffer, 5, (short)20); // food +WriteFloat(buffer, 7, 5.0f); // saturation +buffer[11] = (byte)0; // damage source (0 = unknown) + +player.getConnection().send(buffer); +``` + +--- + +@section example_abilities Example: Setting Player Abilities + +PlayerAbilitiesPacket (ID 202) updates the player's ability flags and speeds. + +```csharp +// enable flying for the player +byte[] buffer = new byte[10]; +buffer[0] = (byte)202; // packet id + +// 0x01=invulnerable, 0x02=flying, 0x04=canFly, 0x08=instabuild (creative) +buffer[1] = (byte)(0x02 | 0x04); // flying + canFly + +WriteFloat(buffer, 2, 0.05f); // fly speed (default 0.05) +WriteFloat(buffer, 6, 0.1f); // walk speed (default 0.1) + +player.getConnection().send(buffer); +``` + +--- + +@section example_explosion Example: Creating an Explosion Effect + +ExplodePacket (ID 60) creates a client-side explosion with optional block destruction and knockback. + +```csharp +// create an explosion at (100.0, 64.0, 200.0) with radius 4.0 and no destroyed blocks +byte[] buffer = new byte[46]; +buffer[0] = (byte)60; // packet id + +buffer[1] = (byte)0; // knockbackOnly = false (full explosion with position data) + +// position (double precision, 8 bytes each) +byte[] xBytes = BitConverter.GetBytes(100.0); +byte[] yBytes = BitConverter.GetBytes(64.0); +byte[] zBytes = BitConverter.GetBytes(200.0); +Buffer.BlockCopy(xBytes, 0, buffer, 2, 8); +Buffer.BlockCopy(yBytes, 0, buffer, 10, 8); +Buffer.BlockCopy(zBytes, 0, buffer, 18, 8); + +WriteFloat(buffer, 26, 4.0f); // radius +WriteInt(buffer, 30, 0); // destroyed block count (0 = no blocks) + +// knockback velocity applied to player +WriteFloat(buffer, 34, 0.0f); // knockback X +WriteFloat(buffer, 38, 0.5f); // knockback Y (push player up) +WriteFloat(buffer, 42, 0.0f); // knockback Z + +player.getConnection().send(buffer); +``` + +--- + +@section example_xp Example: Setting the XP Bar + +SetExperiencePacket (ID 43) updates the XP bar. + +```csharp +// Set XP bar to 50% progress, level 10, total 300 XP +byte[] buffer = new byte[1 + 4 + 2 + 2]; // 9 bytes +buffer[0] = (byte)43; // packet id + +WriteFloat(buffer, 1, 0.5f); // bar progress (0.0 to 1.0) +WriteShort(buffer, 5, (short)10); // level +WriteShort(buffer, 7, (short)300); // total XP points + +player.getConnection().send(buffer); +``` + +--- + +@section packet_reference Complete packet reference + +All server-to-client packet layouts. The packet ID byte is always `buffer[0]` and is not listed in the field tables below. only the payload after the ID is shown. + +@subsection packet_ref_general General / Connection Packets + +@subsubsection pkt_0 Packet 0 - KeepAlivePacket + +Connection keepalive. The client echoes this back. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | id | Keepalive ID. The client should respond with the same value. | + +**Total payload: 4 bytes.** + +@subsubsection pkt_3 Packet 3 - ChatPacket + +Send a chat/system message to the client. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | messageType | Message type enum value. | +| 2 | `short` | packedCounts | Packed field: high nibble = string arg count, low nibble = int arg count. Computed as `(stringCount << 4) \| intCount`. | +| 4 | `utf[]` | stringArgs | Variable number of `utf` strings (the message text, source name, item name, etc). | +| ... | `int[]` | intArgs | Variable number of `int` values (for example source entity type). | + +**Total payload: variable.** For a simple chat message, `messageType` = 0, one string arg (the message), zero int args. The packed counts would be `(1 << 4) | 0` = `0x0010`. + +@subsubsection pkt_255 Packet 255 - DisconnectPacket + +Disconnect the client. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | reason | Disconnect reason code. | + +**Total payload: 4 bytes.** + +@subsection packet_ref_world World & Environment Packets + +@subsubsection pkt_4 Packet 4 - SetTimePacket + +Set world time. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `long` | gameTime | Total game time in ticks (monotonically increasing). | +| 8 | `long` | dayTime | Time of day in ticks (0-24000 range). | + +**Total payload: 16 bytes.** + +@subsubsection pkt_6 Packet 6 - SetSpawnPositionPacket + +Set the world spawn point. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | x | Spawn X coordinate. | +| 4 | `int` | y | Spawn Y coordinate. | +| 8 | `int` | z | Spawn Z coordinate. | + +**Total payload: 12 bytes.** + +@subsubsection pkt_9 Packet 9 - RespawnPacket + +Sent on respawn or dimension change. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | dimension | Dimension ID (0=Overworld, -1=Nether, 1=End). | +| 1 | `byte` | gameType | Game mode ID (0=Survival, 1=Creative, 2=Adventure). | +| 2 | `short` | mapHeight | World height. | +| 4 | `utf` | levelType | Level type name (for example "DEFAULT", "FLAT"). | +| ... | `long` | mapSeed | World seed. | +| ... | `byte` | difficulty | Difficulty (0=Peaceful, 1=Easy, 2=Normal, 3=Hard). | +| ... | `bool` | newSeaLevel | Whether the new sea level is active. | +| ... | `short` | newEntityId | The player's new entity ID. | +| ... | `short` | xzSize | World XZ size. | +| ... | `byte` | hellScale | Nether scale factor. | + +**Total payload: variable** (depends on level type string length). + +@subsubsection pkt_53 Packet 53 - TileUpdatePacket + +Update a single block. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | x | Block X coordinate. | +| 4 | `byte` | y | Block Y coordinate (0-255). | +| 5 | `int` | z | Block Z coordinate. | +| 9 | `short` | block | Block type ID. | +| 11 | `byte` | dataLevel | Block data/metadata value. | + +**Total payload: 12 bytes.** + +@subsubsection pkt_54 Packet 54 - TileEventPacket + +Trigger a block action (note blocks, pistons, chests). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | x | Block X coordinate. | +| 4 | `short` | y | Block Y coordinate. | +| 6 | `int` | z | Block Z coordinate. | +| 10 | `byte` | b0 | Action parameter 1 (depends on block type). | +| 11 | `byte` | b1 | Action parameter 2 (depends on block type). | +| 12 | `short` | tile | Block type ID. | + +**Total payload: 14 bytes.** + +@subsubsection pkt_55 Packet 55 - TileDestructionPacket + +Show a block breaking animation (crack overlay). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | id | Breaker entity ID. | +| 4 | `int` | x | Block X coordinate. | +| 8 | `int` | y | Block Y coordinate. | +| 12 | `int` | z | Block Z coordinate. | +| 16 | `byte` | state | Destroy stage (0-9). Any other value removes the overlay. | + +**Total payload: 17 bytes.** + +@subsubsection pkt_60 Packet 60 - ExplodePacket + +Explosion with optional block destruction and knockback. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `bool` | knockbackOnly | If true, only knockback fields follow (no position/radius/blocks). | + +If `knockbackOnly` is **false** (typical explosion): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 1 | `double` | x | Explosion center X. | +| 9 | `double` | y | Explosion center Y. | +| 17 | `double` | z | Explosion center Z. | +| 25 | `float` | radius | Explosion radius. | +| 29 | `int` | count | Number of destroyed block offsets. | +| 33 | `byte[count*3]` | offsets | For each block: 3 signed bytes (dx, dy, dz) relative to the center. | +| ... | `float` | knockbackX | Player knockback velocity X. | +| ... | `float` | knockbackY | Player knockback velocity Y. | +| ... | `float` | knockbackZ | Player knockback velocity Z. | + +If `knockbackOnly` is **true** (just apply knockback, no visual explosion): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 1 | `float` | knockbackX | Player knockback velocity X. | +| 5 | `float` | knockbackY | Player knockback velocity Y. | +| 9 | `float` | knockbackZ | Player knockback velocity Z. | + +**Total payload: variable.** + +@subsubsection pkt_61 Packet 61 - LevelEventPacket + +World event (sounds, particles, door effects, etc). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | type | Event type ID (for example 1000=click sound, 1005=bow sound, 2000=smoke, 2001=break block). | +| 4 | `int` | x | Block X coordinate. | +| 8 | `byte` | y | Block Y coordinate. | +| 9 | `int` | z | Block Z coordinate. | +| 13 | `int` | data | Event-specific data (for example block ID for break effect, direction for smoke). | +| 17 | `bool` | globalEvent | If true, event is global (all players hear it regardless of distance). | + +**Total payload: 18 bytes.** + +@subsubsection pkt_62 Packet 62 - LevelSoundPacket + +Play a sound at a position. Positions use x8 scaling. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | soundId | Sound type ID. Maps to the \ref Minecraft.Server.FourKit.Sound "Sound" enum. | +| 4 | `int` | x | X coordinate * 8. | +| 8 | `int` | y | Y coordinate * 8. | +| 12 | `int` | z | Z coordinate * 8. | +| 16 | `float` | volume | Sound volume (1.0 = normal). | +| 20 | `float` | pitch | Sound pitch (1.0 = normal). | + +**Total payload: 24 bytes.** + +@subsubsection pkt_63 Packet 63 - LevelParticlesPacket + +Spawn particles at a position. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `utf` | name | Particle name string (for example "flame", "smoke", "heart"). | +| ... | `float` | x | Center X coordinate. | +| ... | `float` | y | Center Y coordinate. | +| ... | `float` | z | Center Z coordinate. | +| ... | `float` | xDist | Random X offset range. | +| ... | `float` | yDist | Random Y offset range. | +| ... | `float` | zDist | Random Z offset range. | +| ... | `float` | maxSpeed | Maximum particle speed. | +| ... | `int` | count | Number of particles to spawn. | + +**Total payload: variable** (depends on particle name string length) **+ 32 bytes** for the fixed fields. + +@subsubsection pkt_70 Packet 70 - GameEventPacket + +Game state change notification. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | event | Event type. | +| 1 | `byte` | param | Event parameter. | + +Event types: +| Value | Name | Parameter | +|-------|------|-----------| +| 0 | No Bed | (unused) | +| 1 | Start Rain | (unused) | +| 2 | Stop Rain | (unused) | +| 3 | Change Game Mode | 0=Survival, 1=Creative, 2=Adventure | +| 4 | Win Game | 0=show credits, 1=just respawn | + +**Total payload: 2 bytes.** + +@subsubsection pkt_130 Packet 130 - SignUpdatePacket + +Update sign text. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | x | Sign X coordinate. | +| 4 | `short` | y | Sign Y coordinate. | +| 6 | `int` | z | Sign Z coordinate. | +| 10 | `bool` | verified | Whether the sign text has been verified. | +| 11 | `bool` | censored | Whether the sign text has been censored. | +| 12 | `utf` | line1 | First line of text. | +| ... | `utf` | line2 | Second line of text. | +| ... | `utf` | line3 | Third line of text. | +| ... | `utf` | line4 | Fourth line of text. | + +**Total payload: variable** (12 bytes fixed + 4 * `utf` strings). + +@subsection packet_ref_entity Entity Packets + +@subsubsection pkt_8 Packet 8 - SetHealthPacket + +Update health, food, and saturation. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `float` | health | Current health (0.0-20.0, 20 = full). | +| 4 | `short` | food | Food level (0-20). | +| 6 | `float` | saturation | Food saturation level. | +| 10 | `byte` | damageSource | Damage source type (for telemetry). | + +**Total payload: 11 bytes.** + +@subsubsection pkt_18 Packet 18 - AnimatePacket + +Play an entity animation. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | action | Animation type (1=swing arm, 2=damage, 3=leave bed, 104=crouch, 105=uncrouch). | + +**Total payload: 5 bytes.** + +@subsubsection pkt_20 Packet 20 - AddPlayerPacket + +Spawn a named player entity. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `utf` | name | Player name. | +| ... | `int` | x | X coordinate * 32 (fixed-point). | +| ... | `int` | y | Y coordinate * 32 (fixed-point). | +| ... | `int` | z | Z coordinate * 32 (fixed-point). | +| ... | `byte` | yRot | Yaw (angle * 256 / 360). | +| ... | `byte` | xRot | Pitch (angle * 256 / 360). | +| ... | `byte` | yHeadRot | Head yaw (angle * 256 / 360). | +| ... | `short` | carriedItem | Item ID of held item. | +| ... | `playerUID` | xuid | Player XUID (8 bytes). | +| ... | `playerUID` | onlineXuid | Online XUID for splitscreen guests (8 bytes). | +| ... | `byte` | playerIndex | Local player index. | +| ... | `int` | skinId | Custom skin ID. | +| ... | `int` | capeId | Custom cape ID. | +| ... | `int` | gamePrivileges | Player game privileges bitfield. | +| ... | `metadata` | entityData | Entity metadata (SynchedEntityData). | + +**Total payload: variable** (includes entity metadata). + +@subsubsection pkt_23 Packet 23 - AddEntityPacket + +Spawn a non-mob entity (minecart, arrow, falling block, etc). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `byte` | type | Entity type ID (for example 1=boat, 10=minecart, 50=TNT, 60=arrow, 70=falling block). | +| 3 | `int` | x | X coordinate * 32 (fixed-point). | +| 7 | `int` | y | Y coordinate * 32 (fixed-point). | +| 11 | `int` | z | Z coordinate * 32 (fixed-point). | +| 15 | `byte` | yRot | Yaw (angle * 256 / 360). | +| 16 | `byte` | xRot | Pitch (angle * 256 / 360). | +| 17 | `int` | data | Entity-specific data (for example block ID for falling blocks, owner entity ID for projectiles). | + +If `data` is non-zero, three additional velocity fields follow: + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 21 | `short` | xVel | X velocity * 8000. | +| 23 | `short` | yVel | Y velocity * 8000. | +| 25 | `short` | zVel | Z velocity * 8000. | + +**Total payload: 21 bytes** (no velocity) or **27 bytes** (with velocity). + +@subsubsection pkt_24 Packet 24 - AddMobPacket + +Spawn a mob. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `byte` | mobType | Mob type ID (for example 50=Creeper, 51=Skeleton, 52=Spider, 54=Zombie, 90=Pig, 91=Sheep). | +| 3 | `int` | x | X coordinate * 32 (fixed-point). | +| 7 | `int` | y | Y coordinate * 32 (fixed-point). | +| 11 | `int` | z | Z coordinate * 32 (fixed-point). | +| 15 | `byte` | yRot | Yaw (angle * 256 / 360). | +| 16 | `byte` | xRot | Pitch (angle * 256 / 360). | +| 17 | `byte` | yHeadRot | Head yaw (angle * 256 / 360). | +| 18 | `short` | xVel | X velocity * 8000. | +| 20 | `short` | yVel | Y velocity * 8000. | +| 22 | `short` | zVel | Z velocity * 8000. | +| 24 | `metadata` | entityData | Entity metadata (SynchedEntityData). | + +**Total payload: 24 bytes + variable metadata.** + +@subsubsection pkt_26 Packet 26 - AddExperienceOrbPacket + +Spawn an XP orb. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `int` | x | X coordinate * 32 (fixed-point). | +| 8 | `int` | y | Y coordinate * 32 (fixed-point). | +| 12 | `int` | z | Z coordinate * 32 (fixed-point). | +| 16 | `short` | value | XP value of the orb. | + +**Total payload: 18 bytes.** + +@subsubsection pkt_28 Packet 28 - SetEntityMotionPacket + +Set entity velocity. Has two encoding modes based on a flag bit in the entity ID field. + +The first field is a `short` combining the entity ID and a flag: +- **Bits 0-10** (mask `0x07FF`): Entity ID (max 2047). +- **Bit 11** (mask `0x0800`): If set, velocity uses 3 bytes (lower precision). If clear, 3 shorts (full precision). + +**Full precision mode** (flag clear): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | idAndFlag | Entity ID (low 11 bits), flag=0. | +| 2 | `short` | xVel | X velocity * 8000. | +| 4 | `short` | yVel | Y velocity * 8000. | +| 6 | `short` | zVel | Z velocity * 8000. | + +**Total payload: 8 bytes.** + +**Compact mode** (flag set, bit 11 = 1): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | idAndFlag | Entity ID (low 11 bits) OR `0x0800`. | +| 2 | `byte` | xVel | X velocity / 16 (sign-extended, then multiplied by 16 on read). | +| 3 | `byte` | yVel | Y velocity / 16. | +| 4 | `byte` | zVel | Z velocity / 16. | + +**Total payload: 5 bytes.** The server automatically picks compact mode when velocity values fit. + +@subsubsection pkt_29 Packet 29 - RemoveEntitiesPacket + +Despawn one or more entities. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | count | Number of entities to remove. | +| 1 | `int[count]` | entityIds | Array of entity IDs (4 bytes each). | + +**Total payload: 1 + (count * 4) bytes.** + +@subsubsection pkt_30_33 Packets 30-33 - MoveEntityPacket + +Relative entity movement/rotation updates. Four sub-types: + +**Packet 30 - MoveEntityPacket** (base, no movement): `short` entityId only. **Payload: 2 bytes.** + +**Packet 31 - MoveEntityPacket.Pos** (position only): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `byte` | dx | X delta (signed, in 1/32 block units). | +| 3 | `byte` | dy | Y delta. | +| 4 | `byte` | dz | Z delta. | + +**Payload: 5 bytes.** + +**Packet 32 - MoveEntityPacket.Rot** (rotation only): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `byte` | yRot | New yaw (angle * 256 / 360). | +| 3 | `byte` | xRot | New pitch (angle * 256 / 360). | + +**Payload: 4 bytes.** + +**Packet 33 - MoveEntityPacket.PosRot** (position + rotation): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `byte` | dx | X delta. | +| 3 | `byte` | dy | Y delta. | +| 4 | `byte` | dz | Z delta. | +| 5 | `byte` | yRot | New yaw. | +| 6 | `byte` | xRot | New pitch. | + +**Payload: 7 bytes.** + +@subsubsection pkt_34 Packet 34 - TeleportEntityPacket + +Teleport an entity to an absolute position. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `int` | x | X coordinate * 32 (fixed-point). | +| 6 | `int` | y | Y coordinate * 32 (fixed-point). | +| 10 | `int` | z | Z coordinate * 32 (fixed-point). | +| 14 | `byte` | yRot | Yaw (angle * 256 / 360). | +| 15 | `byte` | xRot | Pitch (angle * 256 / 360). | + +**Total payload: 16 bytes.** + +@subsubsection pkt_35 Packet 35 - RotateHeadPacket + +Update an entity's head rotation. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | yHeadRot | Head yaw (angle * 256 / 360). | + +**Total payload: 5 bytes.** + +@subsubsection pkt_38 Packet 38 - EntityEventPacket + +Trigger an entity event (hurt, death, eating, etc). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | eventId | Event type (2=hurt, 3=death, 9=eating finished). | + +**Total payload: 5 bytes.** + +@subsubsection pkt_39 Packet 39 - SetEntityLinkPacket + +Attach or detach entities (leash, riding). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | sourceId | Entity being attached (the rider/leashed entity). | +| 4 | `int` | destId | Entity being attached to (the vehicle/fence). `-1` to detach. | +| 8 | `byte` | type | Link type. | + +**Total payload: 9 bytes.** + +@subsubsection pkt_40 Packet 40 - SetEntityDataPacket + +Update entity metadata. See \ref metadata_encoding "Metadata Encoding" for how to construct the metadata blob. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `metadata` | data | Packed entity metadata entries. | + +**Total payload: 4 bytes + variable metadata.** + +@subsubsection pkt_41 Packet 41 - UpdateMobEffectPacket + +Apply or update a potion effect. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | effectId | Effect ID (1=speed, 2=slowness, 3=haste, 4=mining fatigue, 5=strength, ...). | +| 5 | `byte` | amplifier | Effect level (0 = level I, 1 = level II, etc). | +| 6 | `short` | duration | Duration in ticks. | + +**Total payload: 8 bytes.** + +@subsubsection pkt_42 Packet 42 - RemoveMobEffectPacket + +Remove a potion effect. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | effectId | Effect ID to remove. | + +**Total payload: 5 bytes.** + +@subsubsection pkt_43 Packet 43 - SetExperiencePacket + +Update the XP bar. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `float` | progress | XP bar fill amount (0.0-1.0). | +| 4 | `short` | level | Current level. | +| 6 | `short` | totalXP | Total experience points. | + +**Total payload: 8 bytes.** + +@subsubsection pkt_71 Packet 71 - AddGlobalEntityPacket + +Spawn a global entity (lightning bolt). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | type | Entity type (1 = lightning bolt). | +| 5 | `int` | x | X coordinate * 32 (fixed-point). | +| 9 | `int` | y | Y coordinate * 32 (fixed-point). | +| 13 | `int` | z | Z coordinate * 32 (fixed-point). | + +**Total payload: 17 bytes.** + +@subsection packet_ref_player Player Packets + +@subsubsection pkt_5 Packet 5 - SetEquippedItemPacket + +Change the visible held item for an entity. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `short` | slot | Equipment slot (0=held). | +| 6 | `item` | item | Item data (see \ref item_data "Item Data"). | + +**Total payload: 6 bytes + item data** (2 bytes if empty, 5 bytes if present). + +@subsubsection pkt_200 Packet 200 - AwardStatPacket + +Award a statistic or achievement. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | statId | Statistic/achievement ID. | +| 4 | `int` | length | Length of parameter data blob in bytes. | +| 8 | `byte[length]` | data | Parameter data (typically a 4-byte `int` count). | + +**Total payload: 8 bytes + length bytes.** For a simple stat increment, `length` = 4 and `data` contains an `int` count. + +@subsubsection pkt_202 Packet 202 - PlayerAbilitiesPacket + +Update player abilities. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | flags | Bitfield: `0x01`=invulnerable, `0x02`=flying, `0x04`=canFly, `0x08`=instabuild (creative). | +| 1 | `float` | flySpeed | Fly speed (default 0.05). | +| 5 | `float` | walkSpeed | Walk speed (default 0.1). | + +**Total payload: 9 bytes.** + +@subsection packet_ref_container Container Packets + +@subsubsection pkt_100 Packet 100 - ContainerOpenPacket + +Open a container window. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | containerId | Window ID. | +| 1 | `byte` | type | Container type (0=chest, 1=workbench, 2=furnace, 3=dispenser, 4=enchanting table). | +| 2 | `byte` | size | Number of slots. | +| 3 | `bool` | customName | Whether a custom title follows. | + +If `type` == HORSE (type 12): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 4 | `int` | entityId | Horse entity ID. | + +If `customName` is true: + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| ... | `utf` | title | Custom container title. | + +**Total payload: 4 bytes minimum**, variable with conditionals. + +@subsubsection pkt_101 Packet 101 - ContainerClosePacket + +Close a container window. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | containerId | Window ID. | + +**Total payload: 1 byte.** + +@subsubsection pkt_103 Packet 103 - ContainerSetSlotPacket + +Set a single slot in a container. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | containerId | Window ID. | +| 1 | `short` | slot | Slot index. | +| 3 | `item` | item | Item data (see \ref item_data "Item Data"). | + +**Total payload: 3 bytes + item data** (2 bytes if empty, 5 bytes if present). + +@subsubsection pkt_104 Packet 104 - ContainerSetContentPacket + +Set all slots in a container. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | containerId | Window ID. | +| 1 | `short` | count | Number of item slots. | +| 3 | `item[count]` | items | Array of item data entries. | + +**Total payload: 3 bytes + count * item data.** + +@subsection packet_ref_all_ids All Packet IDs + +Every registered packet. **S-C** = server to client, **C-S** = client to server. + +Just know that some of these packets have no function (such as scoreboards), most should work. + +| ID | Name | S-C | C-S | Notes | +|----|------|:---:|:---:|-------| +| 0 | KeepAlivePacket | ✓ | ✓ | Connection keepalive. | +| 1 | LoginPacket | ✓ | ✓ | Login handshake. | +| 2 | PreLoginPacket | ✓ | ✓ | Pre-login handshake. | +| 3 | ChatPacket | ✓ | ✓ | Chat messages. | +| 4 | SetTimePacket | ✓ | | World time. | +| 5 | SetEquippedItemPacket | ✓ | | Held item display. | +| 6 | SetSpawnPositionPacket | ✓ | | World spawn point. | +| 7 | InteractPacket | | ✓ | Player interact with entity. | +| 8 | SetHealthPacket | ✓ | | Health/food/saturation. | +| 9 | RespawnPacket | ✓ | ✓ | Respawn/dimension change. | +| 10 | MovePlayerPacket | ✓ | ✓ | Player position (base). | +| 11 | MovePlayerPacket.Pos | ✓ | ✓ | Player position only. | +| 12 | MovePlayerPacket.Rot | ✓ | ✓ | Player rotation only. | +| 13 | MovePlayerPacket.PosRot | ✓ | ✓ | Player position + rotation. | +| 14 | PlayerActionPacket | | ✓ | Block breaking, item dropping. | +| 15 | UseItemPacket | | ✓ | Place block / use item. | +| 16 | SetCarriedItemPacket | ✓ | ✓ | Hotbar slot selection. | +| 17 | EntityActionAtPositionPacket | ✓ | | Sleep in bed. | +| 18 | AnimatePacket | ✓ | ✓ | Entity animations. | +| 19 | PlayerCommandPacket | | ✓ | Sneak, sprint, etc. | +| 20 | AddPlayerPacket | ✓ | | Spawn named player. | +| 22 | TakeItemEntityPacket | ✓ | | Item pickup animation. | +| 23 | AddEntityPacket | ✓ | | Spawn non-mob entity. | +| 24 | AddMobPacket | ✓ | | Spawn mob. | +| 25 | AddPaintingPacket | ✓ | | Spawn painting. | +| 26 | AddExperienceOrbPacket | ✓ | | Spawn XP orb. | +| 27 | PlayerInputPacket | | ✓ | Vehicle steering input. | +| 28 | SetEntityMotionPacket | ✓ | | Entity velocity. | +| 29 | RemoveEntitiesPacket | ✓ | | Despawn entities. | +| 30 | MoveEntityPacket | ✓ | | Entity movement (base). | +| 31 | MoveEntityPacket.Pos | ✓ | | Entity position delta. | +| 32 | MoveEntityPacket.Rot | ✓ | | Entity rotation. | +| 33 | MoveEntityPacket.PosRot | ✓ | | Entity pos + rot delta. | +| 34 | TeleportEntityPacket | ✓ | | Entity absolute position. | +| 35 | RotateHeadPacket | ✓ | | Entity head rotation. | +| 38 | EntityEventPacket | ✓ | | Entity events (hurt, death). | +| 39 | SetEntityLinkPacket | ✓ | | Leash / riding. | +| 40 | SetEntityDataPacket | ✓ | | Entity metadata update. | +| 41 | UpdateMobEffectPacket | ✓ | | Apply potion effect. | +| 42 | RemoveMobEffectPacket | ✓ | | Remove potion effect. | +| 43 | SetExperiencePacket | ✓ | | XP bar update. | +| 44 | UpdateAttributesPacket | ✓ | | Entity attributes. | +| 50 | ChunkVisibilityPacket | ✓ | | Chunk visibility. | +| 51 | BlockRegionUpdatePacket | ✓ | | Chunk data. | +| 52 | ChunkTilesUpdatePacket | ✓ | | Multi-block change. | +| 53 | TileUpdatePacket | ✓ | | Single block change. | +| 54 | TileEventPacket | ✓ | | Block action. | +| 55 | TileDestructionPacket | ✓ | | Block breaking animation. | +| 60 | ExplodePacket | ✓ | | Explosion. | +| 61 | LevelEventPacket | ✓ | | World event (sounds, particles). | +| 62 | LevelSoundPacket | ✓ | | Sound effect. | +| 63 | LevelParticlesPacket | ✓ | | Particle effect. | +| 70 | GameEventPacket | ✓ | | Game state change. | +| 71 | AddGlobalEntityPacket | ✓ | | Lightning bolt. | +| 100 | ContainerOpenPacket | ✓ | | Open container. | +| 101 | ContainerClosePacket | ✓ | ✓ | Close container. | +| 102 | ContainerClickPacket | | ✓ | Click container slot. | +| 103 | ContainerSetSlotPacket | ✓ | ✓ | Set container slot. | +| 104 | ContainerSetContentPacket | ✓ | | Set all container slots. | +| 105 | ContainerSetDataPacket | ✓ | | Container progress bar data. | +| 106 | ContainerAckPacket | ✓ | ✓ | Transaction acknowledgement. | +| 107 | SetCreativeModeSlotPacket | ✓ | ✓ | Creative inventory action. | +| 108 | ContainerButtonClickPacket | | ✓ | Enchanting / other button. | +| 130 | SignUpdatePacket | ✓ | ✓ | Sign text update. | +| 131 | ComplexItemDataPacket | ✓ | | Map data. | +| 132 | TileEntityDataPacket | ✓ | | Tile entity NBT data. | +| 133 | TileEditorOpenPacket | ✓ | | Open tile entity editor. | +| 200 | AwardStatPacket | ✓ | | Award statistic. | +| 201 | PlayerInfoPacket | ✓ | ✓ | Player list info. | +| 202 | PlayerAbilitiesPacket | ✓ | ✓ | Player abilities. | +| 206 | SetObjectivePacket | ✓ | | Scoreboard objective. | +| 207 | SetScorePacket | ✓ | | Scoreboard score. | +| 208 | SetDisplayObjectivePacket | ✓ | | Scoreboard display slot. | +| 209 | SetPlayerTeamPacket | ✓ | | Scoreboard team. | +| 255 | DisconnectPacket | ✓ | ✓ | Disconnect. | + +--- + +@section where_to_look Where to Look + +- **\ref Minecraft.Server.FourKit.Experimental.PlayerConnection "PlayerConnection"** - Get it via `player.getConnection()`. This is what you call `send()` on. +- **\ref Minecraft.Server.FourKit.Sound "Sound"** - All sound type IDs for LevelSoundPacket (62). Cast to `int` for the wire value. +- **\ref Minecraft.Server.FourKit.GameMode "GameMode"** - Game mode constants for GameEventPacket (70) and PlayerAbilitiesPacket (202). +- **\ref Minecraft.Server.FourKit.Location "Location"** - Get world coordinates from `player.getLocation()` for position encoding. +- **Packet source files** - The definitive reference is the `write()` method of each packet class in thre code (for example `LevelSoundPacket.cpp`, `TeleportEntityPacket.cpp`). + +--- + +@section extra-info Extra info + +- **Packet IDs** are a single byte. Only the low byte matters on the wire. +- **Position values** in entity packets use *32 fixed-point. Sound event positions use *8. +- **Rotation angles** are a single byte: `(byte)(angle * 256.0 / 360.0)`. +- **Velocity** is `(short)(velocity * 8000.0)`, clamped to +/-3.9 blocks/tick before encoding. +- **Strings** use modified UTF-8 with a 2-byte length prefix. +- **Item data** writes `-1` as a short for empty slots (2 bytes), or short ID + byte count + short damage for occupied slots (5 bytes). +- The 4-byte size header is written by the server automatically - don't include it in your byte array. +- FourKit's \ref Minecraft.Server.FourKit.Sound "Sound" enum values map directly to the sound IDs used in LevelSoundPacket. VERY USEFUL!!!! +- If you send malformed data, the client will likely disconnect or crash, or do some funny weird stuff. Test carefully. +- Some packets have conditional fields (ExplodePacket, ContainerOpenPacket, SetEntityMotionPacket). Read the wire format tables above carefully. +- **Entity metadata** - see \ref metadata_encoding "Metadata Encoding" for how to construct SynchedEntityData blobs by hand. diff --git a/Minecraft.Server/FourKitBridge.cpp b/Minecraft.Server/FourKitBridge.cpp index da7b16822..220e694a4 100644 --- a/Minecraft.Server/FourKitBridge.cpp +++ b/Minecraft.Server/FourKitBridge.cpp @@ -17,14 +17,15 @@ namespace FourKitBridge { typedef void(__stdcall *fn_initialize)(); -typedef int(__stdcall* fn_fire_player_prelogin)(const char* nameUtf8, int nameByteLen, const char* ipUtf8, int ipByteLen, int port); +typedef void(__stdcall *fn_shutdown)(); +typedef void(__stdcall *fn_fire_world_save)(); +typedef int(__stdcall *fn_fire_player_prelogin)(const char* nameUtf8, int nameByteLen, const char* ipUtf8, int ipByteLen, int port); typedef int(__stdcall *fn_fire_player_login)(const char* nameUtf8, int nameByteLen, const char* ipUtf8, int ipByteLen, int port, int type, unsigned long long* offlineXUID, unsigned long long* onlineXUID); typedef void(__stdcall *fn_fire_player_join)(int entityId, const char *nameUtf8, int nameByteLen, const char *uuidUtf8, int uuidByteLen, unsigned long long offlineXUID, unsigned long long onlineXUID); typedef void(__stdcall *fn_fire_player_quit)(int entityId); typedef int(__stdcall *fn_fire_player_kick)(int entityId, int disconnectReason, const char *reasonUtf8, int reasonByteLen, char *outBuf, int outBufSize, int *outLen); -typedef void(__stdcall *fn_shutdown)(); typedef int(__stdcall *fn_fire_player_move)(int entityId, double fromX, double fromY, double fromZ, double toX, double toY, double toZ, @@ -56,7 +57,8 @@ typedef int(__stdcall *fn_fire_player_death)(int entityId, const char *deathMsgUtf8, int deathMsgByteLen, int exp, char *outMsgBuf, int outMsgBufSize, int *outMsgLen, int *outKeepInventory, int *outNewExp, int *outNewLevel, int *outKeepLevel); -typedef void(__stdcall *fn_set_player_callbacks)(void *kickPlayer, void *banPlayer, void *banPlayerIp, void *getPlayerAddress); +typedef void(__stdcall *fn_set_player_callbacks)(void *kickPlayer, void *banPlayer, void *banPlayerIp, void *getPlayerAddress, void *getPlayerLatency); +typedef void(__stdcall *fn_set_player_connection_callbacks)(void *sendRaw); typedef long long(__stdcall *fn_fire_player_drop_item)(int entityId, int itemId, int itemCount, int itemAux, int *outItemId, int *outItemCount, int *outItemAux); @@ -103,13 +105,14 @@ struct OpenContainerInfo static std::unordered_map s_openContainerInfo; static fn_initialize s_managedInit = nullptr; +static fn_shutdown s_managedShutdown = nullptr; +static fn_fire_world_save s_managedFireWorldSave = nullptr; static fn_fire_player_prelogin s_managedFirePreLogin = nullptr; static fn_fire_player_login s_managedFireLogin = nullptr; static fn_fire_player_join s_managedFireJoin = nullptr; static fn_update_entity_id s_managedUpdateEntityId = nullptr; static fn_fire_player_quit s_managedFireQuit = nullptr; static fn_fire_player_kick s_managedFireKick = nullptr; -static fn_shutdown s_managedShutdown = nullptr; static fn_fire_player_move s_managedFireMove = nullptr; static fn_set_native_callbacks s_managedSetCallbacks = nullptr; static fn_set_world_callbacks s_managedSetWorldCallbacks = nullptr; @@ -122,6 +125,7 @@ static fn_fire_sign_change s_managedFireSignChange = nullptr; static fn_fire_entity_death s_managedFireEntityDeath = nullptr; static fn_fire_player_death s_managedFirePlayerDeath = nullptr; static fn_set_player_callbacks s_managedSetPlayerCallbacks = nullptr; +static fn_set_player_connection_callbacks s_managedSetPlayerConnectionCallbacks = nullptr; static fn_fire_player_drop_item s_managedFirePlayerDropItem = nullptr; static fn_set_inventory_callbacks s_managedSetInventoryCallbacks = nullptr; static fn_fire_player_interact s_managedFirePlayerInteract = nullptr; @@ -171,13 +175,14 @@ void Initialize() struct { const wchar_t *name; void **target; } entries[] = { {L"Initialize", (void **)&s_managedInit}, - {L"FirePlayerPreLogin", (void**)&s_managedFirePreLogin}, + {L"Shutdown", (void **)&s_managedShutdown}, + {L"FireWorldSave", (void **)&s_managedFireWorldSave}, + {L"FirePlayerPreLogin", (void **)&s_managedFirePreLogin}, {L"FirePlayerLogin", (void **)&s_managedFireLogin}, {L"FirePlayerJoin", (void **)&s_managedFireJoin}, {L"FirePlayerQuit", (void **)&s_managedFireQuit}, {L"FirePlayerKick", (void **)&s_managedFireKick}, {L"FirePlayerMove", (void **)&s_managedFireMove}, - {L"Shutdown", (void **)&s_managedShutdown}, {L"SetNativeCallbacks", (void **)&s_managedSetCallbacks}, {L"SetWorldCallbacks", (void **)&s_managedSetWorldCallbacks}, {L"UpdatePlayerEntityId", (void **)&s_managedUpdateEntityId}, @@ -189,7 +194,8 @@ void Initialize() {L"FireSignChange", (void **)&s_managedFireSignChange}, {L"FireEntityDeath", (void **)&s_managedFireEntityDeath}, {L"FirePlayerDeath", (void **)&s_managedFirePlayerDeath}, - {L"SetPlayerCallbacks", (void **)&s_managedSetPlayerCallbacks}, + {L"SetPlayerCallbacks", (void**)&s_managedSetPlayerCallbacks}, + {L"SetPlayerConnectionCallbacks", (void **)&s_managedSetPlayerConnectionCallbacks}, {L"FirePlayerDropItem", (void **)&s_managedFirePlayerDropItem}, {L"SetInventoryCallbacks", (void **)&s_managedSetInventoryCallbacks}, {L"FirePlayerInteract", (void **)&s_managedFirePlayerInteract}, @@ -257,7 +263,11 @@ void Initialize() (void *)&NativeKickPlayer, (void *)&NativeBanPlayer, (void *)&NativeBanPlayerIp, - (void *)&NativeGetPlayerAddress); + (void *)&NativeGetPlayerAddress, + (void *)&NativeGetPlayerLatency); + + s_managedSetPlayerConnectionCallbacks( + (void*)&NativeSendRaw); s_managedSetInventoryCallbacks( (void *)&NativeGetPlayerInventory, @@ -314,6 +324,18 @@ void Shutdown() LogInfo("fourkit", "FourKit shut down."); } +void FireWorldSave() +{ + if (!s_initialized || !s_managedFireWorldSave) + { + return; + } + + s_managedFireWorldSave(); + + LogDebugf("fourkit", "Fired WorldSave"); +} + bool FirePlayerPreLogin(const std::wstring& name, const std::string& ip, int port) { if (!s_initialized || !s_managedFirePreLogin) diff --git a/Minecraft.Server/FourKitBridge.h b/Minecraft.Server/FourKitBridge.h index 2f07211a1..99c64b917 100644 --- a/Minecraft.Server/FourKitBridge.h +++ b/Minecraft.Server/FourKitBridge.h @@ -5,6 +5,7 @@ namespace FourKitBridge { void Initialize(); void Shutdown(); + void FireWorldSave(); bool FirePlayerPreLogin(const std::wstring& name, const std::string& ip, int port); bool FirePlayerLogin(const std::wstring& name, const std::string& ip, int port, int type, unsigned long long* onlineXUID, unsigned long long* offlineXUID); void FirePlayerJoin(int entityId, const std::wstring& name, const std::wstring& uuid, unsigned long long onlineXUID, unsigned long long offlineXUID); diff --git a/Minecraft.Server/FourKitNatives.cpp b/Minecraft.Server/FourKitNatives.cpp index 79dcc89f4..5d50dfc38 100644 --- a/Minecraft.Server/FourKitNatives.cpp +++ b/Minecraft.Server/FourKitNatives.cpp @@ -612,6 +612,25 @@ int __cdecl NativeGetPlayerAddress(int entityId, char *outIpBuf, int outIpBufSiz return 1; } +int __cdecl NativeGetPlayerLatency(int entityId) +{ + auto player = FindPlayer(entityId); + if (!player) return -1; + + return player->latency; +} + +int __cdecl NativeSendRaw(int entityId, unsigned char *bufferData, int bufferSize) +{ + auto player = FindPlayer(entityId); + if (!player) return -1; + + if (!player->connection || !player->connection->connection) + return -1; + + player->connection->connection->send(bufferData, bufferSize); +} + void WriteInventoryItemData(std::shared_ptr item, int index, int* outBuffer) { if (item) { //ItemFlags Key: diff --git a/Minecraft.Server/FourKitNatives.h b/Minecraft.Server/FourKitNatives.h index a3a005246..df96d9182 100644 --- a/Minecraft.Server/FourKitNatives.h +++ b/Minecraft.Server/FourKitNatives.h @@ -34,7 +34,11 @@ namespace FourKitBridge void __cdecl NativeKickPlayer(int entityId, int reason); int __cdecl NativeBanPlayer(int entityId, const char *reasonUtf8, int reasonByteLen); int __cdecl NativeBanPlayerIp(int entityId, const char *reasonUtf8, int reasonByteLen); - int __cdecl NativeGetPlayerAddress(int entityId, char *outIpBuf, int outIpBufSize, int *outPort); + int __cdecl NativeGetPlayerAddress(int entityId, char* outIpBuf, int outIpBufSize, int* outPort); + int __cdecl NativeGetPlayerLatency(int entityId); + + //plr connection + int __cdecl NativeSendRaw(int entityId, unsigned char* dataBuf, int dataBufSize); // inv void __cdecl NativeGetPlayerInventory(int entityId, int *outData); diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index 0f1dff910..705fff42b 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -678,6 +678,7 @@ int main(int argc, char **argv) { LogWorldIO("requesting autosave"); app.SetXuiServerAction(kServerActionPad, eXuiServerAction_AutoSaveGame); + FourKitBridge::FireWorldSave(); autosaveRequested = true; } nextAutosaveTick = now + autosaveIntervalMs; @@ -688,6 +689,8 @@ int main(int argc, char **argv) serverCli.Stop(); app.m_bShutdown = true; + FourKitBridge::Shutdown(); //close out the translation layer early for plugin shutdown + LogInfof("shutdown", "Dedicated server stopped"); MinecraftServer *server = MinecraftServer::getInstance(); if (server != NULL) @@ -721,7 +724,6 @@ int main(int argc, char **argv) } LogInfof("shutdown", "Cleaning up and exiting."); - FourKitBridge::Shutdown(); WinsockNetLayer::Shutdown(); LogDebugf("shutdown", "Network layer shutdown complete."); g_NetworkManager.Terminate(); diff --git a/Minecraft.World/Connection.cpp b/Minecraft.World/Connection.cpp index d44502667..e3a61e374 100644 --- a/Minecraft.World/Connection.cpp +++ b/Minecraft.World/Connection.cpp @@ -1,3 +1,4 @@ +#include "Connection.h" #include "stdafx.h" #include "InputOutputStream.h" #include "Socket.h" @@ -32,6 +33,7 @@ void Connection::_init() disconnectReason = DisconnectPacket::eDisconnect_None; noInputTicks = 0; estimatedRemaining = 0; + estimatedRemainingRaw = 0; fakeLag = 0; slowWriteDelay = 50; @@ -145,6 +147,23 @@ void Connection::setListener(PacketListener *packetListener) this->packetListener = packetListener; } +void Connection::send(unsigned char* buffer, int size) +{ + if (quitting) return; + + MemSect(15); + // 4J Jev, synchronized (&writeLock) + EnterCriticalSection(&writeLock); + + estimatedRemainingRaw += size; + + outgoingRaw.push(std::make_pair(buffer, size)); + + // 4J Jev, end synchronized. + LeaveCriticalSection(&writeLock); + MemSect(0); +} + void Connection::send(shared_ptr packet) { if (quitting) return; @@ -232,6 +251,32 @@ bool Connection::writeTick() didSomething = true; } + if (!outgoingRaw.empty()) + { + std::pair rawPacket; + EnterCriticalSection(&writeLock); + + rawPacket = outgoingRaw.front(); + outgoingRaw.pop(); + estimatedRemainingRaw -= rawPacket.second; + + LeaveCriticalSection(&writeLock); + + for (int i = 0; i < rawPacket.second; i++) { + byteArrayDos->writeByte(rawPacket.first[i]); + } + + // 4J Stu - Changed this so that rather than writing to the network stream through a buffered stream we want to: + // a) Only push whole "game" packets to QNet, rather than amalgamated chunks of data that may include many packets, and partial packets + // b) To be able to change the priority and queue of a packet if required + //sos->writeWithFlags( baos->buf, 0, baos->size(), 0 ); + //baos->reset(); + + int value = rawPacket.first[0]; + writeSizes[value] += rawPacket.second; + didSomething = true; + } + if ((slowWriteDelay-- <= 0) && !outgoing_slow.empty() && (fakeLag == 0 || System::currentTimeMillis() - outgoing_slow.front()->createTime >= fakeLag)) { shared_ptr packet; diff --git a/Minecraft.World/Connection.h b/Minecraft.World/Connection.h index 9db005120..d745932ee 100644 --- a/Minecraft.World/Connection.h +++ b/Minecraft.World/Connection.h @@ -54,6 +54,7 @@ private: queue > incoming; // 4J - was using synchronizedList... CRITICAL_SECTION incoming_cs; // ... now has this critical section + queue> outgoingRaw; // 4J - was using synchronizedList - but don't think it is required as usage is wrapped in writeLock critical section queue > outgoing; // 4J - was using synchronizedList - but don't think it is required as usage is wrapped in writeLock critical section queue > outgoing_slow; // 4J - was using synchronizedList - but don't think it is required as usage is wrapped in writeLock critical section @@ -76,6 +77,7 @@ private: int noInputTicks; int estimatedRemaining; + int estimatedRemainingRaw; int tickCount; // 4J Added @@ -99,6 +101,7 @@ public: void setListener(PacketListener *packetListener); void send(shared_ptr packet); + void send(unsigned char *buffer, int size); public: void queueSend(shared_ptr packet);