这是indexloc提供的服务,不要输入任何密码
Skip to content
Open
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
148 changes: 139 additions & 9 deletions minecraft/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import (
"encoding/base64"
"errors"
"fmt"
"io"
"log/slog"
"net"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/go-gl/mathgl/mgl32"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/google/uuid"
Expand All @@ -19,13 +29,6 @@ import (
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
"github.com/sandertv/gophertunnel/minecraft/text"
"io"
"log/slog"
"net"
"strings"
"sync"
"sync/atomic"
"time"
)

// exemptedResourcePack is a resource pack that is exempted from being downloaded. These packs may be directly
Expand Down Expand Up @@ -141,6 +144,14 @@ type Conn struct {
shieldID atomic.Int32

additional chan packet.Packet

// receivedPackets tracks which packets have been received from the server
// Used to ensure proper packet ordering for anticheat checks
receivedPackets sync.Map

// serverVersionOverrides is a map of server addresses to specific game versions to use
// when connecting to those servers
serverVersionOverrides map[string]string
}

// newConn creates a new Minecraft connection for the net.Conn passed, reading and writing compressed
Expand Down Expand Up @@ -612,6 +623,9 @@ func (conn *Conn) receive(data []byte) error {

// handle tries to handle the incoming packetData.
func (conn *Conn) handle(pkData *packetData) error {
// Record this packet type as received, which helps us ensure proper packet ordering
conn.recordPacketReceived(pkData.h.PacketID)

for _, id := range conn.expectedIDs.Load().([]uint32) {
if id == pkData.h.PacketID {
// If the packet was expected, so we handle it right now.
Expand Down Expand Up @@ -1202,6 +1216,32 @@ func (conn *Conn) handleResourcePackChunkRequest(pk *packet.ResourcePackChunkReq
// handleStartGame handles an incoming StartGame packet. It is the signal that the player has been added to a
// world, and it obtains most of its dedicated properties.
func (conn *Conn) handleStartGame(pk *packet.StartGame) error {
conn.receivedPackets.Store("gotStartGame", true)

// Check if there's a version override for this server
if conn.serverVersionOverrides != nil {
for serverPattern, version := range conn.serverVersionOverrides {
// Try exact match first
if serverPattern == conn.clientData.ServerAddress {
pk.BaseGameVersion = version
break
}

// If not exact match, try regex match
matched, err := regexp.MatchString(serverPattern, conn.clientData.ServerAddress)
if err != nil {
// If regex is invalid, log error but continue with other patterns
conn.log.Error(fmt.Sprintf("Invalid regex pattern for server version override: %s - %v", serverPattern, err))
continue
}

if matched {
pk.BaseGameVersion = version
break
}
}
}

conn.gameData = GameData{
Difficulty: pk.Difficulty,
WorldName: pk.WorldName,
Expand Down Expand Up @@ -1249,8 +1289,41 @@ func (conn *Conn) handleItemRegistry(pk *packet.ItemRegistry) error {
}
}

_ = conn.WritePacket(&packet.RequestChunkRadius{ChunkRadius: 16})
conn.expect(packet.IDChunkRadiusUpdated, packet.IDPlayStatus)
// Check if all required packets have been received
if conn.haveRequiredPacketsArrived() {
// All packets received, we can send RequestChunkRadius
_ = conn.WritePacket(&packet.RequestChunkRadius{ChunkRadius: 16})
conn.expect(packet.IDChunkRadiusUpdated, packet.IDPlayStatus)
return nil
}

// Not all required packets received yet, we'll keep waiting
// Start a goroutine to periodically check and send RequestChunkRadius when ready
go func() {
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()

timeout := time.After(5 * time.Second)

for {
select {
case <-ticker.C:
if conn.haveRequiredPacketsArrived() {
_ = conn.WritePacket(&packet.RequestChunkRadius{ChunkRadius: 16})
conn.expect(packet.IDChunkRadiusUpdated, packet.IDPlayStatus)
return
}
case <-timeout:
// Safety timeout, send the request anyway
_ = conn.WritePacket(&packet.RequestChunkRadius{ChunkRadius: 16})
conn.expect(packet.IDChunkRadiusUpdated, packet.IDPlayStatus)
return
case <-conn.ctx.Done():
return
}
}
}()

return nil
}

Expand Down Expand Up @@ -1319,6 +1392,7 @@ func (conn *Conn) handleSetLocalPlayerAsInitialised(pk *packet.SetLocalPlayerAsI
func (conn *Conn) handlePlayStatus(pk *packet.PlayStatus) error {
switch pk.Status {
case packet.PlayStatusLoginSuccess:
// Send ClientCacheStatus immediately after login success
if err := conn.WritePacket(&packet.ClientCacheStatus{Enabled: conn.cacheEnabled}); err != nil {
return fmt.Errorf("send ClientCacheStatus: %w", err)
}
Expand Down Expand Up @@ -1367,6 +1441,32 @@ func (conn *Conn) tryFinaliseClientConn() {
conn.waitingForSpawn.Store(false)
conn.gameDataReceived.Store(false)

_, gotStartGame := conn.receivedPackets.Load("gotStartGame")

_ = conn.WritePacket(&packet.Interact{
ActionType: packet.InteractActionMouseOverEntity,
TargetEntityRuntimeID: 0,
Position: mgl32.Vec3{},
})

if gotStartGame {
_ = conn.WritePacket(&packet.PlayerAuthInput{
Pitch: 0,
Yaw: 0,
Position: conn.gameData.PlayerPosition,
MoveVector: mgl32.Vec2{},
HeadYaw: 0,
InputData: protocol.NewBitset(packet.PlayerAuthInputBitsetSize),
InputMode: packet.InputModeTouch,
PlayMode: packet.PlayModeNormal,
InteractionModel: packet.InteractionModelTouch,
InteractPitch: 0,
InteractYaw: 0,
Tick: 0,
Delta: mgl32.Vec3{},
})
}

close(conn.spawn)
conn.loggedIn = true
_ = conn.WritePacket(&packet.SetLocalPlayerAsInitialised{EntityRuntimeID: conn.gameData.EntityRuntimeID})
Expand Down Expand Up @@ -1430,3 +1530,33 @@ func (conn *Conn) closeErr(op string) error {
return conn.wrap(net.ErrClosed, op)
}
}

func (conn *Conn) recordPacketReceived(packetID uint32) {
conn.receivedPackets.Store(packetID, true)
}

func (conn *Conn) hasReceivedPacket(packetID uint32) bool {
_, ok := conn.receivedPackets.Load(packetID)
return ok
}

func (conn *Conn) haveRequiredPacketsArrived() bool {
requiredPackets := []uint32{
packet.IDAvailableActorIdentifiers, // AvailableEntityIdentifiers
packet.IDBiomeDefinitionList, // BiomeDefinitionList
packet.IDUpdateAttributes, // UpdateAttributes
packet.IDAvailableCommands, // AvailableCommands
packet.IDUpdateAbilities, // UpdateAbilities
packet.IDSetActorData, // SetEntityData
packet.IDInventoryContent, // InventoryContent
packet.IDMobEquipment, // MobEquipment
packet.IDPlayerList, // PlayerList
}

for _, id := range requiredPackets {
if !conn.hasReceivedPacket(id) {
return false
}
}
return true
}
6 changes: 6 additions & 0 deletions minecraft/dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ type Dialer struct {
// the client when an XUID is present without logging in.
// For getting this to work with BDS, authentication should be disabled.
KeepXBLIdentityData bool

// BaseGameVersion is a map of server addresses to game versions to use when connecting to those servers.
// The keys are regex patterns that match against server addresses, and the values are the game versions
// to use when connecting to a server that matches the pattern.
BaseGameVersion map[string]string
}

// Dial dials a Minecraft connection to the address passed over the network passed. The network is typically
Expand Down Expand Up @@ -203,6 +208,7 @@ func (d Dialer) DialContext(ctx context.Context, network, address string) (conn
conn.cacheEnabled = d.EnableClientCache
conn.disconnectOnInvalidPacket = d.DisconnectOnInvalidPackets
conn.disconnectOnUnknownPacket = d.DisconnectOnUnknownPackets
conn.serverVersionOverrides = d.BaseGameVersion

defaultIdentityData(&conn.identityData)
defaultClientData(address, conn.identityData.DisplayName, &conn.clientData)
Expand Down
4 changes: 4 additions & 0 deletions minecraft/example_dial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package minecraft_test

import (
"fmt"

"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/auth"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
Expand All @@ -11,6 +12,9 @@ func ExampleDial() {
// Create a minecraft.Dialer with an auth.TokenSource to authenticate to the server.
dialer := minecraft.Dialer{
TokenSource: auth.TokenSource,
BaseGameVersion: map[string]string{
"^.*\\.hivebedrock\\.network.*$": "1.17.0",
},
}
// Dial a new connection to the target server.
address := "mco.mineplex.com:19132"
Expand Down
1 change: 1 addition & 0 deletions minecraft/example_listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package minecraft_test

import (
"fmt"

"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)
Expand Down
9 changes: 5 additions & 4 deletions minecraft/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import (
"crypto/rand"
"errors"
"fmt"
"github.com/sandertv/gophertunnel/minecraft/internal"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
"log/slog"
"net"
"slices"
"sync"
"sync/atomic"
"time"

"github.com/sandertv/gophertunnel/minecraft/internal"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
)

// ListenConfig holds settings that may be edited to change behaviour of a Listener.
Expand Down