Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions minecraft/DOCS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# minecraft

`minecraft` currently provides a small client for querying Minecraft Bedrock server status over UDP.

## Quick Start

Use the package directly:

```go
package main

import (
"fmt"
"log"
"time"

"github.com/barluscuda/dextools/minecraft"
)

func main() {
client := &minecraft.Bedrock{
Timeout: 5 * time.Second,
}

status, err := client.ServerStatus("play.nethergames.org", 19132)
if err != nil {
log.Fatal(err)
}

fmt.Println("MOTD:", status.MOTD)
fmt.Println("Version:", status.Version)
fmt.Printf("Players: %d/%d\n", status.PlayersOnline, status.PlayersMax)
fmt.Println("Ping:", status.Ping)
}
```

## Behavior

- `Bedrock.ServerStatus` sends a UDP Bedrock status ping to the target host and port.
- `Bedrock.Timeout` is used for both dialing and the read deadline.
- The response parser expects a valid Bedrock unconnected pong payload with the `MCPE` edition marker.
- Invalid numeric fields in the server response return an error instead of silently becoming `0`.
- `Ping` is measured from request write to response read.

## Available Types

The package currently exposes:

- `Bedrock`
- `BedrockServerStatusStruct`

## Client

### `Bedrock`

`Bedrock` is the client used to query a Bedrock server.

```go
type Bedrock struct {
Timeout time.Duration
}
```

## Status Response

### `BedrockServerStatusStruct`

`ServerStatus` returns a `*BedrockServerStatusStruct` with the parsed server response.

```go
type BedrockServerStatusStruct struct {
Ping time.Duration
MOTD string
Protocol int
Version string
PlayersOnline int
PlayersMax int
ServerID string
LevelName string
GameMode string
GameModeNumeric int
IPv4Port int
IPv6Port int
}
```

Field meanings:

- `Ping`: round-trip duration for the status request.
- `MOTD`: server message of the day.
- `Protocol`: Bedrock protocol version number.
- `Version`: Bedrock version string reported by the server.
- `PlayersOnline`: currently connected player count.
- `PlayersMax`: maximum configured player count.
- `ServerID`: server identifier from the Bedrock response.
- `LevelName`: world or level name.
- `GameMode`: game mode name.
- `GameModeNumeric`: numeric game mode identifier.
- `IPv4Port`: IPv4 listener port reported by the server.
- `IPv6Port`: IPv6 listener port reported by the server.

## API

### `func (b *Bedrock) ServerStatus(host string, port int) (*BedrockServerStatusStruct, error)`

`ServerStatus` connects to `host:port`, sends a Bedrock status ping, parses the response, and returns the structured result.

Common error cases:

- UDP dial failures
- request timeout or read timeout
- malformed status packets
- unexpected response payloads

## Testing

Unit tests for the Bedrock status client live in [bedrock-server-status_test.go](./bedrock-server-status_test.go).
141 changes: 141 additions & 0 deletions minecraft/bedrock-server-status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package minecraft

import (
"bytes"
"encoding/binary"
"fmt"
"net"
"strconv"
"time"
)

type BedrockServerStatusStruct struct {
Ping time.Duration
MOTD string
Protocol int
Version string
PlayersOnline int
PlayersMax int
ServerID string
LevelName string
GameMode string
GameModeNumeric int
IPv4Port int
IPv6Port int
}

func (b *Bedrock) ServerStatus(host string, port int) (*BedrockServerStatusStruct, error) {
addr := fmt.Sprintf("%s:%d", host, port)

conn, err := net.DialTimeout("udp", addr, b.Timeout)
if err != nil {
return nil, err
}
defer conn.Close()

_ = conn.SetDeadline(time.Now().Add(b.Timeout))

var packet bytes.Buffer
packet.WriteByte(0x01)

timestamp := time.Now().UnixMilli()
_ = binary.Write(&packet, binary.BigEndian, timestamp)

packet.Write([]byte{
0x00, 0xff, 0xff, 0x00,
0xfe, 0xfe, 0xfe, 0xfe,
0xfd, 0xfd, 0xfd, 0xfd,
0x12, 0x34, 0x56, 0x78,
})

_ = binary.Write(&packet, binary.BigEndian, int64(1))

start := time.Now()

if _, err := conn.Write(packet.Bytes()); err != nil {
return nil, err
}

buf := make([]byte, 2048)
n, err := conn.Read(buf)
if err != nil {
return nil, err
}

ping := time.Since(start)

return parseBedrockStatusResponse(buf[:n], ping)
}

func parseBedrockStatusResponse(packet []byte, ping time.Duration) (*BedrockServerStatusStruct, error) {
if len(packet) < 35 || packet[0] != 0x1c {
return nil, fmt.Errorf("invalid response")
}

offset := 1 + 8 + 8 + 16
if offset+2 > len(packet) {
return nil, fmt.Errorf("invalid packet")
}

strLen := int(binary.BigEndian.Uint16(packet[offset : offset+2]))
offset += 2
if offset+strLen > len(packet) {
return nil, fmt.Errorf("invalid string length")
}

parts := bytes.Split(packet[offset:offset+strLen], []byte(";"))
if len(parts) < 12 {
return nil, fmt.Errorf("invalid bedrock response")
}
if string(parts[0]) != "MCPE" {
return nil, fmt.Errorf("invalid edition: %q", string(parts[0]))
}

parseInt := func(field string, raw []byte) (int, error) {
value, err := strconv.Atoi(string(raw))
if err != nil {
return 0, fmt.Errorf("invalid %s: %q", field, string(raw))
}
return value, nil
}

protocol, err := parseInt("protocol", parts[2])
if err != nil {
return nil, err
}
playersOnline, err := parseInt("players online", parts[4])
if err != nil {
return nil, err
}
playersMax, err := parseInt("players max", parts[5])
if err != nil {
return nil, err
}
gameModeNumeric, err := parseInt("game mode numeric", parts[9])
if err != nil {
return nil, err
}
ipv4Port, err := parseInt("IPv4 port", parts[10])
if err != nil {
return nil, err
}
ipv6Port, err := parseInt("IPv6 port", parts[11])
if err != nil {
return nil, err
}

return &BedrockServerStatusStruct{
Ping: ping,
MOTD: string(parts[1]),
Protocol: protocol,
Version: string(parts[3]),
PlayersOnline: playersOnline,
PlayersMax: playersMax,
ServerID: string(parts[6]),
LevelName: string(parts[7]),
GameMode: string(parts[8]),
GameModeNumeric: gameModeNumeric,
IPv4Port: ipv4Port,
IPv6Port: ipv6Port,
}, nil
}
Loading
Loading