RegionIO/internal/network/configuration.go
Master290 a7bb9496ae 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.
2026-06-24 00:32:51 +03:00

220 lines
6.3 KiB
Go

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)
}