1517 lines
39 KiB
Go
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
|
|
}
|