Initial commit: RegionIO Minecraft server core (26.1.2/protocol 775)

Vanilla-faithful overworld generator (final_density + multi-noise biomes),
full connection lifecycle (status/login/configuration/play), chunk streaming,
creative block editing, and the protocol/nbt/registry infrastructure.
This commit is contained in:
Master290 2026-06-24 00:32:51 +03:00
commit a7bb9496ae
146 changed files with 217621 additions and 0 deletions

View file

@ -0,0 +1,220 @@
package network
import (
"errors"
"regionio/internal/protocol"
"regionio/internal/registry"
)
// ClientSettings holds the subset of client_information we currently track.
type ClientSettings struct {
Locale string
ViewDistance int8
ChatMode int32
MainHand int32
}
// beginConfiguration is called on entering the configuration phase. It mirrors
// the vanilla opening sequence: server brand, enabled feature flags, then the
// known-packs negotiation. The client's known-packs reply triggers the registry
// data (see handleKnownPacks).
func (h *handler) beginConfiguration() error {
if err := h.sendBrand(); err != nil {
return err
}
if err := h.sendEnabledFeatures(); err != nil {
return err
}
return h.sendKnownPacks()
}
// sendBrand reports the server brand on the minecraft:brand plugin channel.
func (h *handler) sendBrand() error {
w := protocol.NewWriter(32)
w.String("minecraft:brand").String("RegionIO")
return h.conn.SendWriter(protocol.ConfigCustomPayloadCB, w)
}
// sendEnabledFeatures enables the vanilla feature flag set. The client requires
// this so that vanilla content (blocks, items, registries) is active.
func (h *handler) sendEnabledFeatures() error {
w := protocol.NewWriter(24)
w.VarInt(1)
w.String("minecraft:vanilla")
return h.conn.SendWriter(protocol.ConfigUpdateEnabledFeatures, w)
}
// handleConfiguration drives the configuration phase:
//
// S→C select_known_packs (on entry)
// C→S client_information → record settings
// C→S custom_payload (brand) → log the client brand
// C→S select_known_packs → send registry data, then finish_configuration
// C→S finish_configuration → switch to the Play phase
func (h *handler) handleConfiguration(pkt protocol.Packet) error {
switch pkt.ID {
case protocol.ConfigClientInformation:
return h.handleClientInformation(pkt)
case protocol.ConfigCustomPayload:
return h.handleConfigCustomPayload(pkt)
case protocol.ConfigKnownPacksServer:
return h.handleKnownPacks(pkt)
case protocol.ConfigKeepAliveServer, protocol.ConfigPong,
protocol.ConfigResourcePackResp, protocol.ConfigCookieResponse:
h.log.Debug("configuration packet ignored", "id", pkt.ID)
return nil
case protocol.ConfigFinishServerbound:
h.conn.SetState(protocol.StatePlay)
h.log.Info("entered play phase", "name", h.conn.Profile.Name)
return h.beginPlay()
default:
h.log.Debug("unknown configuration packet", "id", pkt.ID)
return nil
}
}
// handleClientInformation records the client's settings (no longer the trigger
// to finish configuration; that is now driven by known-packs negotiation).
func (h *handler) handleClientInformation(pkt protocol.Packet) error {
r := pkt.Body()
locale, err := r.String()
if err != nil {
return err
}
vd, err := r.ReadByte()
if err != nil {
return err
}
chatMode, err := r.VarInt()
if err != nil {
return err
}
if _, err := r.Bool(); err != nil { // chat colors
return err
}
if _, err := r.ReadByte(); err != nil { // displayed skin parts
return err
}
mainHand, err := r.VarInt()
if err != nil {
return err
}
h.log.Info("client information",
"locale", locale, "view_distance", int8(vd),
"chat_mode", chatMode, "main_hand", mainHand)
return nil
}
// handleConfigCustomPayload logs the client brand and ignores other channels.
func (h *handler) handleConfigCustomPayload(pkt protocol.Packet) error {
r := pkt.Body()
channel, err := r.String()
if err != nil {
return err
}
if channel == "minecraft:brand" {
brand, err := r.String()
if err != nil {
return err
}
h.log.Info("client brand", "brand", brand)
} else {
h.log.Debug("plugin message", "channel", channel)
}
return nil
}
// handleKnownPacks reads the client's known packs and, once we know what it
// already has, sends the registry data followed by finish_configuration.
func (h *handler) handleKnownPacks(pkt protocol.Packet) error {
r := pkt.Body()
count, err := r.VarInt()
if err != nil {
return err
}
if count < 0 || count > 1024 {
return errors.New("implausible known-pack count")
}
hasCore := false
for i := int32(0); i < count; i++ {
ns, err := r.String()
if err != nil {
return err
}
id, err := r.String()
if err != nil {
return err
}
ver, err := r.String()
if err != nil {
return err
}
if ns == registry.CorePack.Namespace && id == registry.CorePack.ID &&
ver == registry.CorePack.Version {
hasCore = true
}
h.log.Debug("client known pack", "namespace", ns, "id", id, "version", ver)
}
if !hasCore {
// Without the matching pack the client cannot fill in registry data
// from its built-in copy; sending has_data=false would desync it.
h.log.Warn("client lacks matching core pack; registry data may desync",
"want", registry.CorePack.Version)
}
if err := h.sendRegistries(); err != nil {
return err
}
if err := h.sendUpdateTags(); err != nil {
return err
}
return h.sendFinishConfiguration()
}
// sendKnownPacks advertises the vanilla core pack to the client.
func (h *handler) sendKnownPacks() error {
p := registry.CorePack
w := protocol.NewWriter(32)
w.VarInt(1)
w.String(p.Namespace).String(p.ID).String(p.Version)
return h.conn.SendWriter(protocol.ConfigKnownPacksCB, w)
}
// sendRegistries sends one registry_data packet per synchronized registry. Each
// entry is sent with has_data=false; the client supplies the contents from its
// matching known pack.
func (h *handler) sendRegistries() error {
for _, reg := range registry.Synced() {
w := protocol.NewWriter(64 + len(reg.Entries)*24)
w.String(reg.Name)
w.VarInt(int32(len(reg.Entries)))
for _, entry := range reg.Entries {
w.String(entry)
w.Bool(false) // has_data: client uses its own copy
}
if err := h.conn.SendWriter(protocol.ConfigRegistryData, w); err != nil {
return err
}
}
h.log.Debug("sent registry data", "registries", len(registry.Synced()))
return nil
}
// sendUpdateTags sends the captured vanilla tag set. Tags map registry entries
// (by numeric index) into named groups the client and gameplay rely on.
func (h *handler) sendUpdateTags() error {
return h.conn.Send(protocol.ConfigUpdateTags, registry.Tags())
}
// sendFinishConfiguration signals configuration is complete; the client replies
// with its own finish_configuration to enter Play.
func (h *handler) sendFinishConfiguration() error {
return h.conn.Send(protocol.ConfigFinishClientbound, nil)
}

89
internal/network/conn.go Normal file
View file

@ -0,0 +1,89 @@
// Package network owns the TCP listener and per-connection lifecycle: framing,
// the handshake state machine, and dispatch to per-state handlers.
package network
import (
"bufio"
"net"
"sync"
"regionio/internal/protocol"
"regionio/internal/server"
)
// Conn wraps a TCP connection with buffered reads and tracks protocol state.
type Conn struct {
raw net.Conn
br *bufio.Reader
state protocol.State
// compressionThreshold is -1 until Set Compression is negotiated.
compressionThreshold int32
// Profile is populated once the client identifies during login.
Profile server.Profile
// writeMu serializes writes so a background sender (e.g. keep-alive) and
// the read-loop handler never interleave bytes on the wire.
writeMu sync.Mutex
}
// NewConn wraps a raw TCP connection.
func NewConn(raw net.Conn) *Conn {
return &Conn{
raw: raw,
br: bufio.NewReaderSize(raw, 4096),
state: protocol.StateHandshaking,
compressionThreshold: -1,
}
}
// State returns the current protocol state.
func (c *Conn) State() protocol.State { return c.state }
// SetState transitions to a new protocol state.
func (c *Conn) SetState(s protocol.State) { c.state = s }
// EnableCompression sets the compression threshold for all subsequent packets.
// The caller must send the Set Compression packet (uncompressed) first.
func (c *Conn) EnableCompression(threshold int32) { c.compressionThreshold = threshold }
// CompressionEnabled reports whether compression is active.
func (c *Conn) CompressionEnabled() bool { return c.compressionThreshold >= 0 }
// RemoteAddr returns the peer address for logging.
func (c *Conn) RemoteAddr() net.Addr { return c.raw.RemoteAddr() }
// ReadPacket reads the next frame using the current compression settings.
func (c *Conn) ReadPacket() (protocol.Packet, error) {
return protocol.ReadPacket(c.br, c.compressionThreshold)
}
// Send writes a packet with the given ID and pre-encoded body. Safe for
// concurrent use.
func (c *Conn) Send(id int32, body []byte) error {
c.writeMu.Lock()
defer c.writeMu.Unlock()
return protocol.WritePacket(c.raw, c.compressionThreshold, id, body)
}
// SendWriter writes a packet whose body was built with a protocol.Writer.
func (c *Conn) SendWriter(id int32, w *protocol.Writer) error {
return c.Send(id, w.Bytes())
}
// SendFramed writes an already-framed packet (e.g. a cached chunk) verbatim.
// The frame must have been built for this connection's compression threshold.
// Safe for concurrent use.
func (c *Conn) SendFramed(frame []byte) error {
c.writeMu.Lock()
defer c.writeMu.Unlock()
_, err := c.raw.Write(frame)
return err
}
// CompressionThreshold returns the active threshold (-1 if disabled).
func (c *Conn) CompressionThreshold() int32 { return c.compressionThreshold }
// Close closes the underlying connection.
func (c *Conn) Close() error { return c.raw.Close() }

133
internal/network/handler.go Normal file
View file

@ -0,0 +1,133 @@
package network
import (
"errors"
"io"
"log/slog"
"net"
"regionio/internal/protocol"
"regionio/internal/server"
)
// handler drives one connection through its state machine until it closes.
// It is owned by a single goroutine (the read loop), so the play fields below
// need no synchronization.
type handler struct {
conn *Conn
srv *server.Server
log *slog.Logger
// Play-phase chunk streaming state.
loaded map[[2]int32]bool // chunks currently sent to the client
centerX int32
centerZ int32
hasCenter bool
// Creative inventory state for block placement.
heldSlot int32 // selected hotbar index (0-8)
hotbar [9]int32 // item network IDs per hotbar slot (-1 = empty)
}
// serve runs the read/dispatch loop for a single connection.
func (h *handler) serve() {
defer h.conn.Close()
for {
pkt, err := h.conn.ReadPacket()
if err != nil {
if !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
h.log.Debug("connection closed", "err", err)
}
return
}
if err := h.dispatch(pkt); err != nil {
h.log.Debug("dispatch error", "state", h.conn.State(), "id", pkt.ID, "err", err)
return
}
}
}
// dispatch routes a packet to the handler for the current state.
func (h *handler) dispatch(pkt protocol.Packet) error {
switch h.conn.State() {
case protocol.StateHandshaking:
return h.handleHandshake(pkt)
case protocol.StateStatus:
return h.handleStatus(pkt)
case protocol.StateLogin:
return h.handleLogin(pkt)
case protocol.StateConfiguration:
return h.handleConfiguration(pkt)
case protocol.StatePlay:
return h.handlePlay(pkt)
default:
return errors.New("no handler for state " + h.conn.State().String())
}
}
// handleHandshake reads the single handshake packet and transitions state.
func (h *handler) handleHandshake(pkt protocol.Packet) error {
if pkt.ID != protocol.HandshakeID {
return errors.New("unexpected packet in handshaking state")
}
r := pkt.Body()
protoVer, err := r.VarInt()
if err != nil {
return err
}
addr, err := r.String()
if err != nil {
return err
}
port, err := r.Uint16()
if err != nil {
return err
}
next, err := r.VarInt()
if err != nil {
return err
}
h.log.Debug("handshake",
"protocol", protoVer, "addr", addr, "port", port, "next", next)
switch next {
case protocol.NextStateStatus:
h.conn.SetState(protocol.StateStatus)
case protocol.NextStateLogin, protocol.NextStateTransfer:
h.conn.SetState(protocol.StateLogin)
default:
return errors.New("invalid next state in handshake")
}
return nil
}
// handleStatus answers the server-list ping: status request and ping/pong.
func (h *handler) handleStatus(pkt protocol.Packet) error {
switch pkt.ID {
case protocol.StatusRequestID:
jsonBytes, err := h.srv.StatusJSON(0)
if err != nil {
return err
}
w := protocol.NewWriter(len(jsonBytes) + 4)
w.String(string(jsonBytes))
return h.conn.SendWriter(protocol.StatusResponseID, w)
case protocol.PingRequestID:
// Echo the client's payload back verbatim for latency measurement.
payload, err := pkt.Body().Int64()
if err != nil {
return err
}
w := protocol.NewWriter(8)
w.Int64(payload)
return h.conn.SendWriter(protocol.PongResponseID, w)
default:
return errors.New("unexpected packet in status state")
}
}

View file

@ -0,0 +1,64 @@
package network
import (
"context"
"fmt"
"log/slog"
"net"
"regionio/internal/server"
)
// Listener accepts TCP connections and serves each in its own goroutine.
type Listener struct {
srv *server.Server
log *slog.Logger
}
// NewListener constructs a Listener bound to srv.
func NewListener(srv *server.Server, log *slog.Logger) *Listener {
return &Listener{srv: srv, log: log}
}
// ListenAndServe binds the configured address and accepts connections until
// ctx is cancelled.
func (l *Listener) ListenAndServe(ctx context.Context) error {
cfg := l.srv.Config()
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var lc net.ListenConfig
ln, err := lc.Listen(ctx, "tcp", addr)
if err != nil {
return fmt.Errorf("listen on %s: %w", addr, err)
}
l.log.Info("RegionIO listening", "addr", addr, "version", "26.1.2")
// Close the listener when the context is cancelled to unblock Accept.
go func() {
<-ctx.Done()
_ = ln.Close()
}()
for {
raw, err := ln.Accept()
if err != nil {
if ctx.Err() != nil {
return nil // graceful shutdown
}
l.log.Warn("accept failed", "err", err)
continue
}
go l.serveConn(raw)
}
}
// serveConn wraps a raw connection and runs its state-machine handler.
func (l *Listener) serveConn(raw net.Conn) {
conn := NewConn(raw)
h := &handler{
conn: conn,
srv: l.srv,
log: l.log.With("peer", raw.RemoteAddr().String()),
}
h.serve()
}

95
internal/network/login.go Normal file
View file

@ -0,0 +1,95 @@
package network
import (
"errors"
"regionio/internal/protocol"
"regionio/internal/server"
)
// handleLogin drives the offline-mode login phase:
//
// C→S Login Start → derive offline profile
// S→C Set Compression → (optional) enable zlib for later packets
// S→C Login Success → confirm the profile
// C→S Login Acknowledged → switch to the Configuration phase
//
// Encryption (online mode) is intentionally not implemented here.
func (h *handler) handleLogin(pkt protocol.Packet) error {
switch pkt.ID {
case protocol.LoginStartID:
return h.handleLoginStart(pkt)
case protocol.LoginAcknowledgedID:
// Client acknowledges Login Success; both sides enter Configuration.
h.conn.SetState(protocol.StateConfiguration)
h.log.Info("player logged in",
"name", h.conn.Profile.Name,
"uuid", uuidString(h.conn.Profile.UUID))
return h.beginConfiguration()
default:
return errors.New("unexpected packet in login state")
}
}
func (h *handler) handleLoginStart(pkt protocol.Packet) error {
r := pkt.Body()
name, err := r.String()
if err != nil {
return err
}
if name == "" || len(name) > 16 {
return errors.New("invalid login name")
}
// The client also sends a UUID, but in offline mode we derive our own so it
// is stable and independent of what the client claims.
if _, err := r.UUID(); err != nil {
return err
}
h.conn.Profile = server.Profile{
UUID: server.OfflineUUID(name),
Name: name,
}
// Negotiate compression before Login Success so that packet (and every
// later one) is sent in the compressed format.
if t := h.srv.Config().CompressionThreshold; t >= 0 {
w := protocol.NewWriter(protocol.VarIntLen(int32(t)))
w.VarInt(int32(t))
if err := h.conn.SendWriter(protocol.SetCompressionID, w); err != nil {
return err
}
h.conn.EnableCompression(int32(t))
}
return h.sendLoginSuccess()
}
// sendLoginSuccess writes the Login Success packet. For protocol 775 the body
// is: UUID, Username, then a VarInt-prefixed array of profile properties (none
// in offline mode).
func (h *handler) sendLoginSuccess() error {
p := h.conn.Profile
w := protocol.NewWriter(16 + len(p.Name) + 2)
w.UUID(p.UUID)
w.String(p.Name)
w.VarInt(0) // property count
return h.conn.SendWriter(protocol.LoginSuccessID, w)
}
// uuidString formats a UUID as the canonical 8-4-4-4-12 hex string.
func uuidString(u [16]byte) string {
const hexdigits = "0123456789abcdef"
var b [36]byte
j := 0
for i := 0; i < 16; i++ {
if i == 4 || i == 6 || i == 8 || i == 10 {
b[j] = '-'
j++
}
b[j] = hexdigits[u[i]>>4]
b[j+1] = hexdigits[u[i]&0x0f]
j += 2
}
return string(b[:j])
}

404
internal/network/play.go Normal file
View file

@ -0,0 +1,404 @@
package network
import (
"math"
"time"
"regionio/internal/nbt"
"regionio/internal/protocol"
"regionio/internal/registry"
"regionio/internal/world"
)
// Spawn coordinates. Y sits above the maximum terrain height so the player
// drops onto the generated surface rather than spawning inside it.
const (
spawnX = 8.5
spawnY = 200.0
spawnZ = 8.5
)
// chunkRadius is how many chunks around the player we send (a square of side
// 2*radius+1). Kept modest until streaming by view distance exists.
const chunkRadius = 4
// beginPlay sends the join sequence once the client enters the Play phase and
// starts the keep-alive loop. In milestone 4a no chunks are sent, so the client
// reaches the "loading terrain" screen and waits.
func (h *handler) beginPlay() error {
for i := range h.hotbar {
h.hotbar[i] = -1 // empty
}
if err := h.sendPlayLogin(); err != nil {
return err
}
// "Start waiting for level chunks": tells the client to show the loading
// screen until chunks arrive.
if err := h.sendGameEvent(protocol.GameEventStartWaitingChunks, 0); err != nil {
return err
}
if err := h.sendPlayerPosition(1); err != nil {
return err
}
if err := h.streamAround(0, 0); err != nil {
return err
}
go h.keepAliveLoop()
return nil
}
// streamAround recenters the client's chunk cache on (centerX, centerZ) and
// sends the chunks newly in range. Chunks that fall out of range are dropped by
// the client automatically once it receives the new cache center, so we only
// send the difference and track the currently-loaded set.
func (h *handler) streamAround(centerX, centerZ int32) error {
cc := protocol.NewWriter(8)
cc.VarInt(centerX)
cc.VarInt(centerZ)
if err := h.conn.SendWriter(protocol.PlayChunkCacheCenter, cc); err != nil {
return err
}
cache := h.srv.Chunks()
next := make(map[[2]int32]bool, (2*chunkRadius+1)*(2*chunkRadius+1))
sent := 0
for cx := centerX - chunkRadius; cx <= centerX+chunkRadius; cx++ {
for cz := centerZ - chunkRadius; cz <= centerZ+chunkRadius; cz++ {
key := [2]int32{cx, cz}
next[key] = true
if h.loaded[key] {
continue
}
if err := h.conn.SendFramed(cache.Frame(cx, cz)); err != nil {
return err
}
sent++
}
}
h.loaded = next
h.centerX, h.centerZ, h.hasCenter = centerX, centerZ, true
h.log.Debug("streamed chunks", "center_x", centerX, "center_z", centerZ, "new", sent)
return nil
}
// onPlayerMove recenters chunk streaming when the player crosses into a new
// chunk. X and Z are the player's block-precise coordinates.
func (h *handler) onPlayerMove(x, z float64) error {
cx := int32(int64(math.Floor(x)) >> 4)
cz := int32(int64(math.Floor(z)) >> 4)
if h.hasCenter && cx == h.centerX && cz == h.centerZ {
return nil
}
return h.streamAround(cx, cz)
}
// sendPlayLogin writes the clientbound play "login" packet. Field layout was
// confirmed against the 26.1.2 vanilla server capture.
func (h *handler) sendPlayLogin() error {
dimTypeIdx := registry.Index("minecraft:dimension_type", "minecraft:overworld")
if dimTypeIdx < 0 {
dimTypeIdx = 0
}
w := protocol.NewWriter(128)
w.Int32(1) // entity ID
w.Bool(false) // is hardcore
// Dimension names: the worlds available on this server.
dims := []string{"minecraft:overworld", "minecraft:the_end", "minecraft:the_nether"}
w.VarInt(int32(len(dims)))
for _, d := range dims {
w.String(d)
}
w.VarInt(int32(h.srv.Config().MaxPlayers)) // max players (legacy)
w.VarInt(10) // view distance
w.VarInt(10) // simulation distance
w.Bool(false) // reduced debug info
w.Bool(true) // enable respawn screen
w.Bool(false) // do limited crafting
w.VarInt(int32(dimTypeIdx)) // dimension type (registry index)
w.String("minecraft:overworld") // dimension name (this world)
w.Int64(0) // hashed seed
w.Byte(1) // game mode: creative (instant break, creative inventory)
w.Byte(0xFF) // previous game mode: -1 (none)
w.Bool(false) // is debug
w.Bool(false) // is flat
w.Bool(false) // has death location
w.VarInt(0) // portal cooldown
w.VarInt(63) // sea level (overworld)
w.Bool(false) // enforces secure chat
return h.conn.SendWriter(protocol.PlayLogin, w)
}
// sendGameEvent writes a game_event packet (event id + float value).
func (h *handler) sendGameEvent(event byte, value float32) error {
w := protocol.NewWriter(5)
w.Byte(event)
w.Float32(value)
return h.conn.SendWriter(protocol.PlayGameEvent, w)
}
// sendPlayerPosition teleports the player to spawn. The client must echo the
// teleport ID back via accept_teleportation.
func (h *handler) sendPlayerPosition(teleportID int32) error {
w := protocol.NewWriter(64)
w.VarInt(teleportID)
w.Float64(spawnX).Float64(spawnY).Float64(spawnZ) // position
w.Float64(0).Float64(0).Float64(0) // velocity
w.Float32(0) // yaw
w.Float32(0) // pitch
w.Int32(0) // relative flags
return h.conn.SendWriter(protocol.PlayPlayerPosition, w)
}
// keepAliveLoop sends a keep-alive every 15 seconds. It exits as soon as a send
// fails, which happens when the connection closes.
func (h *handler) keepAliveLoop() {
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
for range ticker.C {
id := time.Now().UnixMilli()
w := protocol.NewWriter(8)
w.Int64(id)
if err := h.conn.SendWriter(protocol.PlayKeepAliveCB, w); err != nil {
return
}
}
}
// handlePlay dispatches serverbound play packets. Most are tolerated for now;
// teleport and keep-alive are acknowledged/logged.
func (h *handler) handlePlay(pkt protocol.Packet) error {
switch pkt.ID {
case protocol.PlayAcceptTeleport:
id, err := pkt.Body().VarInt()
if err != nil {
return err
}
h.log.Debug("teleport confirmed", "id", id)
return nil
case protocol.PlayKeepAliveServer:
// A response to our keep-alive; presence is enough for liveness.
h.log.Debug("keep-alive ack")
return nil
case protocol.PlayPlayerLoaded:
h.log.Info("player loaded into world", "name", h.conn.Profile.Name)
return nil
case protocol.PlayMovePos, protocol.PlayMovePosRot:
// Both packets begin with the absolute X, Y, Z position.
r := pkt.Body()
x, err := r.Float64()
if err != nil {
return err
}
if _, err := r.Float64(); err != nil { // feet Y, unused for streaming
return err
}
z, err := r.Float64()
if err != nil {
return err
}
return h.onPlayerMove(x, z)
case protocol.PlayPlayerAction:
return h.handlePlayerAction(pkt)
case protocol.PlayChatMessage:
return h.handleChat(pkt)
case protocol.PlayUseItemOn:
return h.handleUseItemOn(pkt)
case protocol.PlaySetCarriedItem:
slot, err := pkt.Body().Uint16()
if err != nil {
return err
}
if slot < 9 {
h.heldSlot = int32(slot)
}
return nil
case protocol.PlaySetCreativeSlot:
return h.handleCreativeSlot(pkt)
default:
h.log.Debug("play packet ignored", "id", pkt.ID)
return nil
}
}
// handleChat reads a chat message (only the leading text field is needed) and
// echoes it to the player as a system message prefixed with their name. Once a
// player registry exists this will broadcast to everyone.
func (h *handler) handleChat(pkt protocol.Packet) error {
msg, err := pkt.Body().String()
if err != nil {
return err
}
line := "<" + h.conn.Profile.Name + "> " + msg
h.log.Info("chat", "msg", line)
return h.sendSystemChat(line)
}
// sendSystemChat sends a plain-text system chat message. The text component is
// network NBT; a bare string tag is the shorthand for {"text": ...}.
func (h *handler) sendSystemChat(text string) error {
w := protocol.NewWriter(len(text) + 8)
w.Raw(nbt.Marshal(nbt.String(text)))
w.Bool(false) // not an action-bar overlay
return h.conn.SendWriter(protocol.PlaySystemChat, w)
}
// handlePlayerAction processes digging. In creative the client sends
// START_DESTROY_BLOCK (status 0) for an instant break; survival also sends
// STOP/FINISH (status 2). Either way we clear the block, push a block_update,
// and acknowledge the sequence so the client keeps its predicted change.
func (h *handler) handlePlayerAction(pkt protocol.Packet) error {
r := pkt.Body()
status, err := r.VarInt()
if err != nil {
return err
}
x, y, z, err := r.Position()
if err != nil {
return err
}
if _, err := r.ReadByte(); err != nil { // face
return err
}
seq, err := r.VarInt()
if err != nil {
return err
}
const startDig, finishDig = 0, 2
if status == startDig || status == finishDig {
if h.srv.Chunks().SetBlock(x, y, z, world.StateAir) {
if err := h.sendBlockUpdate(x, y, z, world.StateAir); err != nil {
return err
}
h.log.Debug("block broken", "x", x, "y", y, "z", z)
}
}
return h.sendBlockChangedAck(seq)
}
// hotbarInvStart is the inventory slot index of hotbar slot 0.
const hotbarInvStart = 36
// handleCreativeSlot records the item a creative player placed into a slot so we
// know what block to place. The packet is: Short slot, then an item stack
// (VarInt count; if non-empty, VarInt item id followed by components we ignore).
func (h *handler) handleCreativeSlot(pkt protocol.Packet) error {
r := pkt.Body()
slot, err := r.Uint16()
if err != nil {
return err
}
hotbarIdx := int(slot) - hotbarInvStart
if hotbarIdx < 0 || hotbarIdx >= len(h.hotbar) {
return nil // not a hotbar slot; ignored
}
count, err := r.VarInt()
if err != nil {
return err
}
if count <= 0 {
h.hotbar[hotbarIdx] = -1 // emptied
return nil
}
itemID, err := r.VarInt()
if err != nil {
return err
}
h.hotbar[hotbarIdx] = itemID // remaining component data is not needed
return nil
}
// faceOffsets maps a Direction (block face) to the unit offset of the block
// placed against it: DOWN, UP, NORTH, SOUTH, WEST, EAST.
var faceOffsets = [6][3]int{
{0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}, {-1, 0, 0}, {1, 0, 0},
}
// handleUseItemOn places the held block against the clicked face. Layout
// (captured from the client): Hand, Position, Face, cursor XYZ floats,
// insideBlock bool, worldBorderHit bool, sequence.
func (h *handler) handleUseItemOn(pkt protocol.Packet) error {
r := pkt.Body()
if _, err := r.VarInt(); err != nil { // hand
return err
}
x, y, z, err := r.Position()
if err != nil {
return err
}
face, err := r.VarInt()
if err != nil {
return err
}
// Skip cursor (3 floats) + insideBlock + worldBorderHit, then read sequence.
for i := 0; i < 3; i++ {
if _, err := r.Float32(); err != nil {
return err
}
}
if _, err := r.Bool(); err != nil {
return err
}
if _, err := r.Bool(); err != nil {
return err
}
seq, err := r.VarInt()
if err != nil {
return err
}
if face >= 0 && int(face) < len(faceOffsets) {
if state, ok := h.heldBlock(); ok {
off := faceOffsets[face]
px, py, pz := x+off[0], y+off[1], z+off[2]
if h.srv.Chunks().SetBlock(px, py, pz, state) {
if err := h.sendBlockUpdate(px, py, pz, state); err != nil {
return err
}
h.log.Debug("block placed", "x", px, "y", py, "z", pz, "state", state)
}
}
}
return h.sendBlockChangedAck(seq)
}
// heldBlock returns the block state for the currently held item, if it is a
// placeable block.
func (h *handler) heldBlock() (uint16, bool) {
itemID := h.hotbar[h.heldSlot]
if itemID < 0 {
return 0, false
}
return world.ItemToBlock(itemID)
}
// sendBlockUpdate notifies the client of a single block change.
func (h *handler) sendBlockUpdate(x, y, z int, state uint16) error {
w := protocol.NewWriter(12)
w.Position(x, y, z)
w.VarInt(int32(state))
return h.conn.SendWriter(protocol.PlayBlockUpdate, w)
}
// sendBlockChangedAck confirms a block-action sequence so the client does not
// roll back its predicted change.
func (h *handler) sendBlockChangedAck(sequence int32) error {
w := protocol.NewWriter(4)
w.VarInt(sequence)
return h.conn.SendWriter(protocol.PlayBlockChangedAck, w)
}