RegionIO/internal/network/handler.go

150 lines
4.1 KiB
Go

package network
import (
"context"
"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); play-phase chunk streaming
// is delegated to a background streamer goroutine.
type handler struct {
conn *Conn
srv *server.Server
log *slog.Logger
// ctx is the connection lifetime context, set in serve(). It cancels when
// the read loop ends, stopping the streamer and any derived work.
ctx context.Context
// Background chunk streamer (Play phase). requestRecenter pushes here.
streamer *streamer
// viewDistance is the client's requested view distance (from
// client_information), clamped; used to size the streamer.
viewDistance int
session *server.PlayerSession
knownPlayers map[[16]byte]bool
knownEntities map[int32]visibleEntity
// 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. It owns the
// streamer's lifecycle: the streamer context is cancelled when this loop exits,
// stopping generation and sending so no goroutine outlives the connection.
func (h *handler) serve() {
defer h.conn.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // stops the streamer when the read loop ends
h.ctx = ctx
defer func() { h.srv.UnregisterPlayer(h.session) }()
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. The Play
// handler reads h.ctx (the connection lifetime context) to launch the streamer.
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(h.srv.PlayerCount())
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")
}
}