This commit is contained in:
Bruno Rybársky 2024-10-26 12:41:37 +02:00
parent 78b2a2f022
commit a6a7a94c20
5 changed files with 2021 additions and 135 deletions

350
main.go

@ -28,6 +28,7 @@ const (
var maxPlayers int32
var icon string
var iconData []byte
var serverID = make([]byte, 20)
var serverPrivateKey *rsa.PrivateKey
@ -38,7 +39,8 @@ var usernameRegex = regexp.MustCompile("^[a-zA-Z0-9_]{3,16}$")
var world World
func readIcon(fileName string) (icon string) {
iconData, err := os.ReadFile(fileName)
var err error
iconData, err = os.ReadFile(fileName)
if err != nil {
return
}
@ -85,6 +87,7 @@ func main() {
LabelInt: 0,
URL: "https://brn.systems",
}},
TickRate: 20,
}
var err error
@ -125,7 +128,6 @@ func handleRequest(conn net.Conn) {
var player = Player{
state: 0,
conn: conn,
name: "",
version: 0,
requestedAddress: "",
requestedPort: 0,
@ -195,9 +197,9 @@ func handleRequest(conn net.Conn) {
errOut = err
break
}
player.name = username
player.Entity.Name = username
player.uuid = playerUUID
if !usernameRegex.MatchString(player.name) {
if !usernameRegex.MatchString(player.Entity.Name) {
errOut = errors.New("bad username")
break
}
@ -235,7 +237,7 @@ func handleRequest(conn net.Conn) {
errOut = errors.New("joined session not found")
break
}
if player.profile.ID != strings.Replace(player.uuid.String(), "-", "", -1) || player.profile.Name != player.name {
if player.profile.ID != strings.Replace(player.uuid.String(), "-", "", -1) || player.profile.Name != player.Entity.Name {
errOut = errors.New("wrong session name")
break
}
@ -354,6 +356,341 @@ func handleRequest(conn net.Conn) {
case 4:
switch packetID {
case 0x00: // Confirm Teleportation
teleportID, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
// Handle teleport confirmation logic here
if player.teleportID == teleportID {
player.waitingForTeleportConfirm = false
}
case 0x01: // Query Block Entity Tag
transactionID, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
var position BlockPosition
err = position.receivePosition(packetData)
if err != nil {
errOut = err
break
}
// Handle block entity query logic here
player.queryBlockEntityTag(transactionID, position)
case 0x02: // Change Difficulty
difficulty, err := packetData.ReadByte()
if err != nil {
errOut = err
break
}
// Change difficulty logic, ensure player is an operator
if player.PermissionLevel >= 2 {
world.Difficulty = difficulty
for _, iterPlayer := range world.Players {
iterPlayer.sendDifficulty()
}
}
case 0x03: // Acknowledge Message
messageCount, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
// Acknowledge message logic here
player.acknowledgeMessage(messageCount)
case 0x04, 0x05: // Chat Command & Signed Chat Command
command, err := receiveString(packetData)
if err != nil {
errOut = err
break
}
player.processChatCommand(command)
case 0x06: // Chat Message
message, err := receiveString(packetData)
if err != nil {
errOut = err
break
}
_ = receiveInt64(packetData)
_ = receiveInt64(packetData)
// Handle the chat message here
player.handleChatMessage(message)
case 0x07: // Player Session
//sessionId, err := receiveUUID(packetData)
//if err != nil {
// errOut = err
// break
//}
//expiresAt := receiveInt64(packetData)
//pubKeyLen, err := receiveVarint(packetData)
//
//maybe implement in the future
case 0x08: // Chunk Batch Received
_ = receiveFloat32(packetData) //chunks per tick
case 0x09: // Client Status
actionID, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
switch actionID {
case 0:
player.sendRespawn(true, true)
case 1:
player.sendAwardStatistics()
}
case 0x0A: // Client Information
// Read Locale (16-character string)
locale, err := receiveString(packetData)
if err != nil {
errOut = err
break
}
player.locale = locale
// Read View Distance (1 byte)
viewDistance, err := packetData.ReadByte()
if err != nil {
errOut = err
break
}
player.viewDistance = int8(viewDistance)
// Read Chat Mode (VarInt Enum)
chatMode, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
player.chatMode = chatMode
// Read Chat Colors (Boolean)
chatColors, err := packetData.ReadByte()
if err != nil {
errOut = err
break
}
player.chatColors = chatColors != 0
// Read Displayed Skin Parts (Unsigned Byte)
displayedSkinParts, err := packetData.ReadByte()
if err != nil {
errOut = err
break
}
player.skinParts = displayedSkinParts
// Read Main Hand (VarInt Enum)
mainHand, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
player.isRightHanded = mainHand == 1
// Read Enable Text Filtering (Boolean)
enableTextFiltering, err := receiveBool(packetData)
if err != nil {
errOut = err
break
}
player.textFiltering = enableTextFiltering
// Read Allow Server Listings (Boolean)
allowServerListings, err := receiveBool(packetData)
if err != nil {
errOut = err
break
}
player.serverListing = allowServerListings
// Successfully parsed the packet
errOut = nil
case 0x0B: // Command Suggestions Request
transactionId, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
text, err := receiveString(packetData)
if err != nil {
errOut = err
break
}
player.requestCommandSuggestions(transactionId, text)
case 0x0C: // Acknowledge Configuration
player.state = 3
case 0x0D: // Click Container Button
windowID, err := packetData.ReadByte()
if err != nil {
errOut = err
break
}
if windowID != player.WindowID {
break
}
buttonID, err := packetData.ReadByte()
if err != nil {
errOut = err
break
}
_ = buttonID
//TODO implement logic
case 0x0E: // Click Container
// Parse fields and handle click container logic here
windowID, err := packetData.ReadByte()
if err != nil {
errOut = err
break
}
if windowID != player.WindowID {
break
}
// Read StateID (VarInt)
stateID, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
// Read Slot (Short)
var slot int16
slot = receiveInt16(packetData)
if err != nil {
errOut = err
break
}
// Read Button (Byte)
button, err := packetData.ReadByte()
if err != nil {
errOut = err
break
}
// Read Mode (VarInt)
mode, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
// Read number of ChangedSlots (VarInt)
changedSlotsCount, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
var packet ClickContainerPacket
// Initialize ChangedSlots slice
packet.ChangedSlots = make([]Slot, changedSlotsCount)
// Read each Slot in the ChangedSlots array
for i := 0; i < int(changedSlotsCount); i++ {
slotData, err := readSlot(data)
if err != nil {
errOut = err
break
}
packet.ChangedSlots[i] = slotData
}
// Read CarriedItem (ItemStack)
carriedItem, err := readSlot(data)
if err != nil {
errOut = err
break
}
// Populate the packet struct
packet.WindowID = windowID
packet.StateID = stateID
packet.Slot = slot
packet.Button = int8(button)
packet.Mode = mode
packet.CarriedItem = carriedItem
player.handleClickContainer(packet)
case 0x0F: // Close Container
windowID, err := packetData.ReadByte()
if err != nil {
errOut = err
break
}
player.sendCloseContainer(windowID)
case 0x10: // Change Container ItemStack State
slotID, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
windowID, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
state, err := receiveBool(packetData)
if err != nil {
errOut = err
break
}
player.changeContainerSlotState(slotID, windowID, state)
case 0x11: // Cookie Response
key, err := receiveString(packetData)
if err != nil {
errOut = err
break
}
hasPayload, err := receiveBool(packetData)
if err != nil {
errOut = err
break
}
// Handle cookie response logic here
player.handleCookieResponse(key, hasPayload)
case 0x12: // Serverbound Plugin Message
channel, err := receiveString(packetData)
if err != nil {
errOut = err
break
}
data := packetData // Rest is the byte array
player.handlePluginMessage(channel, data)
case 0x13: // Debug Sample Subscription
sampleType, err := receiveVarint(packetData)
if err != nil {
errOut = err
break
}
player.subscribeToDebugSample(sampleType)
case 0x14: // Edit Book
// Parse fields and handle book editing logic
player.handleEditBook(packetData)
default:
errOut = fmt.Errorf("unexpected packet %x in play state", packetID)
log.Println(errOut)
@ -368,7 +705,8 @@ func handleRequest(conn net.Conn) {
}
}
if errOut != nil {
if //goland:noinspection GoDfaConstantCondition
errOut != nil {
log.Println(errOut.Error())
_ = player.sendDisconnect(fmt.Sprintf("Your server thread crashed: %s", errOut.Error()))
}

@ -19,7 +19,6 @@ import (
"io"
"log"
"math"
"math/big"
"net/http"
"regexp"
"strings"
@ -34,22 +33,6 @@ func generateRandomBytes(size int) ([]byte, error) {
return randomBytes, nil
}
func serverIDCreate() ([]byte, error) {
result := make([]byte, 20)
charRange := 0x7E - 0x21 + 1
for i := 0; i < 20; i++ {
// Generate a random number in the range [charStart, charEnd]
num, err := rand.Int(rand.Reader, big.NewInt(int64(charRange)))
if err != nil {
return []byte{}, err
}
result[i] = byte(0x21 + num.Int64())
}
return result, nil
}
type TextPiece struct {
Text string `json:"text,omitempty"`
Color string `json:"color,omitempty"`
@ -62,6 +45,14 @@ type TextComponent struct {
CleanText string `json:"cleantext,omitempty"`
}
func getTextComponent(text string) TextComponent {
return TextComponent{
Text: text,
Extra: nil,
CleanText: text,
}
}
type RegistryEntry struct {
EntryID string `json:"entryID,omitempty"`
HasNBT bool `json:"hasNBT,omitempty"`
@ -237,11 +228,51 @@ type MojangProperty struct {
Signature string `json:"signature"`
}
func (mojangProperty *MojangProperty) add(buf *bytes.Buffer) {
addString(buf, mojangProperty.Name)
addString(buf, mojangProperty.Value)
hasSignature := mojangProperty.Signature != ""
addBool(buf, hasSignature)
if hasSignature {
addString(buf, mojangProperty.Signature)
}
}
type PlayerAction struct {
ActionMask byte
Name string
Properties []MojangProperty
GameMode int32
Listed bool
Ping int32
DisplayName string
HasDisplayName bool
}
// Helper to add a player with properties
func (playerAction *PlayerAction) add(buf *bytes.Buffer) {
addString(buf, playerAction.Name)
// Add properties
addVarint(buf, int32(len(playerAction.Properties)))
for _, prop := range playerAction.Properties {
prop.add(buf)
}
}
// Helper to update game mode
// Helper to update listed state
// Helper to update latency (ping)
// Helper to update display name
func (player *Player) hasJoinedSession(sharedSecret []byte) (bool, error) {
serverKey, err := ExportRSAPublicKey(serverPublicKey)
serverIDHash := AuthDigest([][]byte{serverID, sharedSecret, serverKey})
remoteAddr := strings.Split(player.conn.RemoteAddr().String(), ":")[0]
url := fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s", player.name, serverIDHash, remoteAddr)
url := fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s", player.Entity.Name, serverIDHash, remoteAddr)
resp, err := http.Get(url)
if err != nil {
@ -567,37 +598,6 @@ func receiveVarint(buf *bytes.Buffer) (value int32, err error) {
return
}
func receiveVarlong(buf *bytes.Buffer) (currentValue int64) {
var (
b byte
shift uint64
uValue uint64
length uint32
)
for i := 0; i < buf.Len(); i++ {
bx, err := buf.ReadByte()
b = bx
if err != nil {
return
}
length++
uValue |= (uint64(b) & 0x7F) << shift
shift += 7
if b&0x80 == 0 {
break
}
}
if shift < 8*uint64(length) && b&0x40 != 0 {
uValue |= ^uint64(0) << shift
}
currentValue = int64(uValue)
return currentValue
}
func addUUID(buffer *bytes.Buffer, inUUID uuid.UUID) int32 {
binaryUUID, err := inUUID.MarshalBinary()
if err != nil {
@ -626,6 +626,10 @@ func addByte(buffer *bytes.Buffer, byte byte) {
buffer.WriteByte(byte)
}
func addBytes(buffer *bytes.Buffer, bytes []byte) {
buffer.Write(bytes)
}
func addBool(buffer *bytes.Buffer, boolean bool) {
var boolByte byte = 0x00
if boolean {
@ -643,12 +647,6 @@ func receiveBool(buffer *bytes.Buffer) (boolean bool, err error) {
return
}
func addUint16(buffer *bytes.Buffer, value uint16) {
b := make([]byte, 2)
binary.BigEndian.PutUint16(b, value)
buffer.Write(b)
}
// Decodes an uint16 from the buffer.
func receiveUint16(buffer *bytes.Buffer) (value uint16) {
// Create a byte slice to hold the 2 bytes for the uint16.
@ -665,6 +663,22 @@ func receiveUint16(buffer *bytes.Buffer) (value uint16) {
return value
}
// Decodes an uint16 from the buffer.
func receiveInt16(buffer *bytes.Buffer) (value int16) {
// Create a byte slice to hold the 2 bytes for the uint16.
b := make([]byte, 2)
// Read exactly 2 bytes from the buffer.
_, err := buffer.Read(b)
if err != nil {
return 0
}
// Convert the byte slice to uint16 using BigEndian.
value = int16(binary.BigEndian.Uint16(b))
return value
}
func addInt64(buffer *bytes.Buffer, value int64) {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(value))
@ -707,15 +721,6 @@ func addFloat64(buffer *bytes.Buffer, value float64) {
}
// Decodes a float64 from the buffer.
func receiveFloat64(buffer *bytes.Buffer) (value float64) {
b := make([]byte, 8)
_, err := buffer.Read(b)
if err != nil {
return 0
}
value = math.Float64frombits(binary.BigEndian.Uint64(b))
return
}
func addInt32(buffer *bytes.Buffer, value int32) {
b := make([]byte, 4)
@ -724,15 +729,6 @@ func addInt32(buffer *bytes.Buffer, value int32) {
}
// Decodes an int32 from the buffer.
func receiveInt32(buffer *bytes.Buffer) (value int32) {
b := make([]byte, 4)
_, err := buffer.Read(b)
if err != nil {
return 0
}
value = int32(binary.BigEndian.Uint32(b))
return
}
func addInt16(buffer *bytes.Buffer, value int16) {
b := make([]byte, 2)
@ -793,20 +789,3 @@ func addTextComponent(buffer *bytes.Buffer, component TextComponent) {
addString(buffer, jsonString)
return
}
func receiveTextComponent(buf *bytes.Buffer) (component TextComponent, err error) {
jsonString, err := receiveString(buf)
if err != nil {
return
}
if strings.ContainsAny(jsonString, "") {
}
jsonDecoderText := json.NewDecoder(strings.NewReader(jsonString))
err = jsonDecoderText.Decode(&component)
if err != nil {
err = nil
component.Text = jsonString
component.CleanText = cleanMinecraftFormatting(jsonString)
}
return
}

@ -124,17 +124,18 @@ func (player *Player) sendChunks() error {
func (player *Player) sendCloseContainer(windowID uint8) error {
var buf bytes.Buffer
addByte(&buf, windowID)
player.WindowID = 0
return player.sendPacket(0x12, &buf)
}
func (player *Player) sendContainerContent(windowID uint8, slots []*Slot, cursor *Slot) error {
func (player *Player) sendContainerContent(window *Window, cursor *ItemStack) error {
var buf bytes.Buffer
addByte(&buf, windowID)
addByte(&buf, window.ID)
addVarint(&buf, player.lastContainerStateID)
player.lastContainerStateID++
addVarint(&buf, int32(len(slots)))
for _, slot := range slots {
slot.add(&buf)
addVarint(&buf, int32(len(window.Slots)))
for _, slot := range window.Slots {
slot.Stack.add(&buf)
}
cursor.add(&buf)
return player.sendPacket(0x13, &buf)
@ -148,7 +149,7 @@ func (player *Player) sendContainerProperty(windowID uint8, property int16, valu
return player.sendPacket(0x14, &buf)
}
func (player *Player) sendContainerSlot(windowID uint8, slotIndex int16, slot *Slot) error {
func (player *Player) sendContainerSlot(windowID uint8, slotIndex int16, slot *ItemStack) error {
var buf bytes.Buffer
addByte(&buf, windowID)
addVarint(&buf, player.lastContainerStateID)
@ -288,16 +289,16 @@ func (player *Player) sendHurtAnimation(attacker *Entity) error {
return player.sendPacket(0x24, &buf)
}
func (player *Player) sendWorldBorder(border *WorldBorder) error {
func (player *Player) sendWorldBorder() error {
var buf bytes.Buffer
addFloat64(&buf, border.X)
addFloat64(&buf, border.Z)
addFloat64(&buf, border.OldDiameter)
addFloat64(&buf, border.NewDiameter)
addVarlong(&buf, border.Speed)
addVarint(&buf, border.PortalTeleportBoundary)
addVarint(&buf, border.WarningBlocks)
addVarint(&buf, border.WarningTime)
addFloat64(&buf, player.Dimension.Border.X)
addFloat64(&buf, player.Dimension.Border.Z)
addFloat64(&buf, player.Dimension.Border.OldDiameter)
addFloat64(&buf, player.Dimension.Border.NewDiameter)
addVarlong(&buf, player.Dimension.Border.Speed)
addVarint(&buf, player.Dimension.Border.PortalTeleportBoundary)
addVarint(&buf, player.Dimension.Border.WarningBlocks)
addVarint(&buf, player.Dimension.Border.WarningTime)
return player.sendPacket(0x25, &buf)
}
@ -436,7 +437,7 @@ func (player *Player) sendBookOpen(isOffHand bool) error {
func (player *Player) sendOpenScreen(windowID int32, window *Window, title TextComponent) error {
var buf bytes.Buffer
addVarint(&buf, windowID)
addVarint(&buf, window.ID)
addVarint(&buf, window.Type)
addTextComponent(&buf, title)
return player.sendPacket(0x33, &buf)
}
@ -497,3 +498,928 @@ func (player *Player) sendRemoveFromTab(playersToRemove []uuid.UUID) error {
}
return errors.New("no playersToRemove")
}
func (player *Player) sendPlayerInfoUpdate(actions byte, players []uuid.UUID, playerActions []PlayerAction) error {
var buf bytes.Buffer
// Write the actions byte
addByte(&buf, actions)
// Write the number of players
addVarint(&buf, int32(len(players)))
// For each player, write their UUID and corresponding actions
for i, playerUUID := range players {
addUUID(&buf, playerUUID)
action := playerActions[i]
// Based on the action mask, we add the relevant player info
if actions&0x01 != 0 {
action.add(&buf)
}
if actions&0x04 != 0 {
addVarint(&buf, action.GameMode) // Update Game Mode
}
if actions&0x08 != 0 {
addBool(&buf, action.Listed) // Update Listed
}
if actions&0x10 != 0 {
addVarint(&buf, action.Ping) // Update Latency
}
if actions&0x20 != 0 {
addBool(&buf, action.HasDisplayName) // Update Display Name
if action.HasDisplayName {
addString(&buf, action.DisplayName)
}
}
}
return player.sendPacket(0x3E, &buf)
}
func (player *Player) sendLookAtPos(pos EntityPosition, aimEyes bool) error {
var buf bytes.Buffer
addBool(&buf, aimEyes)
pos.add(&buf)
addBool(&buf, false)
return player.sendPacket(0x3F, &buf)
}
func (player *Player) sendLookAtEntity(entity *Entity, atEntityEyes bool, aimEyes bool) error {
var buf bytes.Buffer
addBool(&buf, aimEyes)
entity.Position.add(&buf)
addBool(&buf, true)
addVarint(&buf, entity.ID)
addBool(&buf, atEntityEyes)
return player.sendPacket(0x3F, &buf)
}
func (player *Player) sendPosition() error {
var buf bytes.Buffer
player.Position.add(&buf)
player.Rotation.addBasic(&buf)
addByte(&buf, 0) //all absolute
addVarint(&buf, player.teleportID)
player.teleportID++
return player.sendPacket(0x40, &buf)
}
func (player *Player) sendUpdateRecipeBook(action int32, craftingOpen, craftingFilter, smeltingOpen, smeltingFilter, blastFurnaceOpen, blastFurnaceFilter, smokerOpen, smokerFilter bool, recipeIDs1 []string, recipeIDs2 []string) error {
var buf bytes.Buffer
// Add Action
addVarint(&buf, action)
// Add boolean flags
addBool(&buf, craftingOpen)
addBool(&buf, craftingFilter)
addBool(&buf, smeltingOpen)
addBool(&buf, smeltingFilter)
addBool(&buf, blastFurnaceOpen)
addBool(&buf, blastFurnaceFilter)
addBool(&buf, smokerOpen)
addBool(&buf, smokerFilter)
// Add Array size 1 (for Recipe IDs)
addVarint(&buf, int32(len(recipeIDs1)))
for _, recipeID := range recipeIDs1 {
addString(&buf, recipeID)
}
// If action is 0 (init), add Array size 2 and its Recipe IDs
if action == 0 {
addVarint(&buf, int32(len(recipeIDs2)))
for _, recipeID := range recipeIDs2 {
addString(&buf, recipeID)
}
}
// Send packet with ID 0x41
return player.sendPacket(0x41, &buf)
}
func (player *Player) sendRemoveEntities(entities []*Entity) error {
if len(entities) > 0 {
var buf bytes.Buffer
addVarint(&buf, int32(len(entities)))
for _, entity := range entities {
addVarint(&buf, entity.ID)
}
return player.sendPacket(0x42, &buf)
}
return nil
}
// sendRemoveEntityEffect sends a packet to remove an entity's effect.
func (player *Player) sendRemoveEntityEffect(entity *Entity, effectID int32) error {
var buf bytes.Buffer
// Add Entity ID
addVarint(&buf, entity.ID)
// Add Effect ID
addVarint(&buf, effectID)
// Send packet with ID 0x43
return player.sendPacket(0x43, &buf)
}
// sendResetScore sends a packet to reset a player's score.
func (player *Player) sendResetScore(entityName string, objectiveName string) error {
var buf bytes.Buffer
// Add Entity Name
addString(&buf, entityName)
// Add Has an Objective Name flag
addBool(&buf, len(objectiveName) > 0)
// If there is an Objective Name, add it
if len(objectiveName) > 0 {
addString(&buf, objectiveName)
}
// Send packet with ID 0x44
return player.sendPacket(0x44, &buf)
}
// sendRemoveResourcePack sends a packet to remove a resource pack.
func (player *Player) sendRemoveResourcePack(resourcePackUUID uuid.UUID) error {
var buf bytes.Buffer
// Add Has UUID flag
addBool(&buf, resourcePackUUID != uuid.Nil)
// If there's a specific resource pack, add its UUID
if resourcePackUUID != uuid.Nil {
addUUID(&buf, resourcePackUUID)
}
// Send a packet with ID 0x45
return player.sendPacket(0x45, &buf)
}
// Sends a packet to add a resource pack.
func (player *Player) sendAddResourcePack(resourcePackUUID uuid.UUID, url string, hash string, forced bool, hasPrompt bool, promptMessage TextComponent) error {
var buf bytes.Buffer
// Add UUID
addUUID(&buf, resourcePackUUID)
// Add URL
addString(&buf, url)
// Add Hash (40-character hexadecimal string)
addString(&buf, hash)
// Add a Forced flag
addBool(&buf, forced)
// Add Has Prompt flag
addBool(&buf, hasPrompt)
// If there's a custom prompt, add it
if hasPrompt {
addTextComponent(&buf, promptMessage)
}
// Send packet with ID 0x46
return player.sendPacket(0x46, &buf)
}
// sendRespawn sends a respawn packet.
func (player *Player) sendRespawn(keepAttributes bool, keepMetadata bool) error {
var buf bytes.Buffer
// Add Dimension Type
addVarint(&buf, player.Dimension.ID)
// Add Dimension Name
addString(&buf, player.Dimension.Name)
noise, err := world.Seed.MarshalBinary()
if err != nil {
return err
}
for i := 0; i < 8; i++ {
addByte(&buf, noise[i])
}
// Add Game Mode
addByte(&buf, player.GameMode)
// Add Previous Game Mode
addByte(&buf, byte(player.PreviousGameMode))
// Add Is Debug flag
addBool(&buf, world.Debug)
// Add Is Flat flag
addBool(&buf, world.Flat)
// Add Has a Death Location flag
addBool(&buf, player.DeathPosition != BlockPosition{})
// If the player has a death location, add the dimension and position
if player.DeathPosition.X != 0 && player.DeathPosition.Y != 0 && player.DeathPosition.Z != 0 {
addString(&buf, player.DeathDimension.Name)
player.DeathPosition.add(&buf)
}
// Add Portal Cooldown
addVarint(&buf, player.portalCooldown)
var dataKept byte
if keepAttributes {
dataKept |= 0x01
}
if keepMetadata {
dataKept |= 0x02
}
// Add Data Kept bitmask
addByte(&buf, dataKept)
// Send packet with ID 0x47
return player.sendPacket(0x47, &buf)
}
func (player *Player) sendHeadRotation(entity *Entity) error {
var buf bytes.Buffer
addVarint(&buf, entity.ID)
addByte(&buf, entity.Rotation.Yaw)
return player.sendPacket(0x48, &buf)
}
// sendUpdateSectionBlocks sends a packet to update section blocks.
func (player *Player) sendUpdateSectionBlocks(chunkX, chunkY, chunkZ int, blocks []Block) error {
var buf bytes.Buffer
// Add Chunk Section Position (calculated from chunkX, chunkY, chunkZ)
sectionPosition := ((chunkX & 0x3FFFFF) << 42) | (chunkZ&0x3FFFFF)<<20 | (chunkY & 0xFFFFF)
addInt64(&buf, int64(sectionPosition))
// Add number of blocks
addVarint(&buf, int32(len(blocks)))
// Add each block's state and position
for _, block := range blocks {
blockData := int64(block.BlockStateID<<12) | int64(block.Position.X<<8|block.Position.Z<<4|int32(block.Position.Y))
addVarlong(&buf, blockData)
}
// Send packet with ID 0x49
return player.sendPacket(0x49, &buf)
}
// sendSelectAdvancementsTab sends a packet to select an advancement tab.
func (player *Player) sendSelectAdvancementsTab() error {
var buf bytes.Buffer
// Add Has ID flag
addBool(&buf, player.advancementTab != "")
// If there's an ID, add it
if player.advancementTab != "" {
addString(&buf, player.advancementTab)
}
// Send a packet with ID 0x4A
return player.sendPacket(0x4A, &buf)
}
// sendServerData sends a packet containing the server's MOTD and optional icon.
func (player *Player) sendServerData() error {
var buf bytes.Buffer
// Add MOTD
addTextComponent(&buf, TextComponent{
Text: "Hello there",
Extra: nil,
CleanText: "Hello there",
})
// Add Has Icon flag
addBool(&buf, len(iconData) > 0)
// If there is an icon, add it
if len(iconData) > 0 {
addBytes(&buf, iconData)
}
// Send a packet with ID 0x4B
return player.sendPacket(0x4B, &buf)
}
// sendSetActionBarText sends a packet to set the action bar text.
func (player *Player) sendSetActionBarText(text TextComponent) error {
var buf bytes.Buffer
// Add Action Bar Text
addTextComponent(&buf, text)
// Send packet with ID 0x4C
return player.sendPacket(0x4C, &buf)
}
// sendSetBorderCenter sends a packet to set the border center.
func (player *Player) sendSetBorderCenter() error {
var buf bytes.Buffer
// Add X and Z coordinates
addFloat64(&buf, player.Dimension.Border.X)
addFloat64(&buf, player.Dimension.Border.Z)
// Send a packet with ID 0x4D
return player.sendPacket(0x4D, &buf)
}
// sendSetBorderLerpSize sends a packet to set the border lerp size.
func (player *Player) sendSetBorderLerpSize() error {
var buf bytes.Buffer
// Add Old Diameter
addFloat64(&buf, player.Dimension.Border.OldDiameter)
// Add New Diameter
addFloat64(&buf, player.Dimension.Border.NewDiameter)
// Add Speed
addVarlong(&buf, player.Dimension.Border.Speed)
// Send packet with ID 0x4E
return player.sendPacket(0x4E, &buf)
}
// sendSetBorderSize sends a packet to set the border size.
func (player *Player) sendSetBorderSize() error {
var buf bytes.Buffer
// Add Diameter
addFloat64(&buf, player.Dimension.Border.NewDiameter)
// Send packet with ID 0x4F
return player.sendPacket(0x4F, &buf)
}
// sendSetBorderWarningDelay sends a packet to set the border warning delay.
func (player *Player) sendSetBorderWarningDelay() error {
var buf bytes.Buffer
// Add Warning Time (seconds)
addVarint(&buf, player.Dimension.Border.WarningTime)
// Send packet with ID 0x50
return player.sendPacket(0x50, &buf)
}
// sendSetBorderWarningDistance sends a packet to set the border warning distance.
func (player *Player) sendSetBorderWarningDistance() error {
var buf bytes.Buffer
// Add Warning Distance (blocks)
addVarint(&buf, player.Dimension.Border.WarningBlocks)
// Send packet with ID 0x51
return player.sendPacket(0x51, &buf)
}
// sendSetCamera sends a packet to set the camera view to an entity.
func (player *Player) sendSetCamera(entity *Entity) error {
var buf bytes.Buffer
// Add Camera Entity ID
addVarint(&buf, entity.ID)
// Send packet with ID 0x52
return player.sendPacket(0x52, &buf)
}
// sendSetHeldItem sends a packet to set the player's held item slot.
func (player *Player) sendSetHeldItem() error {
var buf bytes.Buffer
// Add ItemStack
addByte(&buf, player.inventory.activeSlot)
// Send packet with ID 0x53
return player.sendPacket(0x53, &buf)
}
// sendSetCenterChunk sends a packet to set the player's center chunk.
func (player *Player) sendSetCenterChunk() error {
var buf bytes.Buffer
chunkX, chunkZ := player.Position.toChunkPos()
// Add Chunk X and Chunk Z
addVarint(&buf, chunkX)
addVarint(&buf, chunkZ)
// Send packet with ID 0x54
return player.sendPacket(0x54, &buf)
}
// sendSetRenderDistance sends a packet to set the render distance.
func (player *Player) sendSetRenderDistance() error {
var buf bytes.Buffer
// Add View Distance
addVarint(&buf, player.Dimension.RenderDistance)
// Send packet with ID 0x55
return player.sendPacket(0x55, &buf)
}
// sendSetDefaultSpawnPosition sends a packet to set the default spawn position.
func (player *Player) sendSetDefaultSpawnPosition() error {
var buf bytes.Buffer
// Add Position
player.Dimension.SpawnPosition.add(&buf)
// Add Angle
addFloat32(&buf, player.Dimension.SpawnAngle)
// Send packet with ID 0x56
return player.sendPacket(0x56, &buf)
}
// sendDisplayObjective sends a packet to display an objective.
func (player *Player) sendDisplayObjective(position byte, scoreName string) error {
var buf bytes.Buffer
// Add Position
addByte(&buf, position)
// Add Score Name
addString(&buf, scoreName)
// Send a packet with ID 0x57
return player.sendPacket(0x57, &buf)
}
// sendSetEntityMetadata sends a packet to update entity metadata.
func (player *Player) sendSetEntityMetadata(entityID int32, metadata EntityMetadata) error {
var buf bytes.Buffer
// Add Entity ID
addVarint(&buf, entityID)
// Add EntityMetadata
metadata.add(&buf)
// Send packet with ID 0x58
return player.sendPacket(0x58, &buf)
}
// sendLinkEntities sends a packet to link two entities together.
func (player *Player) sendLinkEntities(attached, holding *Entity) error {
var buf bytes.Buffer
// Add Attached Entity ID
addVarint(&buf, attached.ID)
// Add Holding Entity ID
addVarint(&buf, holding.ID)
// Send packet with ID 0x59
return player.sendPacket(0x59, &buf)
}
// sendSetEntityVelocity sends a packet to set an entity's velocity.
func (player *Player) sendSetEntityVelocity(entity *Entity) error {
var buf bytes.Buffer
// Add Entity ID
addVarint(&buf, entity.ID)
// Add Velocities
addInt16(&buf, int16(entity.Velocity.X))
addInt16(&buf, int16(entity.Velocity.Y))
addInt16(&buf, int16(entity.Velocity.Z))
// Send a packet with ID 0x5A
return player.sendPacket(0x5A, &buf)
}
// sendSetEquipment sends a packet to update an entity's equipment.
func (player *Player) sendSetEquipment(entity *Entity) error {
var buf bytes.Buffer
// Add Entity ID
addVarint(&buf, entity.ID)
for i := range 7 {
slot := i
if i == 6 {
slot |= 0x80
}
addByte(&buf, byte(slot))
player.inventory.slots[i].add(&buf)
}
// Send a packet with ID 0x5B
return player.sendPacket(0x5B, &buf)
}
// Function to send experience update packet
func (player *Player) sendSetExperience() error {
var buf bytes.Buffer
baseExperience := int32(7)
experienceMultiplier := int32(3)
level := int32(0)
requiredExperience := int32(0)
nextLevelExperience := baseExperience
// Calculate level based on total experience
for {
// Calculate experience required to reach the next level
requiredExperience += nextLevelExperience
if player.XP < requiredExperience {
break
}
// Increment level and calculate experience required for the next level
level++
nextLevelExperience = baseExperience + (level*experienceMultiplier*(level+2))/2
}
// Calculate experience bar as a percentage of the next level
var experienceBar float32
if level > 0 {
experienceBar = float32(player.XP-requiredExperience+nextLevelExperience) / float32(nextLevelExperience) // Calculate experience bar
} else {
experienceBar = float32(player.XP) / float32(baseExperience) // If the level is 0, baseExperience is used
}
// Add Experience Bar (percentage of next level)
addFloat32(&buf, experienceBar)
// Add Level
addVarint(&buf, level)
// Add Total Experience
addVarint(&buf, player.XP)
// Send a packet with ID 0x5C
return player.sendPacket(0x5C, &buf)
}
// sendSetHealth sends a packet to update the player's health and food levels.
func (player *Player) sendSetHealth() error {
var buf bytes.Buffer
// Add Health
addFloat32(&buf, player.Entity.Health)
// Add Food Level
addVarint(&buf, player.FoodLevel)
// Add Food Saturation
addFloat32(&buf, player.FoodSaturation)
// Send a packet with ID 0x5D
return player.sendPacket(0x5D, &buf)
}
//TODO 0x5E update objectives
func (player *Player) sendPassengers(entity *Entity) error {
var buf bytes.Buffer
addVarint(&buf, entity.ID)
addVarint(&buf, int32(len(entity.Passengers)))
for _, pass := range entity.Passengers {
addVarint(&buf, pass.ID)
}
return player.sendPacket(0x5F, &buf)
}
//TODO 0X60 are teams
//TODO 0x61 is scoreboard update
func (player *Player) sendSimulationDistance() error {
var buf bytes.Buffer
addVarint(&buf, world.SimulationDistance)
return player.sendPacket(0x62, &buf)
}
func (player *Player) sendSubtitleText(text TextComponent) error {
var buf bytes.Buffer
addTextComponent(&buf, text)
return player.sendPacket(0x63, &buf)
}
func (player *Player) sendTime() error {
var buf bytes.Buffer
addInt64(&buf, player.Dimension.World.Age)
addInt64(&buf, player.Dimension.World.Time)
return player.sendPacket(0x64, &buf)
}
func (player *Player) sendTitleText(text TextComponent) error {
var buf bytes.Buffer
addTextComponent(&buf, text)
return player.sendPacket(0x65, &buf)
}
func (player *Player) sendTitleTimes(fadeIn, stay, fadeout int32) error {
var buf bytes.Buffer
addInt32(&buf, fadeIn)
addInt32(&buf, stay)
addInt32(&buf, fadeout)
return player.sendPacket(0x66, &buf)
}
func (player *Player) sendEntitySound(sound Sound, entity *Entity) error {
var buf bytes.Buffer
addVarint(&buf, sound.ID)
if sound.ID == 0 {
addString(&buf, sound.Name)
addBool(&buf, sound.FixedRange)
if sound.FixedRange {
addFloat32(&buf, sound.Range)
}
}
addVarint(&buf, sound.Category)
addVarint(&buf, entity.ID)
addFloat32(&buf, sound.Volume)
addFloat32(&buf, sound.Pitch)
addInt64(&buf, sound.Seed)
return player.sendPacket(0x67, &buf)
}
// sendSoundEffect sends a sound effect packet to the player.
func (player *Player) sendSoundEffect(sound Sound, position EntityPosition) error {
var buf bytes.Buffer
// Add Sound ID (Sound ID + 1; if 0, use Name)
addVarint(&buf, sound.ID+1)
// If Sound ID is 0, add the sound name, fixed range flag, and range
if sound.ID == 0 {
addString(&buf, sound.Name)
addBool(&buf, sound.FixedRange)
if sound.FixedRange {
addFloat32(&buf, sound.Range)
}
}
// Add Sound Category
addVarint(&buf, sound.Category)
// Add Effect Position X, Y, Z (multiplied by 8 to handle the fixed-point encoding)
position.encodeFixedPoint(&buf)
// Add Volume and Pitch
addFloat32(&buf, sound.Volume)
addFloat32(&buf, sound.Pitch)
// Add Seed
addInt64(&buf, sound.Seed)
// Send a packet with ID 0x68
return player.sendPacket(0x68, &buf)
}
func (player *Player) restartConfiguration() error {
var buf bytes.Buffer
return player.sendPacket(0x69, &buf)
}
// sendStopSound sends a packet to stop sound effects for the player.
func (player *Player) sendStopSound(source int32, soundName string) error {
var buf bytes.Buffer
var flags byte
if source >= 0 {
flags |= 0x01
}
if soundName != "" {
flags |= 0x02
}
// Add Flags
addByte(&buf, flags)
// If flags indicate that the Source is present (bit 0x1), add the Source
if flags&0x1 != 0 {
addVarint(&buf, source)
}
// If flags indicate that the Sound Name is present (bit 0x2), add the Sound Name
if flags&0x2 != 0 {
addString(&buf, soundName)
}
// Send packet with ID 0x6A
return player.sendPacket(0x6A, &buf)
}
//TODO 0x6B is store cookie
func (player *Player) sendSystemChatMessage(content TextComponent, overlay bool) error {
var buf bytes.Buffer
addTextComponent(&buf, content)
addBool(&buf, overlay)
return player.sendPacket(0x6C, &buf)
}
func (player *Player) sendTabHeaderAndFooter(header, footer TextComponent) error {
var buf bytes.Buffer
addTextComponent(&buf, header)
addTextComponent(&buf, footer)
return player.sendPacket(0x6D, &buf)
}
// sendTagQueryResponse sends a response to a query for a block/entity NBT tag.
func (player *Player) sendTagQueryResponse(transactionID int32, nbtData []byte) error {
var buf bytes.Buffer
// Add Transaction ID
addVarint(&buf, transactionID)
// Add NBT data (can be TAG_END if no NBT is present)
buf.Write(nbtData)
// Send packet with ID 0x6E
return player.sendPacket(0x6E, &buf)
}
// sendPickupItem sends a packet when an entity picks up an item.
func (player *Player) sendPickupItem(collectedEntityID, collectorEntityID, itemCount int32) error {
var buf bytes.Buffer
// Add Collected Entity ID
addVarint(&buf, collectedEntityID)
// Add Collector Entity ID
addVarint(&buf, collectorEntityID)
// Add Pickup Item Count
addVarint(&buf, itemCount)
// Send packet with ID 0x6F
return player.sendPacket(0x6F, &buf)
}
// sendTeleportEntity sends a teleport entity packet.
func (player *Player) sendTeleportEntity(entity *Entity) error {
var buf bytes.Buffer
// Add Entity ID
addVarint(&buf, entity.ID)
// Add Position (X, Y, Z)
entity.Position.add(&buf)
// Add Yaw and Pitch
entity.Rotation.addBasic(&buf)
// Add On a Ground flag
addBool(&buf, entity.onGround())
// Send packet with ID 0x70
return player.sendPacket(0x70, &buf)
}
// sendSetTickingState sends a packet to adjust the client's ticking state.
func (player *Player) sendSetTickingState() error {
var buf bytes.Buffer
// Add Tick Rate
addFloat32(&buf, world.TickRate)
// Add Is Frozen flag
addBool(&buf, world.TickRate <= 0)
// Send a packet with ID 0x71
return player.sendPacket(0x71, &buf)
}
// sendStepTick sends a packet to advance the client's processing by a specified number of ticks.
func (player *Player) sendStepTick(tickSteps int32) error {
var buf bytes.Buffer
// Add Tick Steps
addVarint(&buf, tickSteps)
// Send a packet with ID 0x72
return player.sendPacket(0x72, &buf)
}
// sendTransferPlay sends a packet to notify the client to transfer to a new server.
func (player *Player) sendTransferPlay(host string, port int32) error {
var buf bytes.Buffer
// Add Host (server hostname or IP)
addString(&buf, host)
// Add Port
addVarint(&buf, port)
// Send a packet with ID 0x73
return player.sendPacket(0x73, &buf)
}
//TODO 0x74 are advancements update
// sendUpdateAttributes sends an attribute update packet for the given entity.
func (player *Player) sendUpdateAttributes(entity *Entity) error {
var buf bytes.Buffer
// Add Entity ID
addVarint(&buf, entity.ID)
// Add Number Of Properties (Attributes)
addVarint(&buf, int32(len(entity.Attributes)))
// Iterate over all attributes and add them to the buffer
for _, attr := range entity.Attributes {
// Add EntityAttribute ID (VarInt Enum)
addVarint(&buf, attr.ID)
// Add EntityAttribute Value (Base Value)
addFloat64(&buf, attr.Value)
// Add Number Of Modifiers for this attribute
addVarint(&buf, int32(len(attr.Modifiers)))
// Add each modifier
for _, mod := range attr.Modifiers {
// Add Modifier ID (UUID or unique identifier)
addUUID(&buf, mod.ID)
// Add Modifier Amount
addFloat64(&buf, mod.Amount)
// Add Modifier Operation
addByte(&buf, mod.Operation)
}
}
// Send packet with ID 0x75
return player.sendPacket(0x75, &buf)
}
// sendEntityEffect sends an effect update packet to apply an effect on the given entity.
func (player *Player) sendEntityEffect(entityEffect *EntityEffect) error {
var buf bytes.Buffer
// Add Entity ID (VarInt)
addVarint(&buf, entityEffect.EntityID)
// Add Effect ID (VarInt)
addVarint(&buf, entityEffect.EffectID)
// Add Amplifier (VarInt)
addVarint(&buf, entityEffect.Amplifier)
// Add Duration (VarInt, use -1 for infinite)
addVarint(&buf, entityEffect.Duration)
// Add Flags (Byte, bitfield)
addByte(&buf, entityEffect.Flags)
// Send a packet with ID 0x76
return player.sendPacket(0x76, &buf)
}
// sendUpdateRecipes sends the Update Recipes packet to the client
func (player *Player) sendUpdateRecipes(packet *UpdateRecipesPacket) error {
var buf bytes.Buffer
// Add number of recipes (VarInt)
addVarint(&buf, packet.NumRecipes)
// Add each recipe entry
for _, recipeEntry := range packet.Recipes {
addVarint(&buf, recipeEntry.TypeID) // Add the recipe type (VarInt)
recipeEntry.encode(&buf) // Add the recipe-specific data
}
// Send the packet (0x77)
return player.sendPacket(0x77, &buf)
}
//0x78 is tags that are implemented cross-state
func (player *Player) sendProjectilePower(entity *Entity) error {
var buf bytes.Buffer
addVarint(&buf, entity.ID)
addFloat64(&buf, entity.ProjectilePower)
return player.sendPacket(0x79, &buf)
}
//0x7A are report details that are implemented cross-state
//0x7B are server links that are implemented cross-state

@ -232,7 +232,7 @@ func (player *Player) sendLoginSuccess() error {
var buf bytes.Buffer
addUUID(&buf, player.uuid)
addString(&buf, player.name)
addString(&buf, player.Entity.Name)
addVarint(&buf, int32(len(player.profile.Properties)))
@ -331,6 +331,12 @@ func (player *Player) removeResourcePack(packUUID uuid.UUID) (err error) {
if hasUUID {
addUUID(&buf, packUUID)
}
switch player.state {
case 3:
return player.sendPacket(0x08, &buf)
case 4:
return player.sendPacket(0x45, &buf)
}
return player.sendPacket(0x08, &buf)
}
@ -347,7 +353,13 @@ func (player *Player) addResourcePack(packUUID uuid.UUID, url string, hash strin
addString(&buf, message)
}
return player.sendPacket(0x09, &buf)
switch player.state {
case 3:
return player.sendPacket(0x09, &buf)
case 4:
return player.sendPacket(0x46, &buf)
}
return errors.New("invalid resource pack state")
}
func (player *Player) sendTransfer(host string, port uint16) (err error) {
@ -386,7 +398,13 @@ func (player *Player) sendUpdateTags() error {
}
}
}
return player.sendPacket(0x0D, &buf)
switch player.state {
case 3:
return player.sendPacket(0x0D, &buf)
case 4:
return player.sendPacket(0x78, &buf)
}
return errors.New("invalid update tags state")
}
func (player *Player) sendDataPacks() error {
@ -407,7 +425,14 @@ func (player *Player) sendReportDetails() (err error) {
addString(&buf, key)
addString(&buf, value)
}
return player.sendPacket(0x0F, &buf)
switch player.state {
case 3:
return player.sendPacket(0x0F, &buf)
case 4:
return player.sendPacket(0x7A, &buf)
}
return errors.New("invalid report details state")
}
func (player *Player) sendServerLinks() error {
@ -423,7 +448,14 @@ func (player *Player) sendServerLinks() error {
}
addString(&buf, serverLink.URL)
}
return player.sendPacket(0x10, &buf)
switch player.state {
case 3:
return player.sendPacket(0x10, &buf)
case 4:
return player.sendPacket(0x7B, &buf)
}
return errors.New("invalid server links state")
}
func (player *Player) sendPluginMessage(channel string, data *bytes.Buffer) (err error) {
@ -461,7 +493,7 @@ func (player *Player) sendStatusResponse(description string) (err error) {
continue
}
playerSamples = append(playerSamples, PlayerSample{
Name: player.name,
Name: player.Entity.Name,
ID: player.uuid.String(),
})
}

@ -5,9 +5,12 @@ import (
"bytes"
"crypto/cipher"
"encoding/binary"
"fmt"
"github.com/google/uuid"
"io"
"math"
"net"
"reflect"
"time"
)
@ -62,9 +65,15 @@ func (abilities *PlayerAbilites) export() (out byte) {
return
}
type Inventory struct {
activeSlot uint8
slots []ItemStack
}
type Player struct {
conn net.Conn
PermissionLevel uint8
state int32
version int32
requestedAddress string
@ -82,8 +91,11 @@ type Player struct {
Dimension *Dimension
DeathDimension *Dimension
DeathPosition BlockPosition
CursorStack ItemStack
Abilites PlayerAbilites
WindowID uint8
Window *Window
FlyingSpeed float32
FOVModifier float32
MessageIndex int32
@ -92,6 +104,7 @@ type Player struct {
CombatEnd int64
Position EntityPosition
Rotation EntityRotation
GameMode uint8
PreviousGameMode int8
@ -109,6 +122,7 @@ type Player struct {
keepAliveSentTimestamp int64
keepAliveReceivedTimestamp int64
completionID int32
pingID uint32
pingSentTimestamp int64
pingReceivedTimestamp int64
@ -116,7 +130,18 @@ type Player struct {
lastContainerStateID int32
name string
teleportID int32
waitingForTeleportConfirm bool
portalCooldown int32
inventory Inventory
advancementTab string
XP int32
FoodLevel int32
FoodSaturation float32
uuid uuid.UUID
}
@ -135,9 +160,48 @@ func (player *Player) endCombat() {
}
}
func (player *Player) processChatCommand(command string) {
for _, iterPlayer := range player.Dimension.World.Players {
iterPlayer.sendSystemChatMessage(getTextComponent(fmt.Sprintf("%s ran %s", player.Entity.Name, command)), false)
}
}
func (player *Player) handleChatMessage(message string) {
for _, iterPlayer := range player.Dimension.World.Players {
iterPlayer.sendChatMessage(player, message)
}
}
func (player *Player) requestCommandSuggestions(id int32, text string) {
player.completionID = id
//TODO implement
}
func (player *Player) IsInSurvivalMode() bool {
return player.GameMode == 0
}
func (player *Player) IsInCreativeMode() bool {
return player.GameMode == 1
}
func (player *Player) IsInAdventureMode() bool {
return player.GameMode == 2
}
func (player *Player) IsInSpectatorMode() bool {
return player.GameMode == 3
}
type Dimension struct {
ID int32
Name string
ID int32
Name string
Chunks [][][]Chunk
Border WorldBorder
RenderDistance int32
SpawnPosition EntityPosition
SpawnAngle float32
World *World
}
type World struct {
@ -156,6 +220,9 @@ type World struct {
Flat bool
EnforceSecureChat bool
Age int64
Time int64
Entities []Entity
Players []Player
RegistryDataThings []RegistryData
@ -165,6 +232,8 @@ type World struct {
ReportDetails map[string]string
ServerLinks []ServerLink
TickRate float32
Maps map[int32]Map
}
@ -180,6 +249,18 @@ type EntityPosition struct {
Z float64
}
func (entityPosition *EntityPosition) encodeFixedPoint(buf *bytes.Buffer) {
addInt32(buf, int32(entityPosition.X*8))
addInt32(buf, int32(entityPosition.Y*8))
addInt32(buf, int32(entityPosition.Z*8))
}
func (entityPosition *EntityPosition) toChunkPos() (chunkX, chunkZ int32) {
chunkX = int32(int64(entityPosition.X) >> 4)
chunkZ = int32(int64(entityPosition.Z) >> 4)
return
}
type FloatOffset struct {
X float32
Y float32
@ -209,6 +290,17 @@ type ExplosionSound struct {
FixedRange float32
}
type Sound struct {
ID int32
Name string
FixedRange bool
Range float32
Category int32
Volume float32
Pitch float32
Seed int64
}
func (explosionSound *ExplosionSound) add(buf *bytes.Buffer) {
addString(buf, explosionSound.Name)
hasFixedRange := explosionSound.FixedRange > 0
@ -395,13 +487,27 @@ type Explosion struct {
}
type Slot struct {
Stack *ItemStack
Number int16
}
func (s Slot) IsEmpty() bool {
return s.Stack.Count == 0
}
type ItemStack struct {
Count int32
ItemID int32
ComponentsToAdd map[int32]any
ComponentsToRemove []int32
MaxCount int32
}
func (slot *Slot) add(buf *bytes.Buffer) {
func (slot *ItemStack) IsEmpty() bool {
return slot.Count == 0
}
func (slot *ItemStack) add(buf *bytes.Buffer) {
addVarint(buf, slot.Count)
if slot.Count > 0 {
addVarint(buf, slot.ItemID)
@ -417,7 +523,7 @@ func (slot *Slot) add(buf *bytes.Buffer) {
}
}
func (slot *Slot) addTrade(buf *bytes.Buffer) {
func (slot *ItemStack) addTrade(buf *bytes.Buffer) {
addVarint(buf, slot.Count)
if slot.Count > 0 {
addVarint(buf, slot.ItemID)
@ -432,6 +538,264 @@ func (slot *Slot) addTrade(buf *bytes.Buffer) {
}
}
func (slot *ItemStack) splitStack() ItemStack {
if slot.Count > 1 {
inNum := slot.Count
outNum := slot.Count / 2
inNum -= outNum
slot.Count = inNum
out := *slot
out.Count = outNum
return out
}
return ItemStack{}
}
func (slot *ItemStack) takeOne() *ItemStack {
if slot.Count > 1 {
slot.Count -= 1
out := *slot
out.Count = 1
return &out
}
return &ItemStack{}
}
func (slot *ItemStack) IsSameType(item *ItemStack) bool {
return slot.ItemID == item.ItemID && reflect.DeepEqual(slot.ComponentsToAdd, item.ComponentsToAdd) && reflect.DeepEqual(slot.ComponentsToRemove, item.ComponentsToRemove)
}
func (slot *ItemStack) Clone() ItemStack {
return *slot
}
func (slot *ItemStack) AddToStack(i *ItemStack) {
if slot.Count == 0 {
slot.Count = i.Count
slot.ItemID = i.ItemID
slot.ComponentsToAdd = i.ComponentsToAdd
slot.ComponentsToRemove = i.ComponentsToRemove
}
}
func (slot *ItemStack) IsFull() bool {
return slot.Count >= slot.MaxCount
}
func (slot *ItemStack) TakeAll() *ItemStack {
newSlot := *slot
slot.Count = 0
return &newSlot
}
func (player *Player) handleClickContainer(packet ClickContainerPacket) error {
if player.WindowID < 255 {
return fmt.Errorf("invalid window ID")
}
switch packet.Mode {
case 0: // Normal click mode
switch packet.Button {
case 0: // Left click
handleLeftClick(player.Window, packet.Slot, packet.CarriedItem)
case 1: // Right click
handleRightClick(player.Window, packet.Slot, packet.CarriedItem)
default:
return fmt.Errorf("unknown button: %d", packet.Button)
}
case 1: // Shift-click mode
handleShiftClick(player.Window, packet.Slot, packet.Button)
case 2: // Number key
handleNumberKeyClick(player.Window, packet.Slot, packet.Button)
case 3: // Middle click (for creative mode)
if player.IsInCreativeMode() {
handleMiddleClick(player.Window, packet.Slot, player)
}
case 4: // Drop key
handleDropKey(player.Window, packet.Slot, packet.Button)
case 5: // Dragging items (painting mode)
handleDraggingMode(player.Window, packet)
case 6: // Double click
handleDoubleClick(player.Window, packet.Slot)
default:
return fmt.Errorf("unknown mode: %d", packet.Mode)
}
return nil
}
func handleLeftClick(window *Window, slot int16, carriedItem ItemStack) {
if carriedItem.IsEmpty() {
// Pick up the stack from the slot
window.Slots[slot] = Slot{} // Clear the slot
} else {
// Place the carried item into the slot
window.setSlot(slot, &carriedItem)
}
}
func handleRightClick(window *Window, slot int16, carriedItem ItemStack) {
slotData := window.GetSlot(slot)
if carriedItem.IsEmpty() {
// Pick up half of the stack in the slot
window.Player.CursorStack = slotData.splitStack()
window.setSlot(slot, slotData) // Update with remaining items
} else {
// Place one item from the carried stack into the slot
if slotData.IsEmpty() {
window.setSlot(slot, carriedItem.takeOne())
} else if slotData.IsSameType(&carriedItem) {
window.AddToSlot(slot, carriedItem.takeOne())
}
}
}
func handleShiftClick(window *Window, slot int16, button int8) {
item := window.GetSlot(slot)
if item.IsEmpty() {
return
}
if button == 0 {
// Move the item to the player's inventory or hotbar
window.MoveItemToOtherInventory(item, slot)
} else {
// Handle shift-right-click if needed
}
}
func handleMiddleClick(window *Window, slot int16, player *Player) {
if !player.IsInCreativeMode() {
return // Middle click is only allowed in creative mode
}
itemInSlot := window.GetSlot(slot)
if itemInSlot.IsEmpty() {
return // No item in the slot to duplicate
}
clonedItem := itemInSlot.Clone() // Clone the item
// Give the player the cloned item
player.CursorStack = clonedItem
}
func handleDropKey(window *Window, slot int16, button int8) {
itemInSlot := window.GetSlot(slot)
if itemInSlot.IsEmpty() {
return // No item to drop
}
if button == 0 {
// Drop the entire stack
dropItem(itemInSlot, window.Player.Position)
window.setSlot(slot, &ItemStack{}) // Remove the stack from the slot
} else if button == 1 {
// Drop a single item (Ctrl + Drop)
singleItem := itemInSlot.takeOne()
dropItem(singleItem, window.Player.Position)
if itemInSlot.IsEmpty() {
window.setSlot(slot, &ItemStack{}) // Clear the slot if empty
} else {
window.setSlot(slot, itemInSlot) // Update the remaining stack
}
}
}
func dropItem(item *ItemStack, position EntityPosition) {
//TODO This function handles the actual item drop in the world
// It could be implemented to spawn an entity that represents the dropped item
}
func handleDoubleClick(window *Window, slot int16) {
itemInSlot := window.GetSlot(slot)
if itemInSlot.IsEmpty() {
return // Nothing to double-click on
}
carriedItem := itemInSlot.Clone() // Pick up the clicked stack
window.setSlot(slot, &ItemStack{}) // Clear the clicked slot
// Loop through the entire inventory and pick up items of the same type
for i := range window.Slots {
slotItem := window.GetSlot(int16(i))
if slotItem.IsSameType(&carriedItem) {
carriedItem.AddToStack(slotItem.TakeAll())
window.setSlot(int16(i), slotItem) // Update the slot with the remaining stack
}
if carriedItem.IsFull() {
break // Stop if the carried stack is full
}
}
// Set the player's carried item to the collected stack
window.Player.CursorStack = carriedItem
}
func handleNumberKeyClick(window *Window, slot int16, button int8) {
hotbarIndex := button - 1 // The Button corresponds to number keys 1-9
if hotbarIndex < 0 || hotbarIndex >= 9 {
return // Invalid hotbar index
}
hotbarSlot := window.GetSlot(int16(hotbarIndex)) // Get the hotbar slot
clickedSlot := window.GetSlot(slot) // Get the clicked slot
// Swap the hotbar slot with the clicked slot
window.setSlot(int16(hotbarIndex), clickedSlot)
window.setSlot(slot, hotbarSlot)
}
func handleDraggingMode(window *Window, packet ClickContainerPacket) {
switch packet.Button {
case 0, 4, 8: // Start dragging
startDragging(packet)
case 1, 5, 9: // Add slot to drag
for _, changedSlot := range packet.ChangedSlots {
window.setSlot(changedSlot.Number, changedSlot.Stack)
}
case 2, 6, 10: // End dragging
endDragging(packet)
}
}
func synchronizeContainer(player *Player, window *Window) {
// Compare slot data with the server's state and resync if necessary
player.sendContainerContent(window, &player.CursorStack)
}
type ClickContainerPacket struct {
WindowID uint8 // Unsigned Byte
StateID int32 // Last received State ID
Slot int16 // Clicked slot number
Button int8 // Byte: the button used
Mode int32 // Inventory operation mode
ChangedSlots []Slot // Array of changed slots
CarriedItem ItemStack // The item currently carried by the cursor
}
// PalettedContainer represents a palette-based storage of entries.
type PalettedContainer struct {
BitsPerEntry uint8
@ -636,14 +1000,66 @@ func (bossBar BossBar) updateFlags(player *Player) error {
}
func (blockPosition *BlockPosition) add(buf *bytes.Buffer) {
var xBuf, yBuf, zBuf []byte
binary.BigEndian.PutUint32(xBuf, uint32(blockPosition.X))
binary.BigEndian.PutUint16(yBuf, uint16(blockPosition.Y))
binary.BigEndian.PutUint32(zBuf, uint32(blockPosition.Z))
// Get the 26-bit values for X and Z
x := blockPosition.X
z := blockPosition.Z
y := blockPosition.Y
buf.Write(xBuf[0:26])
buf.Write(zBuf[0:26])
buf.Write(yBuf[0:12])
// Pack the values into a byte slice
var positionBuf [4]byte // Enough to hold both x and z (4 bytes total)
positionBuf[0] = byte((x & 0x3FFFFFF) >> 18) // Upper 8 bits of x
positionBuf[1] = byte((x & 0x3FFFF) >> 10) // Middle 8 bits of x
positionBuf[2] = byte((x & 0x3FF) >> 2) // Lower 8 bits of x
positionBuf[3] = byte(((x & 0x3) << 6) | ((z & 0x3FFFFFF) >> 20)) // Last 2 bits of x and upper 6 bits of z
// Write X and Z to the buffer
buf.Write(positionBuf[:])
// Continue packing Z into the next part of the byte slice
positionBuf[0] = byte((z & 0x3FFFFFF) >> 18) // Upper 8 bits of z
positionBuf[1] = byte((z & 0x3FFFF) >> 10) // Middle 8 bits of z
positionBuf[2] = byte((z & 0x3FF) >> 2) // Lower 8 bits of z
positionBuf[3] = byte((z & 0x3) << 6) // Last 2 bits of z
// Write Z to the buffer
buf.Write(positionBuf[:])
// Pack Y into the buffer (12-bit signed integer)
yBuf := [2]byte{
byte((y & 0xFFF) >> 4), // Upper 8 bits of y
byte((y & 0xF) << 4), // Last 4 bits of y
}
// Write Y to the buffer
buf.Write(yBuf[:])
}
func (blockPosition *BlockPosition) receivePosition(buf *bytes.Buffer) error {
if buf.Len() < 10 { // 8 bytes for x and z, 2 bytes for y
return io.ErrUnexpectedEOF
}
// Read the first 8 bytes for x and z
positionBuf := make([]byte, 8)
if _, err := buf.Read(positionBuf); err != nil {
return err
}
// Extract X from the first 4 bytes
blockPosition.X = int32(positionBuf[0]&0x3F)<<18 | int32(positionBuf[1])<<10 | int32(positionBuf[2])<<2 | int32(positionBuf[3]&0x3F)
// Extract Z from the next 4 bytes
blockPosition.Z = int32(positionBuf[4]&0x3F)<<18 | int32(positionBuf[5])<<10 | int32(positionBuf[6])<<2 | int32(positionBuf[7]&0x3F)
// Read the next 2 bytes for y
yBuf := make([]byte, 2)
if _, err := buf.Read(yBuf); err != nil {
return err
}
blockPosition.Y = int16(int32(yBuf[0])<<4 | (int32(yBuf[1]) >> 4))
return nil
}
func (entityPosition *EntityPosition) add(buf *bytes.Buffer) {
@ -670,6 +1086,11 @@ func (entityRotation *EntityRotation) add(buf *bytes.Buffer) {
addByte(buf, entityRotation.HeadYaw)
}
func (entityRotation *EntityRotation) addBasic(buf *bytes.Buffer) {
addByte(buf, entityRotation.Pitch)
addByte(buf, entityRotation.Yaw)
}
type Velocity struct {
X float32
Y float32
@ -682,10 +1103,31 @@ func (velocity *Velocity) add(buf *bytes.Buffer) {
addFloat32(buf, velocity.Z)
}
type Modifier struct {
ID uuid.UUID // UUID or another unique identifier for the modifier
Amount float64 // Modifier value (positive or negative)
Operation byte // Operation type (0 = Add, 1 = Add Percentage, 2 = Multiply Percentage)
}
type EntityAttribute struct {
ID int32 // The ID of the attribute (e.g., generic.max_health = 16)
Value float64 // The base value of the attribute
Modifiers []Modifier // List of modifiers applied to the attribute
}
type EntityEffect struct {
EntityID int32 // The ID of the entity being affected
EffectID int32 // ID of the effect (see effect table below)
Amplifier int32 // The effect level (amplifier + 1 is displayed)
Duration int32 // Duration in ticks (-1 for infinite)
Flags byte // Bitfield for ambient, particles, icon, and blend
}
type Entity struct {
World *World
ID int32
UUID uuid.UUID
Name string
Type int32
Position EntityPosition
Rotation EntityRotation
@ -696,6 +1138,11 @@ type Entity struct {
PortalCooldown int32
Vehicle *Entity
PlayersInformed []*Player
Health float32
Passengers []*Entity
Attributes []EntityAttribute // List of attributes for the entity
Effects []EntityEffect // List of effects applied to the entity
ProjectilePower float64
}
// Function to calculate the Euclidean distance between two points in 3D space
@ -763,6 +1210,128 @@ func (XPOrb *XPOrb) add(buf *bytes.Buffer) {
addInt16(buf, XPOrb.Count)
}
// UpdateRecipesPacket Base struct for the Update Recipes packet
type UpdateRecipesPacket struct {
NumRecipes int32 // Number of recipes
Recipes []RecipeEntry // List of recipes
}
// RecipeEntry Base struct for different types of recipes
type RecipeEntry struct {
TypeID int32 // The recipe type (see recipe types table)
Data Recipe // Data for this recipe type
}
// Recipe Different recipe types
type Recipe interface{}
// ShapedCraftingRecipe Shaped Crafting Recipe
type ShapedCraftingRecipe struct {
Group string // Group tag for similar recipes
Category int32 // Category: Building, Redstone, etc.
Width, Height int32 // Dimensions of the crafting grid
Ingredients []Ingredient // Ingredients for the recipe
Result ItemStack // Resulting item
ShowNotification bool // Whether to show a toast notification
}
// ShapelessCraftingRecipe Shapeless Crafting Recipe
type ShapelessCraftingRecipe struct {
Group string // Group tag for similar recipes
Category int32 // Category: Building, Redstone, etc.
Ingredients []Ingredient // List of ingredients (unordered)
Result ItemStack // Resulting item
ShowNotification bool // Whether to show a toast notification
}
// SmeltingRecipe Smelting, Blasting, Smoking, and Campfire Cooking Recipes
type SmeltingRecipe struct {
Group string // Group tag for similar recipes
Category int32 // Category: Food, Blocks, Misc
Ingredient Ingredient // Smelting ingredient
Result ItemStack // Resulting item
Experience float32 // XP reward
CookingTime int32 // Time to cook in ticks
}
type Ingredient struct {
Count int32 // Number of possible items for this ingredient
Items []ItemStack // List of possible items (slots) that can satisfy this ingredient
}
func (recipe *RecipeEntry) encode(buf *bytes.Buffer) {
switch recipe := recipe.Data.(type) {
case ShapedCraftingRecipe:
encodeShapedCrafting(buf, recipe)
case ShapelessCraftingRecipe:
encodeShapelessCrafting(buf, recipe)
case SmeltingRecipe:
encodeSmelting(buf, recipe)
// Other recipe types can be handled similarly...
}
}
func (ingredient *Ingredient) add(buf *bytes.Buffer) {
addVarint(buf, ingredient.Count) // Number of possible items
for _, slot := range ingredient.Items {
slot.add(buf)
}
}
func encodeShapedCrafting(buf *bytes.Buffer, recipe ShapedCraftingRecipe) {
addString(buf, recipe.Group)
addVarint(buf, recipe.Category)
addVarint(buf, recipe.Width)
addVarint(buf, recipe.Height)
// Add each ingredient
for _, ingredient := range recipe.Ingredients {
ingredient.add(buf)
}
// Add result slot
recipe.Result.add(buf)
// Add a show notification flag (boolean)
addBool(buf, recipe.ShowNotification)
}
func encodeShapelessCrafting(buf *bytes.Buffer, recipe ShapelessCraftingRecipe) {
addString(buf, recipe.Group)
addVarint(buf, recipe.Category)
// Add number of ingredients (VarInt)
addVarint(buf, int32(len(recipe.Ingredients)))
// Add each ingredient
for _, ingredient := range recipe.Ingredients {
ingredient.add(buf)
}
// Add result slot
recipe.Result.add(buf)
// Add a show notification flag (boolean)
addBool(buf, recipe.ShowNotification)
}
func encodeSmelting(buf *bytes.Buffer, recipe SmeltingRecipe) {
addString(buf, recipe.Group)
addVarint(buf, recipe.Category)
// Add ingredient
recipe.Ingredient.add(buf)
// Add result slot
recipe.Result.add(buf)
// Add experience (float)
addFloat32(buf, recipe.Experience)
// Add cooking time (VarInt)
addVarint(buf, recipe.CookingTime)
}
type MapIcon struct {
Type int32
X int8
@ -824,9 +1393,9 @@ func (Map *Map) add(buf *bytes.Buffer) {
}
type VillageTrade struct {
In1 Slot
In2 *Slot
Out Slot
In1 ItemStack
In2 *ItemStack
Out ItemStack
Disabled bool
Uses int32
MaxUses int32
@ -870,6 +1439,48 @@ func (VillagerEntity *VillagerEntity) addOffers(buf *bytes.Buffer, windowID int3
addBool(buf, VillagerEntity.CanRestock)
}
type Window struct {
ID int32
type WindowInteractible interface {
click()
}
type Window struct {
ID uint8
Slots []Slot
Type int32
Player *Player
}
func (window *Window) setSlot(index int16, stack *ItemStack) {
window.Slots[index] = Slot{
Stack: stack,
Number: index,
}
}
func (window *Window) GetSlot(slot int16) *ItemStack {
return window.Slots[slot].Stack
}
func (window *Window) AddToSlot(slotIndex int16, stack *ItemStack) {
if window.Slots[slotIndex].Stack.Count == 0 {
window.Slots[slotIndex].Stack = stack
stack.Count = 0
} else if window.Slots[slotIndex].Stack.IsSameType(stack) && !window.Slots[slotIndex].Stack.IsFull() {
addedCount := stack.Count
leftOverCount := int32(0)
if addedCount > stack.MaxCount {
stack.MaxCount = addedCount
leftOverCount = stack.Count - addedCount
}
window.Slots[slotIndex].Stack.Count += addedCount
stack.Count = leftOverCount
}
}
type EntityMetadata struct {
//TODO IMPLEMENT
}
func (metadata *EntityMetadata) add(buf *bytes.Buffer) {
//TODO IMPLEMENT
}