mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 23:54:33 +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).
191 lines
4.7 KiB
Go
191 lines
4.7 KiB
Go
package protocol
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"erupe-ce/cmd/protbot/conn"
|
|
)
|
|
|
|
// PacketHandler is a callback invoked when a server-pushed packet is received.
|
|
type PacketHandler func(opcode uint16, data []byte)
|
|
|
|
// ChannelConn manages a connection to a channel server.
|
|
type ChannelConn struct {
|
|
conn *conn.MHFConn
|
|
ackCounter uint32
|
|
waiters sync.Map // map[uint32]chan *AckResponse
|
|
handlers sync.Map // map[uint16]PacketHandler
|
|
closed atomic.Bool
|
|
}
|
|
|
|
// OnPacket registers a handler for a specific server-pushed opcode.
|
|
// Only one handler per opcode; later registrations replace earlier ones.
|
|
func (ch *ChannelConn) OnPacket(opcode uint16, handler PacketHandler) {
|
|
ch.handlers.Store(opcode, handler)
|
|
}
|
|
|
|
// AckResponse holds the parsed ACK data from the server.
|
|
type AckResponse struct {
|
|
AckHandle uint32
|
|
IsBufferResponse bool
|
|
ErrorCode uint8
|
|
Data []byte
|
|
}
|
|
|
|
// ConnectChannel establishes a connection to a channel server.
|
|
// Channel servers do NOT use the 8 NULL byte initialization.
|
|
func ConnectChannel(addr string) (*ChannelConn, error) {
|
|
c, err := conn.DialDirect(addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("channel connect: %w", err)
|
|
}
|
|
|
|
ch := &ChannelConn{
|
|
conn: c,
|
|
}
|
|
|
|
go ch.recvLoop()
|
|
return ch, nil
|
|
}
|
|
|
|
// NextAckHandle returns the next unique ACK handle for packet requests.
|
|
func (ch *ChannelConn) NextAckHandle() uint32 {
|
|
return atomic.AddUint32(&ch.ackCounter, 1)
|
|
}
|
|
|
|
// SendPacket encrypts and sends raw packet data (including the 0x00 0x10 terminator
|
|
// which is already appended by the Build* functions in packets.go).
|
|
func (ch *ChannelConn) SendPacket(data []byte) error {
|
|
return ch.conn.SendPacket(data)
|
|
}
|
|
|
|
// WaitForAck waits for an ACK response matching the given handle.
|
|
func (ch *ChannelConn) WaitForAck(handle uint32, timeout time.Duration) (*AckResponse, error) {
|
|
waitCh := make(chan *AckResponse, 1)
|
|
ch.waiters.Store(handle, waitCh)
|
|
defer ch.waiters.Delete(handle)
|
|
|
|
select {
|
|
case resp := <-waitCh:
|
|
return resp, nil
|
|
case <-time.After(timeout):
|
|
return nil, fmt.Errorf("ACK timeout for handle %d", handle)
|
|
}
|
|
}
|
|
|
|
// Close closes the channel connection.
|
|
func (ch *ChannelConn) Close() error {
|
|
ch.closed.Store(true)
|
|
return ch.conn.Close()
|
|
}
|
|
|
|
// recvLoop continuously reads packets from the channel server and dispatches ACKs.
|
|
func (ch *ChannelConn) recvLoop() {
|
|
for {
|
|
if ch.closed.Load() {
|
|
return
|
|
}
|
|
|
|
pkt, err := ch.conn.ReadPacket()
|
|
if err != nil {
|
|
if ch.closed.Load() {
|
|
return
|
|
}
|
|
fmt.Printf("[channel] read error: %v\n", err)
|
|
return
|
|
}
|
|
|
|
if len(pkt) < 2 {
|
|
continue
|
|
}
|
|
|
|
// Strip trailing 0x00 0x10 terminator if present for opcode parsing.
|
|
// Packets from server: [opcode uint16][fields...][0x00 0x10]
|
|
opcode := binary.BigEndian.Uint16(pkt[0:2])
|
|
|
|
switch opcode {
|
|
case MSG_SYS_ACK:
|
|
ch.handleAck(pkt[2:])
|
|
case MSG_SYS_PING:
|
|
ch.handlePing(pkt[2:])
|
|
default:
|
|
if val, ok := ch.handlers.Load(opcode); ok {
|
|
val.(PacketHandler)(opcode, pkt[2:])
|
|
} else {
|
|
fmt.Printf("[channel] recv opcode 0x%04X (%d bytes)\n", opcode, len(pkt))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleAck parses an ACK packet and dispatches it to a waiting caller.
|
|
// Reference: Erupe network/mhfpacket/msg_sys_ack.go
|
|
func (ch *ChannelConn) handleAck(data []byte) {
|
|
if len(data) < 8 {
|
|
return
|
|
}
|
|
|
|
ackHandle := binary.BigEndian.Uint32(data[0:4])
|
|
isBuffer := data[4] > 0
|
|
errorCode := data[5]
|
|
|
|
var ackData []byte
|
|
if isBuffer {
|
|
payloadSize := binary.BigEndian.Uint16(data[6:8])
|
|
offset := uint32(8)
|
|
if payloadSize == 0xFFFF {
|
|
if len(data) < 12 {
|
|
return
|
|
}
|
|
payloadSize32 := binary.BigEndian.Uint32(data[8:12])
|
|
offset = 12
|
|
if uint32(len(data)) >= offset+payloadSize32 {
|
|
ackData = data[offset : offset+payloadSize32]
|
|
}
|
|
} else {
|
|
if uint32(len(data)) >= offset+uint32(payloadSize) {
|
|
ackData = data[offset : offset+uint32(payloadSize)]
|
|
}
|
|
}
|
|
} else {
|
|
// Simple ACK: 4 bytes of data after the uint16 field.
|
|
if len(data) >= 12 {
|
|
ackData = data[8:12]
|
|
}
|
|
}
|
|
|
|
resp := &AckResponse{
|
|
AckHandle: ackHandle,
|
|
IsBufferResponse: isBuffer,
|
|
ErrorCode: errorCode,
|
|
Data: ackData,
|
|
}
|
|
|
|
if val, ok := ch.waiters.Load(ackHandle); ok {
|
|
waitCh := val.(chan *AckResponse)
|
|
select {
|
|
case waitCh <- resp:
|
|
default:
|
|
}
|
|
} else {
|
|
fmt.Printf("[channel] unexpected ACK handle %d (error=%d, buffer=%v, %d bytes)\n",
|
|
ackHandle, errorCode, isBuffer, len(ackData))
|
|
}
|
|
}
|
|
|
|
// handlePing responds to a server ping to keep the connection alive.
|
|
func (ch *ChannelConn) handlePing(data []byte) {
|
|
var ackHandle uint32
|
|
if len(data) >= 4 {
|
|
ackHandle = binary.BigEndian.Uint32(data[0:4])
|
|
}
|
|
pkt := BuildPingPacket(ackHandle)
|
|
if err := ch.conn.SendPacket(pkt); err != nil {
|
|
fmt.Printf("[channel] ping response failed: %v\n", err)
|
|
}
|
|
}
|