mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
feat(protbot): add headless MHF protocol bot as cmd/protbot
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).
This commit is contained in:
190
cmd/protbot/protocol/channel.go
Normal file
190
cmd/protbot/protocol/channel.go
Normal file
@@ -0,0 +1,190 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user