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 (14 x uint16 = 28 bytes each) for j := uint16(0); j < channelCount; j++ { port := bf.ReadUint16() _ = bf.ReadUint16() // channelIdx | 16 _ = bf.ReadUint16() // maxPlayers _ = bf.ReadUint16() // currentPlayers _ = bf.ReadBytes(18) // remaining channel fields (9 x uint16: 6 zeros + unk319 + unk254 + unk255) _ = 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 }