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 spawnY float64 // 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") } }