mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
Copy MHBridge into the Erupe module as cmd/protbot/ so it can be built, tested, and maintained alongside the server. The bot implements the full sign → entrance → channel login flow and supports lobby entry, chat, session setup, and quest enumeration. The conn/ package keeps its own Blowfish crypto primitives to avoid importing erupe-ce/config (which requires a config file at init).
143 lines
3.7 KiB
Go
143 lines
3.7 KiB
Go
package protocol
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"net"
|
|
|
|
"erupe-ce/common/byteframe"
|
|
|
|
"erupe-ce/cmd/protbot/conn"
|
|
)
|
|
|
|
// ServerEntry represents a channel server from the entrance server response.
|
|
type ServerEntry struct {
|
|
IP string
|
|
Port uint16
|
|
Name string
|
|
}
|
|
|
|
// DoEntrance connects to the entrance server and retrieves the server list.
|
|
// Reference: Erupe server/entranceserver/entrance_server.go and make_resp.go.
|
|
func DoEntrance(addr string) ([]ServerEntry, error) {
|
|
c, err := conn.DialWithInit(addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("entrance connect: %w", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
// Send a minimal packet (the entrance server reads it, checks len > 5 for USR data).
|
|
// An empty/short packet triggers only SV2 response.
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint8(0)
|
|
if err := c.SendPacket(bf.Data()); err != nil {
|
|
return nil, fmt.Errorf("entrance send: %w", err)
|
|
}
|
|
|
|
resp, err := c.ReadPacket()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("entrance recv: %w", err)
|
|
}
|
|
|
|
return parseEntranceResponse(resp)
|
|
}
|
|
|
|
// parseEntranceResponse parses the Bin8-encrypted entrance server response.
|
|
// Reference: Erupe server/entranceserver/make_resp.go (makeHeader, makeSv2Resp)
|
|
func parseEntranceResponse(data []byte) ([]ServerEntry, error) {
|
|
if len(data) < 2 {
|
|
return nil, fmt.Errorf("entrance response too short")
|
|
}
|
|
|
|
// First byte is the Bin8 encryption key.
|
|
key := data[0]
|
|
decrypted := conn.DecryptBin8(data[1:], key)
|
|
|
|
rbf := byteframe.NewByteFrameFromBytes(decrypted)
|
|
|
|
// Read response type header: "SV2" or "SVR"
|
|
respType := string(rbf.ReadBytes(3))
|
|
if respType != "SV2" && respType != "SVR" {
|
|
return nil, fmt.Errorf("unexpected entrance response type: %s", respType)
|
|
}
|
|
|
|
entryCount := rbf.ReadUint16()
|
|
dataLen := rbf.ReadUint16()
|
|
if dataLen == 0 {
|
|
return nil, nil
|
|
}
|
|
expectedSum := rbf.ReadUint32()
|
|
serverData := rbf.ReadBytes(uint(dataLen))
|
|
|
|
actualSum := conn.CalcSum32(serverData)
|
|
if expectedSum != actualSum {
|
|
return nil, fmt.Errorf("entrance checksum mismatch: expected %08X, got %08X", expectedSum, actualSum)
|
|
}
|
|
|
|
return parseServerEntries(serverData, entryCount)
|
|
}
|
|
|
|
// parseServerEntries parses the server info binary blob.
|
|
// Reference: Erupe server/entranceserver/make_resp.go (encodeServerInfo)
|
|
func parseServerEntries(data []byte, entryCount uint16) ([]ServerEntry, error) {
|
|
bf := byteframe.NewByteFrameFromBytes(data)
|
|
var entries []ServerEntry
|
|
|
|
for i := uint16(0); i < entryCount; i++ {
|
|
ipBytes := bf.ReadBytes(4)
|
|
ip := net.IP([]byte{
|
|
byte(ipBytes[3]), byte(ipBytes[2]),
|
|
byte(ipBytes[1]), byte(ipBytes[0]),
|
|
})
|
|
|
|
_ = bf.ReadUint16() // serverIdx | 16
|
|
_ = bf.ReadUint16() // 0
|
|
channelCount := bf.ReadUint16()
|
|
_ = bf.ReadUint8() // Type
|
|
_ = bf.ReadUint8() // Season/rotation
|
|
|
|
// G1+ recommended flag
|
|
_ = bf.ReadUint8()
|
|
|
|
// G51+ (ZZ): skip 1 byte, then read 65-byte padded name
|
|
_ = bf.ReadUint8()
|
|
nameBytes := bf.ReadBytes(65)
|
|
|
|
// GG+: AllowedClientFlags
|
|
_ = bf.ReadUint32()
|
|
|
|
// Parse name (null-separated: name + description)
|
|
name := ""
|
|
for j := 0; j < len(nameBytes); j++ {
|
|
if nameBytes[j] == 0 {
|
|
break
|
|
}
|
|
name += string(nameBytes[j])
|
|
}
|
|
|
|
// Read channel entries
|
|
for j := uint16(0); j < channelCount; j++ {
|
|
port := bf.ReadUint16()
|
|
_ = bf.ReadUint16() // channelIdx | 16
|
|
_ = bf.ReadUint16() // maxPlayers
|
|
_ = bf.ReadUint16() // currentPlayers
|
|
_ = bf.ReadBytes(14) // remaining channel fields (7 x uint16)
|
|
_ = bf.ReadUint16() // 12345
|
|
|
|
serverIP := ip.String()
|
|
// Convert 127.0.0.1 representation
|
|
if binary.LittleEndian.Uint32(ipBytes) == 0x0100007F {
|
|
serverIP = "127.0.0.1"
|
|
}
|
|
|
|
entries = append(entries, ServerEntry{
|
|
IP: serverIP,
|
|
Port: port,
|
|
Name: fmt.Sprintf("%s ch%d", name, j+1),
|
|
})
|
|
}
|
|
}
|
|
|
|
return entries, nil
|
|
}
|