diff --git a/API_REFERENCE.md b/API_REFERENCE.md deleted file mode 100644 index 356c900..0000000 --- a/API_REFERENCE.md +++ /dev/null @@ -1,407 +0,0 @@ -# Strom API Reference - -Strom is a Go Minecraft protocol library for building servers, bots, and proxies. - -## Packages - -### `mc/api` — Accounts & Authentication - -```go -// Account holds a player's name, UUID, and access token. -type Account struct{ Name string; UUID uuid.UUID; AccessToken string } - -// NewAccount creates a profile from name, UUID, and access token. -func NewAccount(name string, uuid uuid.UUID, ygg string) *Account - -// NewAccountFromYGG parses an YGGDRASIL-style access token into an Account. -func NewAccountFromYGG(ygg string) *Account - -// NewOfflineAccount creates an offline (cracked) account from a display name. -func NewOfflineAccount(name string) *Account - -// HasJoined validates a session with Mojang's session server. -func HasJoined(name, server, ip string) (ProfileResp, error) - -// Profile fetches a player profile by UUID (with or without signatures). -func Profile(id string, unsigned bool) (ProfileResp, error) - -// NameToId / NameToIdBulk look up UUIDs from player names. -func NameToId(name string) (IdName, error) -func NameToIdBulk(names []string) ([]IdName, error) - -// BlockedServers returns Mojang's blocklist. -func BlockedServers() ([]string, error) - -// MojangPublicKeys returns Mojang's current signing keys. -func MojangPublicKeys() (MojangPublicKeysResp, error) -``` - -### `mc/client` — Client Connections - -```go -// Connect dials a Minecraft server with SRV record lookup. -func Connect(ctx context.Context, addr string, version string) (*proto.Conn, error) - -// ConnectVersionLess dials a server without version negotiation (used for status pings). -func ConnectVersionLess(ctx context.Context, addr string) (*proto.Conn, error) - -// Login performs the full login handshake (online mode + encryption). -// Functional options: WithoutDns, WithContext, WithVersion. -func Login(connectTo string, account *api.Account, settings ...func(*loginSettings)) (*proto.Conn, error) - -// LoginRaw logs in on an already-connected proto.Conn. -func LoginRaw(c *proto.Conn, account *api.Account) error - -// IgnoreConfig discards all Configuration-state packets (use when bundling login → play). -func IgnoreConfig(c *proto.Conn) error - -// MakeHandshakePacket builds a Handshake packet for the given next state. -func MakeHandshakePacket(c *proto.Conn, nextState proto_base.State) (*v1_21_8.HandshakingToServerPacketSetProtocol, error) -func MakeHandshakePacketAddr(s *proto.Conn, nextState proto_base.State, addr string) (*v1_21_8.HandshakingToServerPacketSetProtocol, error) - -// Status pings a server and returns the parsed response. -func Status(ctx context.Context, addr string) (server.StatusResponse, error) - -// StatusNoDns is like Status but skips SRV record resolution. -func StatusNoDns(addr string) (server.StatusResponse, error) - -// StatusRaw returns a low-level StatusClient for custom status workflows. -func StatusRaw(ctx context.Context, addr string) (*StatusClient, error) -``` - -### `mc/client.StatusClient` — Programmatic Status Pings - -```go -type StatusClient struct { - *proto.Conn - Status string // raw JSON status response - PingSendTime time.Time - PingReceiveTime time.Time - DoPingRoundTripTime bool -} - -func (s *StatusClient) OnStatus(p *v1_21_8.StatusToClientPacketServerInfo) error -func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) error -``` - -### `mc/server` — Server / Listen Side - -```go -// StartServerWithOnConn listens on addr, accepts TCP connections, and calls onConn per connection. -func StartServerWithOnConn(listenAddr string, onConn func(c *proto.Conn) error) error - -// Servee wraps a raw net.Conn into a proto.Conn with Servee actor. -func Servee(cNet net.Conn) *proto.Conn - -// Kick sends a disconnect packet to the client. -func Kick(c *proto.Conn, reason *text.Component, status string) error - -// ServeLogin handles the full login sequence (handshake → encryption → login → config → play). -// Functional options: WithOtherAccount, WithRawStatus, WithStatus, -// WithCompatibleVersions, WithCompatibleVersionsRange, WithoutOnlineMode, WithoutEncryption. -func ServeLogin(c *proto.Conn, settings ...LoginServerSetting) (*LoginServer, error) - -// ServeStatus handles a status-only connection (server list ping). -func ServeStatus(c *proto.Conn, s StatusResponse) error - -// ServeConfig drives the configuration phase. -func ServeConfig(c *proto.Conn) error -``` - -### `mc/proto` — Connection & Packet Helpers - -```go -// NewConn / NewConnCtx create a new proto.Conn with an embedded event loop. -func NewConn() *Conn -func NewConnCtx(ctx context.Context) *Conn - -// SimplePacketToBytes encodes a typed packet to raw wire bytes (no length prefix). -func SimplePacketToBytes(packet proto_base.EncodeDecodeAble) ([]byte, error) - -// SimplePacketToBytesLenPrefix encodes a typed packet with varint length prefix. -func SimplePacketToBytesLenPrefix(packet proto_base.EncodeDecodeAble) ([]byte, error) - -// SimpleBytesToPacket decodes raw wire bytes into a typed packet. -func SimpleBytesToPacket(packetBytes []byte, version int32, direction proto_base.Direction, state proto_base.State) (proto_base.EncodeDecodeAble, error) - -// Packet lookup by type/name. -func LookUpTypeByPacketInfo(direction proto_base.Direction, state proto_base.State, pid, version int32) (proto_base.PacketInfo, bool) -func LookupPacketInfoByType(packet proto_base.EncodeDecodeAble) (proto_base.PacketInfo, bool) -func LookupPacketInfoByNameProtocolVersionAndState(name string, version int32, state proto_base.State) (proto_base.PacketInfo, bool) -``` - -#### `proto.Conn` Methods - -```go -// SetVersion loads the packet definitions for the given Minecraft version. -func (c *Conn) SetVersion(version string) error - -// SetState transitions the connection to a new protocol state (Handshake, Status, Login, Config, Play). -func (c *Conn) SetState(s proto_base.State) - -// State returns the current protocol state. -func (c *Conn) State() proto_base.State - -// Send encodes and writes a typed packet for the current state. -func (c *Conn) Send(packet proto_base.EncodeDecodeAble) error - -// Receive reads, decodes, and returns the next packet. -func (c *Conn) Receive() (proto_base.EncodeDecodeAble, error) - -// StartConn begins the event loop — blocks until the connection closes. -func (c *Conn) StartConn() error - -// Fire dispatches an event through the handler chain. -func (c *Conn) Fire(event any) error - -// RegisterCritical registers a handler that closes the loop if it returns an error. -func (c *Conn) RegisterCritical(hs ...any) - -// RegisterCriticalUntilLatest registers handlers for all versions between the packet version and the latest supported version. -// Auto-unregisters when SetVersion bumps the version. -func (c *Conn) RegisterCriticalUntilLatest(hs ...any) - -// RegisterCriticalUntil registers handlers up to the specified version. -func (c *Conn) RegisterCriticalUntil(until string, hs ...any) - -// HandlerFunctions maps packet types to registered handler reflect.Values. -// Useful for direct handler invocation in tests. -func (c *Conn) HandlerFunctions map[reflect.Type][]reflect.Value -``` - -### `mc/event` — Event Loop - -```go -// NewLoop / NewLoopCtx create an event loop. -func NewLoop() *Loop -func NewLoopCtx(ctx context.Context) *Loop - -// RegisterCritical registers a handler that closes the loop on error. -func (l *Loop) RegisterCritical(hs ...any) - -// FireFound dispatches an event to matching handlers. -func (l *Loop) FireFound(event any) (found bool, err error) - -// ValidateHandler checks that a function matches the handler signature (func(packet T) error). -func ValidateHandler(h any) (eventType reflect.Type, hv reflect.Value) -``` - -### `mc/proto_base` — Wire Types & Primitives - -```go -// Packet types and metadata lookup. -type State int32 // Handshake, Status, Login, Config, Play -type Direction int // ToServer, ToClient -type Actor int // Server, Client -type PacketInfo struct { Direction Direction; State State; Version int32; PacketId int32 } - -// Varint encoding. -func EncodeVarInt(w io.Writer, v int32) error -func DecodeVarInt(r io.Reader) (int32, error) -func EncodeVarLong(w io.Writer, v int64) error -func DecodeVarLong(r io.Reader) (int64, error) - -// String encoding (varint-prefixed UTF-8). -func EncodeString(w io.Writer, s string) error -func DecodeString(r io.Reader) (string, error) - -// Packet direction helpers. -func (a Actor) SendDirection() Direction -func (a Actor) ReceiveDirection() Direction -func (d Direction) Opposite() Direction -func (d Direction) String() string -``` - -### `mc/data` — Minecraft Version Data - -```go -// Version lookup. -func LookUpVersionByName(name string) (VersionInfo, error) -func LookUpVersionByProtocolVersion(version int32) (VersionInfo, error) -func LookUpVersionByDataVersion(version int32) (VersionInfo, error) -func MustLookupProtocolVersion(version string) int32 - -// Block data. -func BlocksForVersion(v string) []*Block -func LookupBlockByStateId(version string, stateId int32) (*Block, bool) -func LookupBlockByName(version string, name string) (*Block, bool) -func StateIdFromBlocKAndStateMap(version string, name string, stateMap map[string]string) (int32, error) -func FromBlockState(version string, stateId int32) (*Block, map[string]string, error) - -// Biome data. -func BiomesForVersion(v string) []*Biome -func LookupBiomeById(version string, id int32) (*Biome, bool) -func LookupBiomeByName(version string, name string) (*Biome, bool) - -// Item data. -func ItemsForVersion(v string) []*Item -func LookupItemById(version string, id int32) (*Item, bool) -func LookupItemByName(version string, name string) (*Item, bool) - -// Noise / generation data. -func LoadMultiNoise(version string) (*MultiNoiseData, error) - -// Registry data. -func LoadRegistry(name string) (*nbt2.Tag, error) - -// Generic JSON loading (relative to minecraft-data directory). -func LoadJson(path string, data any) error -func LoadVersionedJson(version, dataName string, data any) error -``` - -### `mc/crypto` — Encryption Helpers - -```go -func AuthDigest(elems ...[]byte) string -func NewCFB8Decrypt(c cipher.Block, iv []byte) *CFB8 -func NewCFB8Encrypt(c cipher.Block, iv []byte) *CFB8 -func FromOfflinePlayer(displayName string) uuid.UUID -``` - -### `mc/text` — Text Components - -```go -// Pretty wraps a plain string in a Component. -func Pretty(s string) *Component - -// PrettyF wraps a formatted string in a Component. -func PrettyF(format string, args ...any) *Component - -// PrettyPlaceholders replaces placeholders in a translation string. -func PrettyPlaceholders(format string, placeholders map[string]any) *Component -``` - -### `mc/nbt` — Named Binary Tag I/O - -```go -func ReadFile(r io.Reader, data any) error -func ReadUnstructuredFile(r io.Reader) (*Tag, error) -func ReadUnstructuredFilePath(path string) (*Tag, error) -func WriteUnstructuredFile(w io.Writer, n *Tag) error -func WriteUnstructuredFilePath(path string, n *Tag) error -func PrintSNBTAny(t any, w io.Writer) error -``` - -### `mc/level` — World & Chunk Data - -```go -// Chunk reading. -func ReadChunkFromChunkPacketData(r io.Reader, version string, worldHeight int) (*Chunk, error) - -// Section / storage format helpers. -func MakeBiomeFormat(version string) StorageFormat -func MakeBlockFormat(version string) StorageFormat -func ReadSectionStorage(r io.Reader, format StorageFormat) (*Storage, error) -func WriteSectionStorage(w io.Writer, s *Storage) error -func SectionDecodePacket(r io.Reader, version string) (Section, error) -func SectionEncodePacket(w io.Writer, s Section) error -func SectionEquals(a, b Section) bool -func CountStorage(s *Storage, v int32) int -``` - -#### `mc/level/anvil` — Anvil Region Format - -```go -func ReadLevelData(r io.Reader) (Level, error) -func ReadChunkHeader(r *os.File) (*ChunkHeader, error) -``` - -#### `mc/level/structure` / `mc/level/litematic` - -```go -func Load(fileName string) (*Structure, error) // structure -func LoadFromPath(path string) (*Structure, error) // litematic -func LoadFromFile(r io.Reader) (*Structure, error) // litematic -``` - -### `mc/gen` — World Generation - -```go -func XoroshiroFromSeed(seed int64) Xoroshiro -func XoroshiroFromString(s string) Xoroshiro -func PerlinNoiseFromXoroshiro(x Xoroshiro) PerlinNoise -func OctaveFromXoroshiro(x *Xoroshiro, amplitudes []float64, omin, lenn, nmax int) Octave -func DoublePerlinFromXoroshiro(x *Xoroshiro, amplitudes []float64, omin, lenn, nmax int) DoublePerlin -func BiomeNoiseFromXoroshiro(x Xoroshiro, large bool) BiomeNoise -func CreateBiomeDepthSpline() Spline -``` - -### `mc/mapstructure` — Tag-Based Decoder - -```go -// Decode populates a struct from a map using struct tags. -func Decode(data any, v any) error - -// NewFormat creates a custom format with options. -func NewFormat(name string, opts ...Option) *Format - -// Options. -func WithRequireAll() Option -func WithErrorOnExtra() Option -func WithTrySnakeCase() Option -func WithTryLowCase() Option -func WithTypeCodec(decodeFn, encodeFn any) Option -``` - -### `mc/util` — Miscellaneous Helpers - -```go -func CamelCase(s string) string -func SnakeCase(s string) string -func FirstLetterLower(s string) string -func FloorDiv(a, b int32) int32 -func BpeByNum(x float64) uint8 -func Must(err error) -``` - -## Handler Signature - -All event handlers must match: -```go -func(typedPacket) error -``` -The argument must implement `proto_base.EncodeDecodeAble` (has `Decode`/`Encode` methods). Return `event.HandlerDoneErr{}` to stop the loop cleanly, or any other error to close the connection. - -## Quick Recipes - -### Server list ping -```go -// Programmatic -resp, err := client.Status(context.Background(), "mc.hypixel.net:25565") - -// With raw JSON -s, err := client.StatusRaw(ctx, addr) -_ = json.Unmarshal([]byte(s.Status), &resp) -``` - -### Start a server -```go -server.StartServerWithOnConn(":25565", func(c *proto.Conn) error { - c.RegisterCriticalUntilLatest(handleHandshake, handleLogin) - return c.StartConn() -}) -``` - -### Construct a packet without a connection -```go -// Encode to wire format -b, err := proto.SimplePacketToBytesLenPrefix(&v1_21_8.StatusToServerPacketPing{Time: 1234}) - -// Decode from wire format -packet, err := proto.SimpleBytesToPacket(bytes, 772, proto_base.ToClient, proto_base.Status) -info, ok := packet.(*v1_21_8.StatusToClientPacketServerInfo) -``` - -### Look up version data -```go -v, _ := data.LookUpVersionByName("1.21.8") -block, _ := data.LookupBlockByName("1.21.8", "minecraft:stone") -protocol := data.MustLookupProtocolVersion("1.21.8") -``` - -### Read NBT -```go -tag, _ := nbt.ReadUnstructuredFile(file) -data.Level level -nbt.ReadFile(file, &data) -``` diff --git a/mc/api/api.go b/mc/api/api.go index 14aef56..7bd5368 100644 --- a/mc/api/api.go +++ b/mc/api/api.go @@ -289,6 +289,9 @@ func (a *Account) CheckGiftCodeValidity(giftCode string) (valid bool, err error) return } +// NewAccountFromYGG creates an Account by authenticating the YGG token against +// Mojang's API. It makes an HTTP call to fetch the player profile and returns +// an Account with name and UUID populated, or an error if authentication fails. func NewAccountFromYGG(ygg string) (a *Account, err error) { a = &Account{ Ygg: ygg, @@ -303,6 +306,8 @@ func NewAccountFromYGG(ygg string) (a *Account, err error) { return } +// NewAccount creates an Account from explicit name, UUID, and YGG token without +// making any HTTP calls. The account uses Mojang's default API servers. func NewAccount(name string, uuid uuid.UUID, ygg string) (a *Account) { return &Account{ Name: name, diff --git a/mc/api/offline.go b/mc/api/offline.go index 6f6de9f..d92aeb3 100644 --- a/mc/api/offline.go +++ b/mc/api/offline.go @@ -4,6 +4,8 @@ import ( "github.com/admin-else/strom/mc/crypto" ) +// NewOfflineAccount creates an offline-mode Account with a UUID derived from the +// player name. No HTTP calls are made; the UUID is computed locally. func NewOfflineAccount(name string) *Account { return &Account{ Name: name, diff --git a/mc/api/unauthenticated.go b/mc/api/unauthenticated.go index f307327..dafc2a3 100644 --- a/mc/api/unauthenticated.go +++ b/mc/api/unauthenticated.go @@ -9,6 +9,8 @@ import ( "strings" ) +// BlockedServers fetches the list of SHA-1 hashes of blocked server IPs from +// Mojang's session server. The returned strings are hex-encoded hashes. func BlockedServers() (s []string, err error) { resp, err := http.Get("https://sessionserver.mojang.com/blockedservers") if err != nil { @@ -36,6 +38,9 @@ type ProfileResp struct { Properties []ProfileProperty `json:"properties"` } +// HasJoined checks with Mojang's session server whether a player has joined a +// given server. name is the player name, server is the server hash, and ip is +// the optional player IP address. Returns the player's profile on success. func HasJoined(name, server, ip string) (p ProfileResp, err error) { url := "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + name + "&serverId=" + server if ip != "" { @@ -62,6 +67,8 @@ type MojangPublicKeysResp struct { AuthenticationKeys []string `json:"authenticationKeys"` } +// MojangPublicKeys fetches Mojang's public keys for profile property signing and +// player certificate verification from the session server. func MojangPublicKeys() (keys MojangPublicKeysResp, err error) { resp, err := http.Get("https://sessionserver.mojang.com/session/minecraft/profile/public_key_by_token") if err != nil { @@ -72,6 +79,8 @@ func MojangPublicKeys() (keys MojangPublicKeysResp, err error) { return } +// Profile fetches a player's profile by UUID from Mojang's session server. +// If unsigned is false, signed profile properties are included in the response. func Profile(id string, unsinged bool) (p ProfileResp, err error) { url := "https://sessionserver.mojang.com/session/minecraft/profile/" + id if !unsinged { @@ -91,6 +100,8 @@ type IdName struct { Name string `json:"name"` } +// NameToId resolves a Minecraft player name to a UUID and canonical name by +// making an HTTP GET to Mojang's API. func NameToId(name string) (idName IdName, err error) { // may also use these: //https://api.mojang.com/users/profiles/minecraft/ @@ -105,6 +116,8 @@ func NameToId(name string) (idName IdName, err error) { return } +// NameToIdBulk resolves multiple Minecraft player names to UUIDs and canonical +// names in a single HTTP POST to Mojang's API. func NameToIdBulk(names []string) (idNames []IdName, err error) { b := bytes.NewBuffer(nil) err = json.NewEncoder(b).Encode(names) diff --git a/mc/bot/keepalive/mod.go b/mc/bot/keepalive/mod.go index b49578d..e0e0ed2 100644 --- a/mc/bot/keepalive/mod.go +++ b/mc/bot/keepalive/mod.go @@ -13,6 +13,7 @@ func (m *mod) onKeepAlive(p *v1_21_8.PlayToClientPacketKeepAlive) (err error) { return m.Send(&v1_21_8.PlayToServerPacketKeepAlive{KeepAliveId: p.KeepAliveId}) } +// Start begins responding to keep-alive packets on the connection. func Start(c *proto.Conn) { m := &mod{Conn: c} m.RegisterUntilLatest(m.onKeepAlive) diff --git a/mc/client/config_ignorer.go b/mc/client/config_ignorer.go index 44211b6..0922524 100644 --- a/mc/client/config_ignorer.go +++ b/mc/client/config_ignorer.go @@ -47,6 +47,7 @@ func (c *ConfigIgnorer) OnKeepAlive(packet *v1_21_8.ConfigurationToClientPacketK return } +// IgnoreConfig acknowledges configuration packets on the connection and transitions it to the Play state once configuration is complete. func IgnoreConfig(c *proto.Conn) (err error) { ci := &ConfigIgnorer{c} ci.RegisterCritical(ci.Default) diff --git a/mc/client/handshake_packet.go b/mc/client/handshake_packet.go index eafccb5..1b3ee87 100644 --- a/mc/client/handshake_packet.go +++ b/mc/client/handshake_packet.go @@ -9,6 +9,7 @@ import ( "github.com/admin-else/strom/mc/proto_generated/v1_21_8" ) +// MakeHandshakePacket creates a handshake packet using the connection's remote address as the server host. func MakeHandshakePacket(c *proto.Conn, nextState proto_base.State) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) { return MakeHandshakePacketAddr(c, nextState, c.RemoteAddr().String()) } diff --git a/mc/client/login.go b/mc/client/login.go index ea23ce1..cbdd667 100644 --- a/mc/client/login.go +++ b/mc/client/login.go @@ -134,6 +134,7 @@ func (s *LoginClient) OnStart() (err error) { return } +// LoginRaw performs the login handshake on an already-established connection without DNS resolution or configuration handling. func LoginRaw(c *proto.Conn, account *api.Account) (err error) { lc := &LoginClient{ Conn: c, @@ -166,18 +167,21 @@ func LoginRaw(c *proto.Conn, account *api.Account) (err error) { return } +// WithoutDns returns a login setting that disables SRV DNS resolution during login. func WithoutDns() func(ls *loginSettings) { return func(ls *loginSettings) { ls.NoDns = true } } +// WithContext returns a login setting that sets the context used for the connection dial and DNS resolution. func WithContext(ctx context.Context) func(ls *loginSettings) { return func(ls *loginSettings) { ls.Ctx = ctx } } +// WithVersion returns a login setting that forces a specific Minecraft version string for the connection. func WithVersion(version string) func(ls *loginSettings) { return func(ls *loginSettings) { ls.Version = version @@ -191,6 +195,7 @@ type loginSettings struct { IgnoreConfig bool } +// Login resolves the address with SRV DNS, connects, performs the login handshake, and handles the configuration phase, returning an authenticated connection ready for play. func Login(connectTo string, account *api.Account, settings ...func(ls *loginSettings)) (c *proto.Conn, err error) { ls := &loginSettings{} diff --git a/mc/crypto/cfb8.go b/mc/crypto/cfb8.go index aef0ad0..f648643 100644 --- a/mc/crypto/cfb8.go +++ b/mc/crypto/cfb8.go @@ -41,10 +41,12 @@ type CFB8 struct { mutex sync.Mutex } +// NewCFB8Decrypt creates a new CFB8 stream cipher in decrypt mode. func NewCFB8Decrypt(c cipher.Block, iv []byte) *CFB8 { return newCFB8(c, iv, true) } +// NewCFB8Encrypt creates a new CFB8 stream cipher in encrypt mode. func NewCFB8Encrypt(c cipher.Block, iv []byte) *CFB8 { return newCFB8(c, iv, false) } diff --git a/mc/crypto/offline.go b/mc/crypto/offline.go index b074c19..0c5bd41 100644 --- a/mc/crypto/offline.go +++ b/mc/crypto/offline.go @@ -21,6 +21,7 @@ func uuidV3(space []byte, data []byte) uuid.UUID { return u } +// FromOfflinePlayer generates a UUID for an offline-mode (cracked) player from their display name. func FromOfflinePlayer(displayName string) uuid.UUID { return uuidV3([]byte("OfflinePlayer:"), []byte(displayName)) } diff --git a/mc/data/biomes.go b/mc/data/biomes.go index 64af1c1..5f28ff6 100644 --- a/mc/data/biomes.go +++ b/mc/data/biomes.go @@ -28,6 +28,7 @@ type Biome struct { var BiomesCache = make(map[string][]*Biome) +// BiomesForVersion returns the biome definitions for the given Minecraft version, caching them after the first load. func BiomesForVersion(v string) (ret []*Biome) { var ok bool if ret, ok = BiomesCache[v]; ok { @@ -39,6 +40,7 @@ func BiomesForVersion(v string) (ret []*Biome) { return b } +// LookupBiomeById returns the biome with the given numeric ID in the given version. func LookupBiomeById(version string, id int32) (biome *Biome, ok bool) { for _, b := range BiomesForVersion(version) { if b.Id == id { @@ -52,6 +54,7 @@ func LookupBiomeById(version string, id int32) (biome *Biome, ok bool) { var UnknownBiomeNameErr = errors.New("unknown biome Name") +// LookupBiomeByName returns the biome with the given name in the given version. func LookupBiomeByName(version string, name string) (biome *Biome, ok bool) { for _, b := range BiomesForVersion(version) { if b.Name == name { diff --git a/mc/data/blocks.go b/mc/data/blocks.go index 0ddf88c..59ce040 100644 --- a/mc/data/blocks.go +++ b/mc/data/blocks.go @@ -59,6 +59,7 @@ type BlockState struct { var BlocksCache = make(map[string][]*Block) var BlocksCacheIdMap = make(map[string]map[int32]*Block) +// BlocksForVersion returns the block definitions for the given Minecraft version, caching them after the first load. func BlocksForVersion(v string) (ret []*Block) { var ok bool if ret, ok = BlocksCache[v]; ok { @@ -84,6 +85,7 @@ func BlocksForVersion(v string) (ret []*Block) { return b } +// LookupBlockByStateId returns the block for the given global palette state ID in the given version. func LookupBlockByStateId(version string, stateId int32) (block *Block, ok bool) { idmap, found := BlocksCacheIdMap[version] if !found { @@ -101,6 +103,7 @@ func LookupBlockByStateId(version string, stateId int32) (block *Block, ok bool) var UnknownBlockNameErr = errors.New("unknown block Name") +// LookupBlockByName returns the block with the given name in the given version. func LookupBlockByName(version string, name string) (block *Block, ok bool) { for _, b := range BlocksForVersion(version) { if b.Name == name { @@ -111,6 +114,7 @@ func LookupBlockByName(version string, name string) (block *Block, ok bool) { return } +// StateIdFromBlocKAndStateMap returns the global palette state ID for the given block name and state property map in the given version. func StateIdFromBlocKAndStateMap(version string, name string, stateMap map[string]string) (stateId int32, err error) { b, ok := LookupBlockByName(version, name) if !ok { @@ -135,6 +139,7 @@ var BadBlockStateTypeErr = errors.New("bad block state type") var BadEnumErr = errors.New("bad enum") var SupplyAllStatesErr = errors.New("supply all states") +// FromBlockState converts a global palette state ID back into a block definition and a map of state properties. func FromBlockState(version string, stateId int32) (b *Block, stateData map[string]string, err error) { b, ok := LookupBlockByStateId(version, stateId) if !ok { diff --git a/mc/data/data.go b/mc/data/data.go index 276384c..12301c9 100644 --- a/mc/data/data.go +++ b/mc/data/data.go @@ -12,6 +12,7 @@ import ( //go:embed minecraft-data/pc minecraft-data/dataPaths.json var MinecraftData embed.FS +// LoadJson reads a JSON file from the embedded minecraft-data FS and unmarshals it into data. func LoadJson(path string, data any) (err error) { b, err := MinecraftData.ReadFile(path) if err != nil { @@ -21,6 +22,7 @@ func LoadJson(path string, data any) (err error) { return } +// LoadVersionedJson loads a version-specific JSON data file from the embedded minecraft-data FS and unmarshals it into data. func LoadVersionedJson(version, dataName string, data any) (err error) { b, err := MinecraftData.ReadFile("minecraft-data/" + Paths.Data[version][dataName] + "/" + dataName + ".json") if err != nil { diff --git a/mc/data/item.go b/mc/data/item.go index 11e58bd..8ad5a3d 100644 --- a/mc/data/item.go +++ b/mc/data/item.go @@ -19,6 +19,7 @@ type Item struct { var ItemsCache = make(map[string][]*Item) var ItemsCacheMutex = &sync.RWMutex{} +// ItemsForVersion returns the item definitions for the given Minecraft version, caching them after the first load. func ItemsForVersion(v string) (ret []*Item) { var ok bool ItemsCacheMutex.RLock() @@ -35,6 +36,7 @@ func ItemsForVersion(v string) (ret []*Item) { return b } +// LookupItemById returns the item with the given numeric ID in the given version. func LookupItemById(version string, id int32) (item *Item, ok bool) { for _, i := range ItemsForVersion(version) { if i.Id == id { @@ -46,6 +48,7 @@ func LookupItemById(version string, id int32) (item *Item, ok bool) { return } +// LookupItemByName returns the item with the given name in the given version. func LookupItemByName(version string, name string) (item *Item, ok bool) { for _, i := range ItemsForVersion(version) { if i.Name == name { diff --git a/mc/data/protocol_version.go b/mc/data/protocol_version.go index 2bc930d..b0a8000 100644 --- a/mc/data/protocol_version.go +++ b/mc/data/protocol_version.go @@ -20,6 +20,7 @@ type VersionInfo struct { var ProtocolVersions []VersionInfo +// LookUpVersionByName returns the VersionInfo for the given Minecraft version name (e.g. "1.21.11"). func LookUpVersionByName(name string) (ret VersionInfo, err error) { for _, v := range ProtocolVersions { if v.MinecraftVersion == name { @@ -31,6 +32,7 @@ func LookUpVersionByName(name string) (ret VersionInfo, err error) { return } +// LookUpVersionByProtocolVersion returns the VersionInfo for the given protocol version number. func LookUpVersionByProtocolVersion(version int32) (ret VersionInfo, err error) { for _, v := range ProtocolVersions { if v.Version == version { @@ -42,6 +44,7 @@ func LookUpVersionByProtocolVersion(version int32) (ret VersionInfo, err error) return } +// LookUpVersionByDataVersion returns the VersionInfo for the given Minecraft data version number. func LookUpVersionByDataVersion(version int32) (ret VersionInfo, err error) { for _, v := range ProtocolVersions { if v.DataVersion == version { diff --git a/mc/data/registry.go b/mc/data/registry.go index 6e9b893..2b255fd 100644 --- a/mc/data/registry.go +++ b/mc/data/registry.go @@ -11,6 +11,7 @@ import ( //go:embed registry/* var Registry embed.FS +// LoadRegistry loads a registry NBT file by name from the embedded registry data. func LoadRegistry(name string) (n *nbt2.Tag, err error) { r, err := Registry.Open("registry/" + name + ".nbt") if err != nil { diff --git a/mc/event/event_bus.go b/mc/event/event_bus.go index ca7a16a..ad0bc04 100644 --- a/mc/event/event_bus.go +++ b/mc/event/event_bus.go @@ -69,6 +69,8 @@ var WhileClosingErr = errors.New("error while closing") var ContextDoneErr = errors.New("context done") var RecvNotOkErr = errors.New("recv not ok") +// ValidateHandler validates that h is a function taking a single argument and returning an error. +// It panics if the handler does not match the expected signature. func ValidateHandler(h any) (eventType reflect.Type, hv reflect.Value) { hv = reflect.ValueOf(h) if hv.Kind() != reflect.Func { @@ -235,6 +237,7 @@ func (l *Loop) StartLoop() (err error) { return } +// NewLoop creates a new Loop with a background context. func NewLoop() (l *Loop) { return NewLoopCtx(context.Background()) } diff --git a/mc/gen/biome_noise.go b/mc/gen/biome_noise.go index 753b279..e6c877a 100644 --- a/mc/gen/biome_noise.go +++ b/mc/gen/biome_noise.go @@ -48,6 +48,7 @@ type BiomeNoise struct { Temperature, Humidity, ContinentalNess, Erosion, Shift, Weirdness DoublePerlin } +// BiomeNoiseFromXoroshiro creates a BiomeNoise from the given Xoroshiro source. func BiomeNoiseFromXoroshiro(x Xoroshiro, large bool) (v BiomeNoise) { x = x.Split() diff --git a/mc/gen/double.go b/mc/gen/double.go index 8c21bda..90ef4d3 100644 --- a/mc/gen/double.go +++ b/mc/gen/double.go @@ -6,6 +6,7 @@ type DoublePerlin struct { B Octave } +// DoublePerlinFromXoroshiro creates a DoublePerlin from the given Xoroshiro source. func DoublePerlinFromXoroshiro(x *Xoroshiro, amplitudes []float64, omin int, lenn int, nmax int) (d DoublePerlin) { na := -1 nb := -1 diff --git a/mc/gen/noise.go b/mc/gen/noise.go index 417f969..ccc5fc6 100644 --- a/mc/gen/noise.go +++ b/mc/gen/noise.go @@ -116,6 +116,7 @@ func (n PerlinNoise) Sample(x, y, z float64, yamp, ymin float64) (v float64) { return lerp(t3, l1, l5) } +// PerlinNoiseFromXoroshiro initializes a PerlinNoise from the given Xoroshiro source. func PerlinNoiseFromXoroshiro(x Xoroshiro) (p PerlinNoise) { p.xoffset = x.NextFloat64() * 256.0 p.yoffset = x.NextFloat64() * 256.0 diff --git a/mc/gen/octave.go b/mc/gen/octave.go index 867708e..9fd06c3 100644 --- a/mc/gen/octave.go +++ b/mc/gen/octave.go @@ -28,6 +28,7 @@ var octaveLacunaInits = []float64{1, .5, .25, 1. / 8, 1. / 16, 1. / 32, 1. / 64, var octavePersistInits = []float64{0, 1, 2. / 3, 4. / 7, 8. / 15, 16. / 31, 32. / 63, 64. / 127, 128. / 255, 256. / 511, 512. / 1023, 1024. / 2047, 2048. / 4095, 4096. / 8191, 8192. / 16383, 16384. / 32767, 32768. / 65535} +// OctaveFromXoroshiro creates an Octave (multi-octave Perlin noise) from the given Xoroshiro source. func OctaveFromXoroshiro(x *Xoroshiro, amplitudes []float64, omin int, lenn int, nmax int) (o Octave) { if (-omin < 0 || -omin >= len(octaveLacunaInits)) || (lenn < 0 || lenn >= len(octavePersistInits)) { panic("octave initialization out of range") diff --git a/mc/gen/spline.go b/mc/gen/spline.go index 4fbf357..6409a64 100644 --- a/mc/gen/spline.go +++ b/mc/gen/spline.go @@ -183,6 +183,7 @@ func landSpline(f, g, h, i, j, k float32, bl bool) Spline { return s } +// CreateBiomeDepthSpline builds the Minecraft biome depth spline used for terrain generation. func CreateBiomeDepthSpline() Spline { sp1 := landSpline(-0.15, 0.00, 0.0, 0.1, 0.00, -0.03, false) sp2 := landSpline(-0.10, 0.03, 0.1, 0.1, 0.01, -0.03, false) diff --git a/mc/gen/xoroshiro.go b/mc/gen/xoroshiro.go index 67527c9..48856e6 100644 --- a/mc/gen/xoroshiro.go +++ b/mc/gen/xoroshiro.go @@ -42,6 +42,7 @@ func (x *Xoroshiro) NextFloat64() float64 { return float64(x.Next()>>(64-53)) * 1.1102230246251565e-16 } +// XoroshiroFromString creates an Xoroshiro from an MD5 hash of the given string. func XoroshiroFromString(s string) (x Xoroshiro) { sum := md5.Sum([]byte(s)) x.L = binary.BigEndian.Uint64(sum[:8]) @@ -49,6 +50,7 @@ func XoroshiroFromString(s string) (x Xoroshiro) { return } +// XoroshiroFromSeed creates an Xoroshiro from an int64 seed. func XoroshiroFromSeed(seed int64) (x Xoroshiro) { x.L = uint64(seed) ^ 0x6a09e667f3bcc909 x.H = x.L + 0x9e3779b97f4a7c15 diff --git a/mc/level/anvil/level.go b/mc/level/anvil/level.go index dd738f8..eca2dbf 100644 --- a/mc/level/anvil/level.go +++ b/mc/level/anvil/level.go @@ -174,6 +174,7 @@ type Attribute struct { Base float64 } +// ReadLevelData reads and decodes the level.dat NBT file from a reader. func ReadLevelData(r io.Reader) (l Level, err error) { var lDataWrapped struct{ Data Level } err = nbt.ReadFile(r, &lDataWrapped) diff --git a/mc/level/anvil/mca.go b/mc/level/anvil/mca.go index e078709..873ded1 100644 --- a/mc/level/anvil/mca.go +++ b/mc/level/anvil/mca.go @@ -52,6 +52,7 @@ type ChunkHeader struct { Chunk, Time [ChunksRegionFile]int32 } +// ReadChunkHeader reads the chunk header from an MCA region file. func ReadChunkHeader(r *os.File) (ch *ChunkHeader, err error) { var h ChunkHeader err = binary.Read(r, binary.BigEndian, &h) diff --git a/mc/level/chunk.go b/mc/level/chunk.go index 076614b..cdc099a 100644 --- a/mc/level/chunk.go +++ b/mc/level/chunk.go @@ -10,6 +10,7 @@ type Chunk struct { Version string } +// ReadChunkFromChunkPacketData decodes chunk packet data from a reader into a Chunk. func ReadChunkFromChunkPacketData(r io.Reader, version string, worldHeight int) (c *Chunk, err error) { c = &Chunk{} c.Version = version diff --git a/mc/level/litematic/main.go b/mc/level/litematic/main.go index effcca6..8c3f2f9 100644 --- a/mc/level/litematic/main.go +++ b/mc/level/litematic/main.go @@ -94,6 +94,7 @@ type Structure struct { Version string } +// LoadFromPath loads a litematic file from disk. func LoadFromPath(path string) (s *Structure, err error) { f, err := os.Open(path) if err != nil { @@ -104,6 +105,7 @@ func LoadFromPath(path string) (s *Structure, err error) { return } +// LoadFromFile reads and parses a litematic file from a reader. func LoadFromFile(r io.Reader) (s *Structure, err error) { n, err := nbt.ReadUnstructuredFile(r) if err != nil { @@ -112,6 +114,7 @@ func LoadFromFile(r io.Reader) (s *Structure, err error) { return Load(n.Value) } +// Load decodes raw NBT data into a litematic Structure. func Load(v any) (s *Structure, err error) { s = &Structure{} err = mapstructure.Decode(v, &s.File) diff --git a/mc/level/section.go b/mc/level/section.go index 335edb4..afc8b7a 100644 --- a/mc/level/section.go +++ b/mc/level/section.go @@ -30,6 +30,7 @@ type Section struct { Blocks, Biomes *Storage } +// MakeBiomeFormat returns the StorageFormat for biome data at the given version. func MakeBiomeFormat(version string) StorageFormat { directBpe := uint8(math.Ceil(math.Log2(float64(len(data2.BiomesForVersion(version)))))) return StorageFormat{ @@ -39,6 +40,7 @@ func MakeBiomeFormat(version string) StorageFormat { } } +// MakeBlockFormat returns the StorageFormat for block data at the given version. func MakeBlockFormat(version string) StorageFormat { directBpe := uint8(math.Ceil(math.Log2(float64(len(data2.BlocksForVersion(version)))))) return StorageFormat{ @@ -51,6 +53,7 @@ func MakeBlockFormat(version string) StorageFormat { var BadPaletteLenErr = errors.New("bad palette length") +// ReadSectionStorage reads a packed section storage from a reader. func ReadSectionStorage(r io.Reader, format StorageFormat) (s *Storage, err error) { var bpe uint8 err = binary.Read(r, binary.BigEndian, &bpe) @@ -97,6 +100,7 @@ func ReadSectionStorage(r io.Reader, format StorageFormat) (s *Storage, err erro return format.ImportFromReader(r, bpe, pallette) } +// WriteSectionStorage writes a packed section storage to a writer. func WriteSectionStorage(w io.Writer, s *Storage) (err error) { err = binary.Write(w, binary.BigEndian, s.bpe) if err != nil { @@ -122,6 +126,7 @@ func WriteSectionStorage(w io.Writer, s *Storage) (err error) { return binary.Write(w, binary.BigEndian, s.data) } +// SectionDecodePacket decodes a chunk section from the Minecraft packet wire format. func SectionDecodePacket(r io.Reader, version string) (s Section, err error) { s = Section{} err = binary.Read(r, binary.BigEndian, &s.BlockCount) @@ -136,6 +141,7 @@ func SectionDecodePacket(r io.Reader, version string) (s Section, err error) { return } +// SectionEncodePacket encodes a chunk section to the Minecraft packet wire format. func SectionEncodePacket(w io.Writer, s Section) (err error) { err = binary.Write(w, binary.BigEndian, s.BlockCount) if err != nil { @@ -149,6 +155,7 @@ func SectionEncodePacket(w io.Writer, s Section) (err error) { return } +// SectionEquals returns whether two sections are equal. func SectionEquals(a, b Section) bool { if a.BlockCount != b.BlockCount { return false diff --git a/mc/level/storage.go b/mc/level/storage.go index 596d28c..9e8a0c7 100644 --- a/mc/level/storage.go +++ b/mc/level/storage.go @@ -250,6 +250,7 @@ func (r *Storage) Equals(other *Storage) bool { return true } +// CountStorage counts the number of elements in the storage that match the given value. func CountStorage(s *Storage, v int32) (a int) { for i := range s.format.Len { if w, _ := s.Get(i); w == v { diff --git a/mc/level/structure/structure.go b/mc/level/structure/structure.go index 71a83c4..0331cff 100644 --- a/mc/level/structure/structure.go +++ b/mc/level/structure/structure.go @@ -5,6 +5,7 @@ import ( "github.com/go-viper/mapstructure/v2" ) +// Load reads a structure NBT file and returns the decoded Structure. func Load(fileName string) (s *Structure, err error) { n, err := nbt.ReadUnstructuredFilePath(fileName) if err != nil { diff --git a/mc/mapstructure/default.go b/mc/mapstructure/default.go index b9b7e73..3b4022f 100644 --- a/mc/mapstructure/default.go +++ b/mc/mapstructure/default.go @@ -2,6 +2,7 @@ package mapstructure var DefaultFormat = NewFormat("mapstructure", WithRequireAll(), WithTrySnakeCase(), WithTryLowCase()) +// Decode is a stub placeholder. Use Format.Decode instead. func Decode(data any, v any) error { return nil } diff --git a/mc/mapstructure/format.go b/mc/mapstructure/format.go index dd5881a..02d333e 100644 --- a/mc/mapstructure/format.go +++ b/mc/mapstructure/format.go @@ -25,6 +25,7 @@ type Format struct { encoders map[reflect.Type]func(any, string) (any, error) } +// NewFormat creates a new Format with the given name and options. func NewFormat(name string, opts ...Option) *Format { f := &Format{ Name: name, @@ -39,30 +40,35 @@ func NewFormat(name string, opts ...Option) *Format { type Option func(*Format) +// WithRequireAll returns an Option that requires all struct fields to be present in the map. func WithRequireAll() Option { return func(f *Format) { f.requireAll = true } } +// WithErrorOnExtra returns an Option that causes an error when the map contains unmatched keys. func WithErrorOnExtra() Option { return func(f *Format) { f.errorOnExtra = true } } +// WithTrySnakeCase returns an Option that tries to match struct fields using snake_case names. func WithTrySnakeCase() Option { return func(f *Format) { f.trySnakeCase = true } } +// WithTryLowCase returns an Option that tries to match struct fields using lowercase names. func WithTryLowCase() Option { return func(f *Format) { f.tryLowCase = true } } +// WithTypeCodec returns an Option that registers custom decode/encode functions for a type. func WithTypeCodec(decodeFn, encodeFn any) Option { dt := reflect.TypeOf(decodeFn) et := reflect.TypeOf(encodeFn) @@ -94,6 +100,7 @@ func WithTypeCodec(decodeFn, encodeFn any) Option { } } +// Decode populates target (a pointer to a struct) from data (a map[string]any). func (f *Format) Decode(data any, target any) error { v := reflect.ValueOf(target) if v.Kind() != reflect.Pointer || v.IsNil() { @@ -110,6 +117,7 @@ func (f *Format) Decode(data any, target any) error { return f.decode(m, v) } +// Encode converts source (a struct or pointer to a struct) into a map[string]any. func (f *Format) Encode(source any) (map[string]any, error) { v := reflect.ValueOf(source) if v.Kind() == reflect.Pointer { diff --git a/mc/nbt/file.go b/mc/nbt/file.go index 49a91d0..0780ef4 100644 --- a/mc/nbt/file.go +++ b/mc/nbt/file.go @@ -10,6 +10,7 @@ import ( var Format = mapstructure.NewFormat("nbt", mapstructure.WithRequireAll(), mapstructure.WithTrySnakeCase(), mapstructure.WithTryLowCase()) +// ReadFile reads a gzipped NBT file from r and decodes it into data using the package-level Format. func ReadFile(file io.Reader, data any) (err error) { n, err := ReadUnstructuredFile(file) if err != nil { @@ -18,6 +19,7 @@ func ReadFile(file io.Reader, data any) (err error) { return Format.Decode(n.Value, data) } +// WriteUnstructuredFilePath writes an NBT tag as a gzipped file to the given filesystem path. func WriteUnstructuredFilePath(path string, n *Tag) (err error) { f, err := os.Create(path) if err != nil { @@ -27,6 +29,7 @@ func WriteUnstructuredFilePath(path string, n *Tag) (err error) { return WriteUnstructuredFile(f, n) } +// WriteUnstructuredFile writes an NBT tag as a gzipped file to w. func WriteUnstructuredFile(file io.Writer, n *Tag) (err error) { gzw := gzip.NewWriter(file) defer gzw.Close() @@ -34,6 +37,7 @@ func WriteUnstructuredFile(file io.Writer, n *Tag) (err error) { return } +// ReadUnstructuredFile reads a gzipped NBT file from r and returns the root Tag. func ReadUnstructuredFile(file io.Reader) (n *Tag, err error) { r, err := gzip.NewReader(file) if err != nil { @@ -45,6 +49,7 @@ func ReadUnstructuredFile(file io.Reader) (n *Tag, err error) { return } +// ReadUnstructuredFilePath reads a gzipped NBT file from the given filesystem path and returns the root Tag. func ReadUnstructuredFilePath(path string) (n *Tag, err error) { f, err := os.Open(path) if err != nil { diff --git a/mc/proto/connection.go b/mc/proto/connection.go index 02321bb..46f92cc 100644 --- a/mc/proto/connection.go +++ b/mc/proto/connection.go @@ -73,6 +73,7 @@ type Conn struct { Log *slog.Logger } +// StartConn starts the connection event loop, registering the receive routine and login tick handler. func (c *Conn) StartConn() (err error) { c.RegisterEventSource(c.ActivateReceiveRoutine()) c.RegisterCritical(c.OnLoginTick) @@ -81,6 +82,7 @@ func (c *Conn) StartConn() (err error) { return } +// OnLoginTick handles a login tick by receiving a packet and firing it as an event. func (c *Conn) OnLoginTick(_ LoginTick) (err error) { packet, err := c.Receive() if err != nil { @@ -90,30 +92,35 @@ func (c *Conn) OnLoginTick(_ LoginTick) (err error) { return } +// SetState sets the connection's protocol state. func (c *Conn) SetState(state proto_base.State) { c.stateMutex.Lock() defer c.stateMutex.Unlock() c.state = state } +// State returns the current connection protocol state. func (c *Conn) State() proto_base.State { c.stateMutex.RLock() defer c.stateMutex.RUnlock() return c.state } +// SetAsyncPackets enables or disables asynchronous packet handling in the receive routine. func (c *Conn) SetAsyncPackets(asyncPackets bool) { c.asyncPacketsMutex.Lock() defer c.asyncPacketsMutex.Unlock() c.asyncPackets = asyncPackets } +// GetAsyncPackets returns whether asynchronous packet handling is enabled. func (c *Conn) GetAsyncPackets() bool { c.asyncPacketsMutex.RLock() defer c.asyncPacketsMutex.RUnlock() return c.asyncPackets } +// SetVersion sets the connection's Minecraft version by version name and looks up the corresponding protocol version. func (c *Conn) SetVersion(version string) (err error) { versionData, err := data.LookUpVersionByName(version) if err != nil { @@ -124,6 +131,7 @@ func (c *Conn) SetVersion(version string) (err error) { return } +// SetProtocolVersion sets the connection's version by protocol version number and looks up the corresponding version name. func (c *Conn) SetProtocolVersion(version int32) (err error) { versionData, err := data.LookUpVersionByProtocolVersion(version) if err != nil { @@ -202,6 +210,7 @@ func SimplePacketToBytes(packet proto_base.EncodeDecodeAble) (b []byte, err erro return } +// SimplePacketToBytesLenPrefix is like SimplePacketToBytes but also prepends the length-prefix framing. func SimplePacketToBytesLenPrefix(packet proto_base.EncodeDecodeAble) (b []byte, err error) { b, err = SimplePacketToBytes(packet) if err != nil { @@ -282,6 +291,7 @@ func (c *Conn) sendRegisteredPacket(packet proto_base.EncodeDecodeAble) (err err return c.SendRaw(packetBuff.Bytes()) } +// Send encodes and sends a packet to the remote peer, translating protocol versions if needed. func (c *Conn) Send(packet proto_base.EncodeDecodeAble) (err error) { switch packet := packet.(type) { case *UnCodablePacket: @@ -292,6 +302,7 @@ func (c *Conn) Send(packet proto_base.EncodeDecodeAble) (err error) { return } +// Receive reads and decodes a raw packet from the remote peer. func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) { if c.R == nil { return @@ -335,10 +346,12 @@ func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) { } return } +// NewConn creates a new Conn with default settings and a background context. func NewConn() (ret *Conn) { return NewConnCtx(context.Background()) } +// NewConnCtx creates a new Conn with the given context, setting the handshaking state and latest version. func NewConnCtx(ctx context.Context) (ret *Conn) { ret = &Conn{} ret.SetState(proto_base.Handshaking) diff --git a/mc/proto/packet_lookup.go b/mc/proto/packet_lookup.go index 22efb43..e8d68d0 100644 --- a/mc/proto/packet_lookup.go +++ b/mc/proto/packet_lookup.go @@ -22,6 +22,7 @@ func init() { } } +// LookUpTypeByPacketInfo looks up a packet's type information by direction, state, packet id, and protocol version. func LookUpTypeByPacketInfo(direction proto_base.Direction, state proto_base.State, pid, version int32) (p proto_base.PacketInfo, ok bool) { pP, ok := pidMap[PidEtc{direction, state, pid, version}] if !ok { @@ -39,6 +40,7 @@ func LookUpTypeByPacketInfo(direction proto_base.Direction, state proto_base.Sta return } +// LookUpTypeByPacketInfoAndCopyType looks up a packet and returns a newly allocated instance of its type. func LookUpTypeByPacketInfoAndCopyType(direction proto_base.Direction, state proto_base.State, pid, version int32) (p proto_base.EncodeDecodeAble, ok bool) { i, ok := LookUpTypeByPacketInfo(direction, state, pid, version) if !ok { @@ -48,6 +50,7 @@ func LookUpTypeByPacketInfoAndCopyType(direction proto_base.Direction, state pro return } +// LookupPacketInfoByType looks up a packet's type information by matching against the type of the given packet value. func LookupPacketInfoByType(packet proto_base.EncodeDecodeAble) (p proto_base.PacketInfo, ok bool) { undecodedPacket, isUndecoable := packet.(*UnCodablePacket) if isUndecoable { @@ -64,6 +67,7 @@ func LookupPacketInfoByType(packet proto_base.EncodeDecodeAble) (p proto_base.Pa return } +// LookupPacketInfoByTypeAndState looks up a packet's type information by type and state. func LookupPacketInfoByTypeAndState(packet proto_base.EncodeDecodeAble, state proto_base.State) (p proto_base.PacketInfo, ok bool) { for _, p := range proto_generated.Packets { if state == p.State && reflect.TypeOf(p.Type) == reflect.TypeOf(packet) { @@ -73,6 +77,7 @@ func LookupPacketInfoByTypeAndState(packet proto_base.EncodeDecodeAble, state pr return } +// LookupPacketInfoByNameProtocolVersionAndState looks up a packet's type information by name, protocol version, and state. func LookupPacketInfoByNameProtocolVersionAndState(name string, version int32, state proto_base.State) (p proto_base.PacketInfo, ok bool) { for _, p := range proto_generated.Packets { if state == p.State && p.Name == name && p.ProtocolVersion == version { @@ -82,6 +87,7 @@ func LookupPacketInfoByNameProtocolVersionAndState(name string, version int32, s return } +// LookupPacketInfoByNameProtocolVersionStateAndDirection looks up a packet's type information by name, protocol version, state, and direction. func LookupPacketInfoByNameProtocolVersionStateAndDirection(name string, version int32, state proto_base.State, direction proto_base.Direction) (p proto_base.PacketInfo, ok bool) { for _, p := range proto_generated.Packets { if state == p.State && p.Name == name && p.ProtocolVersion == version && p.Direction == direction { diff --git a/mc/proto/raw_conn.go b/mc/proto/raw_conn.go index 019e1e2..ea91b53 100644 --- a/mc/proto/raw_conn.go +++ b/mc/proto/raw_conn.go @@ -27,18 +27,21 @@ type RawConn struct { MaxPacketSize int32 } +// SetCompressionThreshold sets the packet compression threshold. Values below 0 disable compression. func (c *RawConn) SetCompressionThreshold(threshold int32) { c.CompressionThresholdMut.Lock() defer c.CompressionThresholdMut.Unlock() c.compressionThreshold = threshold } +// GetCompressionThreshold returns the current compression threshold. func (c *RawConn) GetCompressionThreshold() int32 { c.CompressionThresholdMut.RLock() defer c.CompressionThresholdMut.RUnlock() return c.compressionThreshold } +// SendRaw sends raw packet bytes over the connection, applying compression if the threshold is met. func (c *RawConn) SendRaw(rawPacketBytes []byte) (err error) { if c.MaxPacketSize > 0 && int32(len(rawPacketBytes)) > c.MaxPacketSize { err = errors.New("packet too large") @@ -90,6 +93,7 @@ func (c *RawConn) SendRaw(rawPacketBytes []byte) (err error) { return } +// ReceiveRaw reads and decompresses (if needed) a raw packet from the connection. func (c *RawConn) ReceiveRaw() (packetBytes []byte, err error) { rawPacketLen, err := proto_base.DecodeVarInt(c.R) if err != nil { @@ -142,6 +146,7 @@ func (c *RawConn) ReceiveRaw() (packetBytes []byte, err error) { return } +// SetSecret enables AES/CFB8 encryption on the connection using the shared secret. func (c *RawConn) SetSecret(sharedSecret []byte) (err error) { var b cipher.Block b, err = aes.NewCipher(sharedSecret) diff --git a/mc/proto/replay/reader.go b/mc/proto/replay/reader.go index 56d8e6e..3577d34 100644 --- a/mc/proto/replay/reader.go +++ b/mc/proto/replay/reader.go @@ -16,6 +16,7 @@ type Reader struct { Version int32 } +// NewReader creates a new replay Reader from an io.Reader. func NewReader(r io.Reader) *Reader { return &Reader{r: r} } diff --git a/mc/proto/replay/writer.go b/mc/proto/replay/writer.go index 3eb8817..b213315 100644 --- a/mc/proto/replay/writer.go +++ b/mc/proto/replay/writer.go @@ -13,6 +13,7 @@ type Writer struct { w io.Writer } +// NewWriter creates a new replay Writer from an io.Writer. func NewWriter(w io.Writer) *Writer { return &Writer{w: w} } diff --git a/mc/proto/smart_convert.go b/mc/proto/smart_convert.go index 5dfc673..2247bbb 100644 --- a/mc/proto/smart_convert.go +++ b/mc/proto/smart_convert.go @@ -11,6 +11,7 @@ import ( "github.com/admin-else/strom/mc/proto_generated" ) +// SmartConvertibleTo reports whether a value of type from can be recursively converted to type to. func SmartConvertibleTo(from, to reflect.Type) bool { switch from.Kind() { case reflect.Struct: @@ -36,6 +37,7 @@ func SmartConvertibleTo(from, to reflect.Type) bool { } } +// SmartConvert recursively converts a reflect.Value from one type to another compatible type. func SmartConvert(from reflect.Value, to reflect.Type) (ret reflect.Value) { switch from.Kind() { case reflect.Struct: diff --git a/mc/proto_base/base_types.go b/mc/proto_base/base_types.go index 89f9652..8b30c61 100644 --- a/mc/proto_base/base_types.go +++ b/mc/proto_base/base_types.go @@ -7,10 +7,12 @@ import ( "math" ) +// UnimplementedErr is returned when a code path is not yet implemented. var ( UnimplementedErr = errors.New("unimplemented") ) +// Direction indicates whether a packet is sent to the server or to the client. type Direction int const ( @@ -18,6 +20,7 @@ const ( ToClient ) +// String returns a human-readable representation of the Direction. func (d Direction) String() string { switch d { case ToServer: @@ -29,10 +32,12 @@ func (d Direction) String() string { } } +// Opposite returns the reverse Direction. func (d Direction) Opposite() Direction { return d ^ 1 } +// State represents the Minecraft connection state. type State int32 const ( @@ -43,6 +48,7 @@ const ( Play ) +// String returns a human-readable representation of the State. func (s State) String() string { switch s { case Handshaking: @@ -60,13 +66,16 @@ func (s State) String() string { } } +// Actor represents a participant in a Minecraft connection. type Actor int const ( - Servee Actor = iota // Servee is the one being served + // Servee is the one being served (e.g. the backend server in a proxy). + Servee Actor = iota Client ) +// String returns a human-readable representation of the Actor. func (a Actor) String() string { switch a { case Servee: @@ -78,6 +87,7 @@ func (a Actor) String() string { } } +// SendDirection returns the Direction in which this Actor sends packets. func (a Actor) SendDirection() Direction { switch a { case Servee: @@ -89,6 +99,7 @@ func (a Actor) SendDirection() Direction { } } +// ReceiveDirection returns the Direction from which this Actor receives packets. func (a Actor) ReceiveDirection() Direction { switch a { case Servee: @@ -101,13 +112,19 @@ func (a Actor) ReceiveDirection() Direction { } type ( + // ToDo is a placeholder type for unimplemented packet fields. ToDo struct{} + // RestBuffer represents the remaining unread bytes in a packet buffer. RestBuffer []byte ) +// ToDoError is returned when a ToDo placeholder is used. var ToDoError = errors.New("to do") + +// BadTypeError is returned when an unexpected type is encountered. var BadTypeError = errors.New("bad type") +// Encode writes the RestBuffer to w. func (b *RestBuffer) Encode(w io.Writer) (err error) { n, err := w.Write(*b) if n != len(*b) { @@ -116,11 +133,13 @@ func (b *RestBuffer) Encode(w io.Writer) (err error) { return } +// Decode reads all remaining bytes from r into the RestBuffer. func (b *RestBuffer) Decode(r io.Reader) (err error) { *b, err = io.ReadAll(r) return } +// EncodeString encodes a length-prefixed UTF-8 string in Minecraft varint format. func EncodeString(w io.Writer, s string) (err error) { err = EncodeVarInt(w, int32(len(s))) if err != nil { @@ -130,6 +149,7 @@ func EncodeString(w io.Writer, s string) (err error) { return } +// DecodeString decodes a length-prefixed UTF-8 string in Minecraft varint format. func DecodeString(r io.Reader) (ret string, err error) { l, err := DecodeVarInt(r) if err != nil { @@ -146,16 +166,19 @@ func DecodeString(r io.Reader) (ret string, err error) { return string(rawString), nil } +// Encode is a no-op stub that always returns ToDoError. func (t ToDo) Encode(_ io.Writer) (err error) { err = ToDoError return } +// Decode is a no-op stub that always returns ToDoError. func (t ToDo) Decode(_ io.Reader) (ret ToDo, err error) { err = ToDoError return } +// EncodeVarInt encodes a signed 32-bit integer in Minecraft varint format. func EncodeVarInt(w io.Writer, v int32) (err error) { uv := uint32(v) for { @@ -175,6 +198,7 @@ func EncodeVarInt(w io.Writer, v int32) (err error) { return nil } +// DecodeVarInt decodes a signed 32-bit integer in Minecraft varint format. func DecodeVarInt(r io.Reader) (ret int32, err error) { for i := range 5 { // 32/7 var b uint8 = 0 @@ -191,6 +215,7 @@ func DecodeVarInt(r io.Reader) (ret int32, err error) { return } +// EncodeVarLong encodes a signed 64-bit integer in Minecraft varlong format. func EncodeVarLong(w io.Writer, v int64) (err error) { uv := uint64(v) for { @@ -210,6 +235,7 @@ func EncodeVarLong(w io.Writer, v int64) (err error) { return nil } +// DecodeVarLong decodes a signed 64-bit integer in Minecraft varlong format. func DecodeVarLong(r io.Reader) (ret int64, err error) { for i := range 10 { // ceil(64/7) var b uint8 @@ -226,6 +252,7 @@ func DecodeVarLong(r io.Reader) (ret int64, err error) { return } +// ErroringIndex looks up key k in map m and returns an error if the key is not found. func ErroringIndex[K comparable, V any, M map[K]V](m M, i K) (v V, err error) { var ok bool v, ok = m[i] @@ -235,11 +262,14 @@ func ErroringIndex[K comparable, V any, M map[K]V](m M, i K) (v V, err error) { return } +// EncodeDecodeAble is implemented by types that can be encoded and decoded for +// Minecraft protocol packets. type EncodeDecodeAble interface { Encode(w io.Writer) (err error) Decode(r io.ReadSeeker) (err error) } +// PacketInfo holds metadata about a Minecraft protocol packet. type PacketInfo struct { Type EncodeDecodeAble Name string @@ -249,6 +279,7 @@ type PacketInfo struct { ProtocolVersion int32 } +// Bool2uint8 converts a bool to a uint8 (1 for true, 0 for false). func Bool2uint8(b bool) uint8 { if b { return 1 @@ -256,6 +287,7 @@ func Bool2uint8(b bool) uint8 { return 0 } +// Bool2uint16 converts a bool to a uint16 (1 for true, 0 for false). func Bool2uint16(b bool) uint16 { if b { return 1 @@ -263,6 +295,7 @@ func Bool2uint16(b bool) uint16 { return 0 } +// Bool2uint32 converts a bool to a uint32 (1 for true, 0 for false). func Bool2uint32(b bool) uint32 { if b { return 1 @@ -270,6 +303,7 @@ func Bool2uint32(b bool) uint32 { return 0 } +// Bool2uint64 converts a bool to a uint64 (1 for true, 0 for false). func Bool2uint64(b bool) uint64 { if b { return 1 @@ -277,13 +311,17 @@ func Bool2uint64(b bool) uint64 { return 0 } +// LpVec3d is a compressed 3D velocity vector used in Minecraft packets. type LpVec3d struct { X, Y, Z float64 } +// Encode encodes the LpVec3d as a compressed velocity triple into w. func (v *LpVec3d) Encode(w io.Writer) error { return writeVelocity(w, *v) } + +// Decode decodes a compressed velocity triple from r into the LpVec3d. func (v *LpVec3d) Decode(r io.ReadSeeker) error { v3, err := readVelocity(r) if err != nil { diff --git a/mc/server/config_server.go b/mc/server/config_server.go index e7e594a..b77563c 100644 --- a/mc/server/config_server.go +++ b/mc/server/config_server.go @@ -14,6 +14,7 @@ type ConfigServer struct { registryData map[string]any } +// ServeConfig runs the configuration phase: sends known packs, registry data, and tags, then transitions to play. func ServeConfig(c *proto.Conn) (err error) { cs := &ConfigServer{Conn: c} n, err := data.LoadRegistry(c.Version) diff --git a/mc/server/kicker.go b/mc/server/kicker.go index 4e44676..11e611b 100644 --- a/mc/server/kicker.go +++ b/mc/server/kicker.go @@ -17,6 +17,7 @@ var ( CantKickInStateErr = errors.New("cant kick in this state") ) +// Kick sends a disconnect/kick packet appropriate for the connection's current state. func Kick(c *proto.Conn, reason *text.Component, status string) (err error) { if c.Actor != proto_base.Servee { err = NotClientErr diff --git a/mc/server/login_server.go b/mc/server/login_server.go index c61b6a5..94b1307 100644 --- a/mc/server/login_server.go +++ b/mc/server/login_server.go @@ -204,6 +204,7 @@ func (l *LoginServer) FinishLogin() (err error) { type LoginServerSetting func(*LoginServer) +// WithOtherAccount returns a setting that overrides the login identity with the given account. func WithOtherAccount(a *api2.Account) LoginServerSetting { return func(s *LoginServer) { s.Given = &NameAndUUID{ @@ -213,12 +214,14 @@ func WithOtherAccount(a *api2.Account) LoginServerSetting { } } +// WithRawStatus returns a setting that sets the raw JSON status response payload. func WithRawStatus(status []byte) LoginServerSetting { return func(s *LoginServer) { s.Status = status } } +// WithStatus marshals a StatusResponse to JSON and passes it to WithRawStatus. func WithStatus(response StatusResponse) LoginServerSetting { var status []byte status, err := json.Marshal(response) @@ -229,6 +232,7 @@ func WithStatus(response StatusResponse) LoginServerSetting { return WithRawStatus(status) } +// WithCompatibleVersionsRange returns a setting that restricts the server to protocol versions in [start, end]. func WithCompatibleVersionsRange(start, end int32) LoginServerSetting { var versions []int32 for i := start; i <= end; i++ { @@ -237,24 +241,28 @@ func WithCompatibleVersionsRange(start, end int32) LoginServerSetting { return WithCompatibleVersions(versions...) } +// WithCompatibleVersions returns a setting that restricts the server to the given protocol versions. func WithCompatibleVersions(versions ...int32) LoginServerSetting { return func(s *LoginServer) { s.CompatibleVersions = versions } } +// WithoutOnlineMode returns a setting that disables online-mode authentication. func WithoutOnlineMode() LoginServerSetting { return func(s *LoginServer) { s.OnlineMode = false } } +// WithoutEncryption returns a setting that disables connection encryption. func WithoutEncryption() LoginServerSetting { return func(s *LoginServer) { s.Encryption = false } } +// ServeLogin runs the full login sequence (handshake, encryption, auth, and configuration handoff) on a connection. func ServeLogin(c *proto.Conn, settings ...LoginServerSetting) (ret *LoginServer, err error) { ret = &LoginServer{Conn: c} ret.OnlineMode = true diff --git a/mc/server/server.go b/mc/server/server.go index e3ff1de..6998b70 100644 --- a/mc/server/server.go +++ b/mc/server/server.go @@ -9,6 +9,7 @@ import ( "github.com/admin-else/strom/mc/text" ) +// Servee wraps a net.Conn in a new proto.Conn configured for the servee side. func Servee(c net.Conn) (ret *proto.Conn) { ret = proto.NewConn() ret.Actor = proto_base.Servee @@ -24,6 +25,7 @@ type ConnAcceptor func(c net.Conn) (err error) type Factory func(c *proto.Conn) (h ConnAcceptor, err error) +// ServeClient accepts a single client connection, creates a handler via the factory, and runs it. func ServeClient(cNet net.Conn, factory Factory) { c := Servee(cNet) defer c.Close() @@ -40,6 +42,7 @@ func ServeClient(cNet net.Conn, factory Factory) { } } +// StartServerWithFactory listens on the given address and spawns ServeClient for every accepted connection. func StartServerWithFactory(listenAddr string, factory Factory) (err error) { l, err := net.Listen("tcp", listenAddr) if err != nil { @@ -57,6 +60,8 @@ func StartServerWithFactory(listenAddr string, factory Factory) (err error) { var ShutDownServerErr = errors.New("server shutting down") +// StartServerWithOnConn listens on the given address and calls onConn for every accepted connection. +// The server loop stops when onConn returns ShutDownServerErr. func StartServerWithOnConn(listenAddr string, onConn func(c *proto.Conn) (err error)) (err error) { l, err := net.Listen("tcp", listenAddr) if err != nil { diff --git a/mc/server/status.go b/mc/server/status.go index 48cdbd2..b750a0a 100644 --- a/mc/server/status.go +++ b/mc/server/status.go @@ -56,6 +56,7 @@ func (p *StatusServer) OnStatusPing(packet *v1_21_8.StatusToServerPacketPing) (e return p.Send(&v1_21_8.StatusToClientPacketPing{Time: packet.Time}) } +// ServeStatus runs the server list ping sequence, responding with the given StatusResponse. func ServeStatus(c *proto.Conn, s StatusResponse) (err error) { status, err := json.Marshal(s) if err != nil { diff --git a/mc/text/text.go b/mc/text/text.go index d18303a..5bd926b 100644 --- a/mc/text/text.go +++ b/mc/text/text.go @@ -526,6 +526,7 @@ func replacePlaceholders(s string, values map[string]any) string { return result } +// PrettyPlaceholders parses a legacy formatted string with named placeholders replaced from the given map, returning a Component. func PrettyPlaceholders(format string, placeholders map[string]any) *Component { s := replacePlaceholders(format, placeholders) return Pretty(s) diff --git a/mc/util/case.go b/mc/util/case.go index 1c460b9..12beec7 100644 --- a/mc/util/case.go +++ b/mc/util/case.go @@ -7,6 +7,7 @@ import ( var boundaries = []rune{' ', '_', '-', '.', ',', ';', ':', '(', ')', '[', ']', '{', '}'} +// CamelCase converts a string with word boundaries to CamelCase. func CamelCase(s string) string { ret := "" atWordBoundary := true @@ -24,6 +25,7 @@ func CamelCase(s string) string { return ret } +// SnakeCase converts a CamelCase string to snake_case. func SnakeCase(s string) string { ret := "" for i, c := range s { @@ -38,6 +40,7 @@ func SnakeCase(s string) string { return ret } +// FirstLetterLower returns the string with the first rune lowercased. func FirstLetterLower(s string) string { return string(unicode.ToLower(rune(s[0]))) + s[1:] } diff --git a/mc/util/math.go b/mc/util/math.go index dd659bc..2f80f99 100644 --- a/mc/util/math.go +++ b/mc/util/math.go @@ -2,10 +2,12 @@ package util import "math" +// FloorDiv returns floor division (rounds toward negative infinity). func FloorDiv(a, b int32) int32 { return (a - ((a%b + b) % b)) / b } +// BpeByNum returns the minimum bits per element needed to represent x distinct values. func BpeByNum(x float64) uint8 { return uint8(math.Ceil(math.Log2(x))) } diff --git a/mc/util/must.go b/mc/util/must.go index a2332d6..0e91b82 100644 --- a/mc/util/must.go +++ b/mc/util/must.go @@ -2,6 +2,7 @@ package util import "fmt" +// MustOk returns v if ok is true, otherwise panics. func MustOk[T any](v T, ok bool) T { if !ok { panic("not ok") @@ -9,6 +10,7 @@ func MustOk[T any](v T, ok bool) T { return v } +// MustT returns v if err is nil, otherwise panics. func MustT[T any](v T, err error) T { if err != nil { panic(err) @@ -16,12 +18,14 @@ func MustT[T any](v T, err error) T { return v } +// Must panics if err is non-nil. func Must(err error) { if err != nil { panic(err) } } +// AssertTypeError asserts that v is of type T and returns it, or returns an error. func AssertTypeError[T any](v any) (T, error) { var zero T r, ok := v.(T) @@ -31,6 +35,7 @@ func AssertTypeError[T any](v any) (T, error) { return r, nil } +// MapGetError returns the value for key k from map m, or an error if not present. func MapGetError[K comparable, V any, M map[K]V](m M, k K) (V, error) { v, ok := m[k] if !ok { diff --git a/mc/util/utils.go b/mc/util/utils.go index 98edce3..4468c6d 100644 --- a/mc/util/utils.go +++ b/mc/util/utils.go @@ -6,6 +6,7 @@ import ( "slices" ) +// OrderedKeys returns the keys of the map sorted in ascending order. func OrderedKeys[Map map[K]V, K cmp.Ordered, V any](m Map) []K { keysIter := maps.Keys(m) keys := slices.Collect(keysIter) @@ -13,6 +14,7 @@ func OrderedKeys[Map map[K]V, K cmp.Ordered, V any](m Map) []K { return keys } +// AssertAndConvertMapValues asserts and converts all values in a map[string]any to type T. func AssertAndConvertMapValues[T any, M map[K]any, K cmp.Ordered](m M) map[K]T { ret := map[K]T{} for k, v := range m { @@ -21,6 +23,7 @@ func AssertAndConvertMapValues[T any, M map[K]any, K cmp.Ordered](m M) map[K]T { return ret } +// ReverseMap returns a new map with keys and values swapped. func ReverseMap[M map[K]V, K cmp.Ordered, V cmp.Ordered](m M) map[V]K { ret := map[V]K{} for k, v := range m { @@ -29,6 +32,7 @@ func ReverseMap[M map[K]V, K cmp.Ordered, V cmp.Ordered](m M) map[V]K { return ret } +// PopFront removes and returns the first element of the slice. func PopFront[T any](s []T) (T, []T, bool) { if len(s) == 0 { var zero T @@ -37,6 +41,7 @@ func PopFront[T any](s []T) (T, []T, bool) { return s[0], s[1:], true } +// CombineNamAndData returns a slice containing name and data. func CombineNamAndData(name string, data any) []any { return []any{name, data} }