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.
133 lines
3.2 KiB
Go
133 lines
3.2 KiB
Go
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")
|
|
}
|
|
}
|