Files
mcsrv/structs.go
2024-12-23 08:46:49 +01:00

1517 lines
39 KiB
Go

package main
import (
"bufio"
"bytes"
"crypto/cipher"
"encoding/binary"
"fmt"
"github.com/google/uuid"
"io"
"math"
"net"
"reflect"
"time"
)
type Version struct {
Name string `json:"name"`
Protocol int `json:"protocol"`
}
type PlayerSample struct {
Name string `json:"name"`
ID string `json:"id"`
}
type Players struct {
Max int32 `json:"max"`
Online int `json:"online"`
Sample []PlayerSample `json:"sample"`
}
type Description struct {
Text string `json:"text"`
}
type ServerStatus struct {
Version Version `json:"version"`
Players Players `json:"players,omitempty"`
Description Description `json:"description,omitempty"`
Favicon string `json:"favicon,omitempty"`
EnforcesSecureChat bool `json:"enforcesSecureChat"`
}
type PlayerAbilites struct {
Invulnerable bool
Flying bool
AllowFlying bool
CreativeMode bool
}
func (abilities *PlayerAbilites) export() (out byte) {
if abilities.Invulnerable {
out |= 0x01
}
if abilities.Flying {
out |= 0x02
}
if abilities.AllowFlying {
out |= 0x04
}
if abilities.CreativeMode {
out |= 0x08
}
return
}
type Inventory struct {
activeSlot uint8
slots []ItemStack
}
type Player struct {
conn net.Conn
PermissionLevel uint8
state int32
version int32
requestedAddress string
requestedPort uint16
compressionThreshold int32
verifyToken []byte
decryptStream cipher.Stream
encryptStream cipher.Stream
reader *bufio.Reader
profile MojangProfile
transferred bool
resourcePackResponse map[uuid.UUID]int32
knownPacks []DatapackInfo
Entity *Entity
Dimension *Dimension
DeathDimension *Dimension
DeathPosition BlockPosition
CursorStack ItemStack
Abilites PlayerAbilites
WindowID uint8
Window *Window
FlyingSpeed float32
FOVModifier float32
MessageIndex int32
CombatStart int64
CombatEnd int64
Position EntityPosition
Rotation EntityRotation
GameMode uint8
PreviousGameMode int8
locale string
viewDistance int8
chatMode int32
chatColors bool
skinParts uint8
isRightHanded bool
textFiltering bool
serverListing bool
keepAlivePayload int64
keepAliveSentTimestamp int64
keepAliveReceivedTimestamp int64
completionID int32
pingID uint32
pingSentTimestamp int64
pingReceivedTimestamp int64
statistics []StatisticPiece
lastContainerStateID int32
teleportID int32
waitingForTeleportConfirm bool
portalCooldown int32
inventory Inventory
advancementTab string
XP int32
FoodLevel int32
FoodSaturation float32
uuid uuid.UUID
}
func (player *Player) startCombat() {
player.CombatStart = time.Now().Unix()
_ = player.sendPacket(0x3B, &bytes.Buffer{})
}
func (player *Player) endCombat() {
player.CombatEnd = time.Now().Unix()
length := player.CombatEnd - player.CombatStart
if length >= 0 && player.CombatEnd != 0 && player.CombatStart != 0 && player.CombatEnd > player.CombatStart {
var buf bytes.Buffer
addInt64(&buf, length)
_ = player.sendPacket(0x3A, &buf)
}
}
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
Chunks [][][]Chunk
Border WorldBorder
RenderDistance int32
SpawnPosition EntityPosition
SpawnAngle float32
World *World
}
type World struct {
Difficulty uint8
DifficultyLocked bool
Hardcore bool
Dimensions []Dimension
ViewDistance int32
SimulationDistance int32
ReducedDebugInfo bool
EnableRespawnScreen bool
DoLimitedCrafting bool
Seed uuid.UUID
Debug bool
Flat bool
EnforceSecureChat bool
Age int64
Time int64
Entities []Entity
Players []Player
RegistryDataThings []RegistryData
FeatureFlags []string
UpdateTags []UpdateTag
DataPacks []DatapackInfo
ReportDetails map[string]string
ServerLinks []ServerLink
TickRate float32
Maps map[int32]Map
}
type StatisticPiece struct {
CategoryID int32
StatisticID int32
Value int32
}
type EntityPosition struct {
X float64
Y float64
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
Z float32
}
type BlockPosition struct {
X int32
Y int16
Z int32
}
type AffectedBlockRecord struct {
OffsetX int8
OffsetY int8
OffsetZ int8
}
func (affectedBlockRecord *AffectedBlockRecord) add(buf *bytes.Buffer) {
addByte(buf, byte(affectedBlockRecord.OffsetX))
addByte(buf, byte(affectedBlockRecord.OffsetY))
addByte(buf, byte(affectedBlockRecord.OffsetZ))
}
type ExplosionSound struct {
Name string
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
addBool(buf, hasFixedRange)
if hasFixedRange {
addFloat32(buf, explosionSound.FixedRange)
}
}
type Particle struct {
ParticleID int32
BlockState int32 // Used by block-related particles
FromColor [3]float32 // Used for color particles (RGB)
ToColor [3]float32 // Used for dust_color_transition particles (RGB)
Scale float32 // Used for dust/dust_color_transition particles
PositionSourceType int32 // Used for vibration particles
BlockPosition BlockPosition // Used for vibration particles (minecraft:block)
EntityID int32 // Used for vibration particles (minecraft:entity)
EntityEyeHeight float32 // Used for vibration particles (minecraft:entity)
Ticks int32 // Used for vibration particles
Delay int32 // Used for shriek particles
Count int32
LongDistance bool
Position EntityPosition
Offset FloatOffset
MaxSpeed float32
}
type WorldBorder struct {
X float64
Z float64
OldDiameter float64
NewDiameter float64
Speed int64
PortalTeleportBoundary int32
WarningBlocks int32
WarningTime int32
}
// Compute yaw between two positions
func calculateYaw(attackerPos, playerPos EntityPosition) float32 {
deltaX := attackerPos.X - playerPos.X
deltaZ := attackerPos.Z - playerPos.Z
// atan2 returns the angle in radians, we convert it to degrees
yaw := math.Atan2(deltaZ, deltaX) * (180 / math.Pi)
// In Minecraft, yaw 0 points towards positive Z, so we adjust the yaw to this system
yaw = -yaw + 90
// Normalize yaw to be between 0 and 360 degrees
if yaw < 0 {
yaw += 360
}
return float32(yaw)
}
func encodeARGB(color [3]float32) int32 {
// Encodes RGB color values into an ARGB int (alpha assumed to be 255)
r := int32(color[0] * 255)
g := int32(color[1] * 255)
b := int32(color[2] * 255)
a := int32(255)
return (a << 24) | (r << 16) | (g << 8) | b
}
func clampScale(scale float32) float32 {
// Clamps the scale between 0.01 and 4.0
if scale < 0.01 {
return 0.01
}
if scale > 4.0 {
return 4.0
}
return scale
}
func (p *Particle) addPacket(buf *bytes.Buffer, count int32) {
addBool(buf, p.LongDistance)
p.Position.add(buf)
p.Offset.add(buf)
addFloat32(buf, p.MaxSpeed)
addInt32(buf, count)
addInt32(buf, p.ParticleID)
// Handle based on particle ID
switch p.ParticleID {
case 1, 2, 28, 105: // minecraft:block, minecraft:block_marker, falling_dust, dust_pillar
addVarint(buf, p.BlockState)
case 13: // minecraft:dust
addFloat32(buf, p.FromColor[0])
addFloat32(buf, p.FromColor[1])
addFloat32(buf, p.FromColor[2])
addFloat32(buf, clampScale(p.Scale))
case 14: // minecraft:dust_color_transition
addFloat32(buf, p.FromColor[0])
addFloat32(buf, p.FromColor[1])
addFloat32(buf, p.FromColor[2])
addFloat32(buf, p.ToColor[0])
addFloat32(buf, p.ToColor[1])
addFloat32(buf, p.ToColor[2])
addFloat32(buf, clampScale(p.Scale))
case 20: // minecraft:entity_effect
addInt32(buf, encodeARGB(p.FromColor))
case 45: // minecraft:vibration
addVarint(buf, p.PositionSourceType)
if p.PositionSourceType == 0 {
p.BlockPosition.add(buf)
} else if p.PositionSourceType == 1 {
addVarint(buf, p.EntityID)
addFloat32(buf, p.EntityEyeHeight)
}
addVarint(buf, p.Ticks)
case 99: // minecraft:shriek
addVarint(buf, p.Delay)
// Particles with no additional data
case 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 32, 33, 34, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 100, 101, 102, 103, 104, 106, 107, 108:
// No additional data required
}
}
func (p *Particle) addExplosion(buf *bytes.Buffer) {
addInt32(buf, p.ParticleID)
// Handle based on particle ID
switch p.ParticleID {
case 1, 2, 28, 105: // minecraft:block, minecraft:block_marker, falling_dust, dust_pillar
addVarint(buf, p.BlockState)
case 13: // minecraft:dust
addFloat32(buf, p.FromColor[0])
addFloat32(buf, p.FromColor[1])
addFloat32(buf, p.FromColor[2])
addFloat32(buf, clampScale(p.Scale))
case 14: // minecraft:dust_color_transition
addFloat32(buf, p.FromColor[0])
addFloat32(buf, p.FromColor[1])
addFloat32(buf, p.FromColor[2])
addFloat32(buf, p.ToColor[0])
addFloat32(buf, p.ToColor[1])
addFloat32(buf, p.ToColor[2])
addFloat32(buf, clampScale(p.Scale))
case 20: // minecraft:entity_effect
addInt32(buf, encodeARGB(p.FromColor))
case 45: // minecraft:vibration
addVarint(buf, p.PositionSourceType)
if p.PositionSourceType == 0 {
p.BlockPosition.add(buf)
} else if p.PositionSourceType == 1 {
addVarint(buf, p.EntityID)
addFloat32(buf, p.EntityEyeHeight)
}
addVarint(buf, p.Ticks)
case 99: // minecraft:shriek
addVarint(buf, p.Delay)
// Particles with no additional data
case 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 32, 33, 34, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 100, 101, 102, 103, 104, 106, 107, 108:
// No additional data required
}
}
type Explosion struct {
Position EntityPosition
Strength float32
AffectedBlockRecords []AffectedBlockRecord
PlayerMotion EntityPosition
BlockInteraction int32
SmallExplosionParticle Particle
LargeExplosionParticle Particle
ExplosionSound ExplosionSound
}
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
}
// WriteToBuffer serializes the ItemStack to a bytes.Buffer
func (i *ItemStack) WriteToBuffer(buf *bytes.Buffer) error {
// Add Item Count as VarInt
addVarint(buf, i.Count)
// Add optional fields only if Count > 0
if i.Count > 0 {
// Add ItemID as VarInt
addVarint(buf, i.ItemID)
// Add Number of components to add as VarInt
addVarint(buf, int32(len(i.ComponentsToAdd)))
for k, v := range i.ComponentsToAdd {
addVarint(buf, k)
// Customize based on an actual type of 'v'
if strVal, ok := v.(string); ok {
addVarint(buf, int32(len(strVal)))
addString(buf, strVal)
}
}
// Add Number of components to remove as VarInt
addVarint(buf, int32(len(i.ComponentsToRemove)))
for _, v := range i.ComponentsToRemove {
addVarint(buf, v)
}
}
return nil
}
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)
addVarint(buf, int32(len(slot.ComponentsToAdd)))
addVarint(buf, int32(len(slot.ComponentsToRemove)))
for componentID, component := range slot.ComponentsToAdd {
addVarint(buf, componentID)
component = component //TODO IMPLEMENT ALL THESE STUPID COMPONENTS
}
for _, componentID := range slot.ComponentsToRemove {
addVarint(buf, componentID)
}
}
}
func (slot *ItemStack) addTrade(buf *bytes.Buffer) {
addVarint(buf, slot.Count)
if slot.Count > 0 {
addVarint(buf, slot.ItemID)
addVarint(buf, int32(len(slot.ComponentsToAdd)))
for componentID, component := range slot.ComponentsToAdd {
addVarint(buf, componentID)
component = component //TODO IMPLEMENT ALL THESE STUPID COMPONENTS
}
for _, componentID := range slot.ComponentsToRemove {
addVarint(buf, componentID)
}
}
}
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
Palette []int32
DataArray []uint64 // Packed data for blocks or biomes
}
// AddEntry adds an entry (block or biome) to the palette and updates the DataArray.
func (p *PalettedContainer) AddEntry(entry int32, index int) {
// Check if the entry already exists in the palette.
for i, val := range p.Palette {
if val == entry {
// Entry already exists, no need to addExplosion to palette.
p.setEntryAtIndex(i, index)
return
}
}
// Add the new entry to the palette.
p.Palette = append(p.Palette, entry)
// Use the index of the new entry in the palette and store it in the DataArray.
p.setEntryAtIndex(len(p.Palette)-1, index)
}
// setEntryAtIndex sets the palette index for the block/biome at the specified index in the DataArray.
func (p *PalettedContainer) setEntryAtIndex(paletteIndex int, index int) {
// Each entry is packed into the DataArray with the specified BitsPerEntry.
entriesPerLong := 64 / p.BitsPerEntry
longIndex := index / int(entriesPerLong)
bitIndex := (index % int(entriesPerLong)) * int(p.BitsPerEntry)
// Clear the bits at the specified location before setting the new value.
mask := uint64(math.MaxUint64>>(64-p.BitsPerEntry)) << bitIndex
p.DataArray[longIndex] &= ^mask
// Set the new value.
p.DataArray[longIndex] |= (uint64(paletteIndex) & (math.MaxUint64 >> (64 - p.BitsPerEntry))) << bitIndex
}
// add serializes the PalettedContainer to a buffer for network transmission.
func (p *PalettedContainer) add(buf *bytes.Buffer) error {
// Write BitsPerEntry
buf.WriteByte(p.BitsPerEntry)
// Write Palette
if len(p.Palette) > 0 {
// Write the length of the palette as a VarInt.
addVarint(buf, int32(len(p.Palette)))
// Write each palette entry as a VarInt.
for _, entry := range p.Palette {
addVarint(buf, entry)
}
} else {
// Write a palette length of 0 for a global palette.
addVarint(buf, 0)
}
// Write DataArray
// First, write the length of the DataArray (in terms of the number of longs).
addVarint(buf, int32(len(p.DataArray)))
// Write each uint64 in the DataArray as a little-endian long.
for _, data := range p.DataArray {
err := binary.Write(buf, binary.BigEndian, data)
if err != nil {
return err
}
}
return nil
}
// ChunkSection represents a 16x16x16 section of a chunk.
type ChunkSection struct {
BlockCount uint16 // Non-air block count
BlockStates PalettedContainer // Block palette and states
Biomes PalettedContainer // Biome palette and data
}
// add serializes the ChunkSection into the buffer.
func (cs *ChunkSection) add(buf *bytes.Buffer) error {
err := binary.Write(buf, binary.BigEndian, cs.BlockCount)
if err != nil {
return err
}
// Write BlockStates and Biomes.
err = cs.BlockStates.add(buf)
if err != nil {
return err
}
err = cs.Biomes.add(buf)
if err != nil {
return err
}
return nil
}
// Chunk represents a chunk column, containing multiple chunk sections.
type Chunk struct {
X int32 // Chunk X coordinate
Z int32 // Chunk Z coordinate
Sections []ChunkSection // Chunk sections (16x16x16)
}
// add serializes the Chunk into the buffer.
func (c *Chunk) add(buf *bytes.Buffer) error {
err := binary.Write(buf, binary.BigEndian, c.X)
if err != nil {
return err
}
err = binary.Write(buf, binary.BigEndian, c.Z)
if err != nil {
return err
}
// Write the number of sections as a VarInt.
addVarint(buf, int32(len(c.Sections)))
// Write each chunk section.
for _, section := range c.Sections {
err := section.add(buf)
if err != nil {
return err
}
}
return nil
}
type Block struct {
Position BlockPosition
BlockEntityType int32
BlockTypeID int32
BlockStateID int32
BlockEntityData map[string]any
DestroyStage uint8
}
type BossBar struct {
UUID uuid.UUID
Title TextComponent
Health float32
Color int32
Division int32
Flags uint8
}
func (bossBar BossBar) add(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 0)
addTextComponent(&buf, bossBar.Title)
addFloat32(&buf, bossBar.Health)
addVarint(&buf, bossBar.Color)
addVarint(&buf, bossBar.Division)
addByte(&buf, bossBar.Flags)
return player.sendPacket(0x0A, &buf)
}
func (bossBar BossBar) remove(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 1)
return player.sendPacket(0x0A, &buf)
}
func (bossBar BossBar) updateHealth(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 2)
addFloat32(&buf, bossBar.Health)
return player.sendPacket(0x0A, &buf)
}
func (bossBar BossBar) updateTitle(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 3)
addTextComponent(&buf, bossBar.Title)
return player.sendPacket(0x0A, &buf)
}
func (bossBar BossBar) updateStyle(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 4)
addVarint(&buf, bossBar.Color)
addVarint(&buf, bossBar.Division)
return player.sendPacket(0x0A, &buf)
}
func (bossBar BossBar) updateFlags(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 5)
addByte(&buf, bossBar.Flags)
return player.sendPacket(0x0A, &buf)
}
func (blockPosition *BlockPosition) add(buf *bytes.Buffer) {
// Get the 26-bit values for X and Z
x := blockPosition.X
z := blockPosition.Z
y := blockPosition.Y
// 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) {
addFloat64(buf, entityPosition.X)
addFloat64(buf, entityPosition.Y)
addFloat64(buf, entityPosition.Z)
}
func (floatOffset *FloatOffset) add(buf *bytes.Buffer) {
addFloat32(buf, floatOffset.X)
addFloat32(buf, floatOffset.Y)
addFloat32(buf, floatOffset.Z)
}
type EntityRotation struct {
Pitch uint8
Yaw uint8
HeadYaw uint8
}
func (entityRotation *EntityRotation) add(buf *bytes.Buffer) {
addByte(buf, entityRotation.Pitch)
addByte(buf, entityRotation.Yaw)
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
Z float32
}
func (velocity *Velocity) add(buf *bytes.Buffer) {
addFloat32(buf, velocity.X)
addFloat32(buf, velocity.Y)
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
Data int32
Velocity Velocity
Animation uint8
status int8
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
func distance(p1, p2 EntityPosition) float64 {
dx := p2.X - p1.X
dy := p2.Y - p1.Y
dz := p2.Z - p1.Z
return math.Sqrt(dx*dx + dy*dy + dz*dz)
}
func getDeltas(p1, p2 EntityPosition) []int16 {
return []int16{
int16((p2.X * 4096) - (p1.X * 4096)),
int16((p2.Y * 4096) - (p1.Y * 4096)),
int16((p2.Z * 4096) - (p1.Z * 4096)),
}
}
func (entity *Entity) move(newPos EntityPosition, withRotation bool) {
diff := distance(entity.Position, newPos)
if diff <= 7 {
if withRotation {
for _, player := range entity.PlayersInformed {
player.sendEntityPositionRotationUpdate(entity, getDeltas(entity.Position, newPos))
}
} else {
for _, player := range entity.PlayersInformed {
player.sendEntityPositionUpdate(entity, getDeltas(entity.Position, newPos))
}
}
} else {
for _, player := range entity.PlayersInformed {
//TODO make entity teleport
player = player
//player.sendEntityTeleport(entity, newPos)
}
}
entity.Position = newPos
}
func (entity *Entity) onGround() bool {
//TODO ADD THIS
return true
}
func (entity *Entity) add(buf *bytes.Buffer) {
addVarint(buf, entity.ID)
addUUID(buf, entity.UUID)
addVarint(buf, entity.Type)
entity.Position.add(buf)
entity.Rotation.add(buf)
addVarint(buf, entity.Data)
entity.Velocity.add(buf)
}
type XPOrb struct {
ID int32
Position EntityPosition
Count int16
}
func (XPOrb *XPOrb) add(buf *bytes.Buffer) {
addVarint(buf, XPOrb.ID)
XPOrb.Position.add(buf)
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
Z int8
Direction int8
DisplayName *TextComponent
}
func (MapIcon *MapIcon) add(buf *bytes.Buffer) {
addVarint(buf, MapIcon.Type)
addByte(buf, byte(MapIcon.X))
addByte(buf, byte(MapIcon.Z))
addByte(buf, byte(MapIcon.Direction))
hasTextComponent := MapIcon.DisplayName != nil
addBool(buf, hasTextComponent)
if hasTextComponent {
addTextComponent(buf, *MapIcon.DisplayName)
}
}
type Map struct {
ID int32
Scale int8
Locked bool
Icons []MapIcon
MapUpdateDataW uint8
MapUpdateDataH uint8
MapUpdateDataX uint8
MapUpdateDataZ uint8
MapUpdateData []uint8
}
func (Map *Map) add(buf *bytes.Buffer) {
addVarint(buf, Map.ID)
addByte(buf, byte(Map.Scale))
addBool(buf, Map.Locked)
iconCount := int32(len(Map.Icons))
addBool(buf, iconCount > 0)
if iconCount > 0 {
addVarint(buf, iconCount)
for _, mapIcon := range Map.Icons {
mapIcon.add(buf)
}
}
addByte(buf, Map.MapUpdateDataH)
if Map.MapUpdateDataH > 0 {
addByte(buf, Map.MapUpdateDataW)
addByte(buf, Map.MapUpdateDataX)
addByte(buf, Map.MapUpdateDataZ)
addVarint(buf, int32(len(Map.MapUpdateData)))
for _, mapUpdatePiece := range Map.MapUpdateData {
addByte(buf, mapUpdatePiece)
}
}
}
type VillageTrade struct {
In1 ItemStack
In2 *ItemStack
Out ItemStack
Disabled bool
Uses int32
MaxUses int32
XP int32
Modifier int32
Multiplier float32
Demand int32
}
func (villageTrade *VillageTrade) add(buf *bytes.Buffer) {
villageTrade.In1.addTrade(buf)
villageTrade.Out.add(buf)
villageTrade.In2.addTrade(buf)
addBool(buf, villageTrade.Disabled)
addInt32(buf, villageTrade.Uses)
addInt32(buf, villageTrade.MaxUses)
addInt32(buf, villageTrade.XP)
addInt32(buf, villageTrade.Modifier)
addFloat32(buf, villageTrade.Multiplier)
addInt32(buf, villageTrade.Demand)
}
type VillagerEntity struct {
Entity *Entity
Trades []VillageTrade
Level int32
XP int32
Wandering bool
CanRestock bool
}
func (VillagerEntity *VillagerEntity) addOffers(buf *bytes.Buffer, windowID int32) {
addVarint(buf, windowID)
addVarint(buf, int32(len(VillagerEntity.Trades)))
for _, trade := range VillagerEntity.Trades {
trade.add(buf)
}
addVarint(buf, VillagerEntity.Level)
addVarint(buf, VillagerEntity.XP)
addBool(buf, !VillagerEntity.Wandering)
addBool(buf, VillagerEntity.CanRestock)
}
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
}