mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-25 17:12:52 +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)
|
||||
}
|
||||
}
|
||||
142
cmd/protbot/protocol/entrance.go
Normal file
142
cmd/protbot/protocol/entrance.go
Normal file
@@ -0,0 +1,142 @@
|
||||
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
|
||||
}
|
||||
23
cmd/protbot/protocol/opcodes.go
Normal file
23
cmd/protbot/protocol/opcodes.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Package protocol implements MHF network protocol message building and parsing.
|
||||
package protocol
|
||||
|
||||
// Packet opcodes (subset from Erupe's network/packetid.go iota).
|
||||
const (
|
||||
MSG_SYS_ACK uint16 = 0x0012
|
||||
MSG_SYS_LOGIN uint16 = 0x0014
|
||||
MSG_SYS_LOGOUT uint16 = 0x0015
|
||||
MSG_SYS_PING uint16 = 0x0017
|
||||
MSG_SYS_CAST_BINARY uint16 = 0x0018
|
||||
MSG_SYS_TIME uint16 = 0x001A
|
||||
MSG_SYS_CASTED_BINARY uint16 = 0x001B
|
||||
MSG_SYS_ISSUE_LOGKEY uint16 = 0x001D
|
||||
MSG_SYS_ENTER_STAGE uint16 = 0x0022
|
||||
MSG_SYS_ENUMERATE_STAGE uint16 = 0x002F
|
||||
MSG_SYS_INSERT_USER uint16 = 0x0050
|
||||
MSG_SYS_DELETE_USER uint16 = 0x0051
|
||||
MSG_SYS_UPDATE_RIGHT uint16 = 0x0058
|
||||
MSG_SYS_RIGHTS_RELOAD uint16 = 0x005D
|
||||
MSG_MHF_LOADDATA uint16 = 0x0061
|
||||
MSG_MHF_ENUMERATE_QUEST uint16 = 0x009F
|
||||
MSG_MHF_GET_WEEKLY_SCHED uint16 = 0x00E1
|
||||
)
|
||||
229
cmd/protbot/protocol/packets.go
Normal file
229
cmd/protbot/protocol/packets.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"erupe-ce/common/byteframe"
|
||||
"erupe-ce/common/stringsupport"
|
||||
)
|
||||
|
||||
// BuildLoginPacket builds a MSG_SYS_LOGIN packet.
|
||||
// Layout mirrors Erupe's MsgSysLogin.Parse:
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint32 ackHandle
|
||||
// uint32 charID
|
||||
// uint32 loginTokenNumber
|
||||
// uint16 hardcodedZero
|
||||
// uint16 requestVersion (set to 0xCAFE as dummy)
|
||||
// uint32 charID (repeated)
|
||||
// uint16 zeroed
|
||||
// uint16 always 11
|
||||
// null-terminated tokenString
|
||||
// 0x00 0x10 terminator
|
||||
func BuildLoginPacket(ackHandle, charID, tokenNumber uint32, tokenString string) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_SYS_LOGIN)
|
||||
bf.WriteUint32(ackHandle)
|
||||
bf.WriteUint32(charID)
|
||||
bf.WriteUint32(tokenNumber)
|
||||
bf.WriteUint16(0) // HardcodedZero0
|
||||
bf.WriteUint16(0xCAFE) // RequestVersion (dummy)
|
||||
bf.WriteUint32(charID) // CharID1 (repeated)
|
||||
bf.WriteUint16(0) // Zeroed
|
||||
bf.WriteUint16(11) // Always 11
|
||||
bf.WriteNullTerminatedBytes([]byte(tokenString))
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildEnumerateStagePacket builds a MSG_SYS_ENUMERATE_STAGE packet.
|
||||
// Layout mirrors Erupe's MsgSysEnumerateStage.Parse:
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint32 ackHandle
|
||||
// uint8 always 1
|
||||
// uint8 prefix length (including null terminator)
|
||||
// null-terminated stagePrefix
|
||||
// 0x00 0x10 terminator
|
||||
func BuildEnumerateStagePacket(ackHandle uint32, prefix string) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_SYS_ENUMERATE_STAGE)
|
||||
bf.WriteUint32(ackHandle)
|
||||
bf.WriteUint8(1) // Always 1
|
||||
bf.WriteUint8(uint8(len(prefix) + 1)) // Length including null terminator
|
||||
bf.WriteNullTerminatedBytes([]byte(prefix))
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildEnterStagePacket builds a MSG_SYS_ENTER_STAGE packet.
|
||||
// Layout mirrors Erupe's MsgSysEnterStage.Parse:
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint32 ackHandle
|
||||
// uint8 isQuest (0=false)
|
||||
// uint8 stageID length (including null terminator)
|
||||
// null-terminated stageID
|
||||
// 0x00 0x10 terminator
|
||||
func BuildEnterStagePacket(ackHandle uint32, stageID string) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_SYS_ENTER_STAGE)
|
||||
bf.WriteUint32(ackHandle)
|
||||
bf.WriteUint8(0) // IsQuest = false
|
||||
bf.WriteUint8(uint8(len(stageID) + 1)) // Length including null terminator
|
||||
bf.WriteNullTerminatedBytes([]byte(stageID))
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildPingPacket builds a MSG_SYS_PING response packet.
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint32 ackHandle
|
||||
// 0x00 0x10 terminator
|
||||
func BuildPingPacket(ackHandle uint32) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_SYS_PING)
|
||||
bf.WriteUint32(ackHandle)
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildLogoutPacket builds a MSG_SYS_LOGOUT packet.
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint8 logoutType (1 = normal logout)
|
||||
// 0x00 0x10 terminator
|
||||
func BuildLogoutPacket() []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_SYS_LOGOUT)
|
||||
bf.WriteUint8(1) // LogoutType = normal
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildIssueLogkeyPacket builds a MSG_SYS_ISSUE_LOGKEY packet.
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint32 ackHandle
|
||||
// uint16 unk0
|
||||
// uint16 unk1
|
||||
// 0x00 0x10 terminator
|
||||
func BuildIssueLogkeyPacket(ackHandle uint32) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_SYS_ISSUE_LOGKEY)
|
||||
bf.WriteUint32(ackHandle)
|
||||
bf.WriteUint16(0)
|
||||
bf.WriteUint16(0)
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildRightsReloadPacket builds a MSG_SYS_RIGHTS_RELOAD packet.
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint32 ackHandle
|
||||
// uint8 count (0 = empty)
|
||||
// 0x00 0x10 terminator
|
||||
func BuildRightsReloadPacket(ackHandle uint32) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_SYS_RIGHTS_RELOAD)
|
||||
bf.WriteUint32(ackHandle)
|
||||
bf.WriteUint8(0) // Count = 0 (no rights entries)
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildLoaddataPacket builds a MSG_MHF_LOADDATA packet.
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint32 ackHandle
|
||||
// 0x00 0x10 terminator
|
||||
func BuildLoaddataPacket(ackHandle uint32) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_MHF_LOADDATA)
|
||||
bf.WriteUint32(ackHandle)
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildCastBinaryPacket builds a MSG_SYS_CAST_BINARY packet.
|
||||
// Layout mirrors Erupe's MsgSysCastBinary.Parse:
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint32 unk (always 0)
|
||||
// uint8 broadcastType
|
||||
// uint8 messageType
|
||||
// uint16 dataSize
|
||||
// []byte payload
|
||||
// 0x00 0x10 terminator
|
||||
func BuildCastBinaryPacket(broadcastType, messageType uint8, payload []byte) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_SYS_CAST_BINARY)
|
||||
bf.WriteUint32(0) // Unk
|
||||
bf.WriteUint8(broadcastType)
|
||||
bf.WriteUint8(messageType)
|
||||
bf.WriteUint16(uint16(len(payload)))
|
||||
bf.WriteBytes(payload)
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildChatPayload builds the inner MsgBinChat binary blob for use with BuildCastBinaryPacket.
|
||||
// Layout mirrors Erupe's binpacket/msg_bin_chat.go Build:
|
||||
//
|
||||
// uint8 unk0 (always 0)
|
||||
// uint8 chatType
|
||||
// uint16 flags (always 0)
|
||||
// uint16 senderNameLen (SJIS bytes + null terminator)
|
||||
// uint16 messageLen (SJIS bytes + null terminator)
|
||||
// null-terminated SJIS message
|
||||
// null-terminated SJIS senderName
|
||||
func BuildChatPayload(chatType uint8, message, senderName string) []byte {
|
||||
sjisMsg := stringsupport.UTF8ToSJIS(message)
|
||||
sjisName := stringsupport.UTF8ToSJIS(senderName)
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint8(0) // Unk0
|
||||
bf.WriteUint8(chatType) // Type
|
||||
bf.WriteUint16(0) // Flags
|
||||
bf.WriteUint16(uint16(len(sjisName) + 1)) // SenderName length (+ null term)
|
||||
bf.WriteUint16(uint16(len(sjisMsg) + 1)) // Message length (+ null term)
|
||||
bf.WriteNullTerminatedBytes(sjisMsg) // Message
|
||||
bf.WriteNullTerminatedBytes(sjisName) // SenderName
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildEnumerateQuestPacket builds a MSG_MHF_ENUMERATE_QUEST packet.
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint32 ackHandle
|
||||
// uint8 unk0 (always 0)
|
||||
// uint8 world
|
||||
// uint16 counter
|
||||
// uint16 offset
|
||||
// uint8 unk1 (always 0)
|
||||
// 0x00 0x10 terminator
|
||||
func BuildEnumerateQuestPacket(ackHandle uint32, world uint8, counter, offset uint16) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_MHF_ENUMERATE_QUEST)
|
||||
bf.WriteUint32(ackHandle)
|
||||
bf.WriteUint8(0) // Unk0
|
||||
bf.WriteUint8(world)
|
||||
bf.WriteUint16(counter)
|
||||
bf.WriteUint16(offset)
|
||||
bf.WriteUint8(0) // Unk1
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
// BuildGetWeeklySchedulePacket builds a MSG_MHF_GET_WEEKLY_SCHEDULE packet.
|
||||
//
|
||||
// uint16 opcode
|
||||
// uint32 ackHandle
|
||||
// 0x00 0x10 terminator
|
||||
func BuildGetWeeklySchedulePacket(ackHandle uint32) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(MSG_MHF_GET_WEEKLY_SCHED)
|
||||
bf.WriteUint32(ackHandle)
|
||||
bf.WriteBytes([]byte{0x00, 0x10})
|
||||
return bf.Data()
|
||||
}
|
||||
412
cmd/protbot/protocol/packets_test.go
Normal file
412
cmd/protbot/protocol/packets_test.go
Normal file
@@ -0,0 +1,412 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"erupe-ce/common/byteframe"
|
||||
)
|
||||
|
||||
// TestBuildLoginPacket verifies that the binary layout matches Erupe's Parse.
|
||||
func TestBuildLoginPacket(t *testing.T) {
|
||||
ackHandle := uint32(1)
|
||||
charID := uint32(100)
|
||||
tokenNumber := uint32(42)
|
||||
tokenString := "0123456789ABCDEF"
|
||||
|
||||
pkt := BuildLoginPacket(ackHandle, charID, tokenNumber, tokenString)
|
||||
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
opcode := bf.ReadUint16()
|
||||
if opcode != MSG_SYS_LOGIN {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", opcode, MSG_SYS_LOGIN)
|
||||
}
|
||||
|
||||
gotAck := bf.ReadUint32()
|
||||
if gotAck != ackHandle {
|
||||
t.Fatalf("ackHandle: got %d, want %d", gotAck, ackHandle)
|
||||
}
|
||||
|
||||
gotCharID0 := bf.ReadUint32()
|
||||
if gotCharID0 != charID {
|
||||
t.Fatalf("charID0: got %d, want %d", gotCharID0, charID)
|
||||
}
|
||||
|
||||
gotTokenNum := bf.ReadUint32()
|
||||
if gotTokenNum != tokenNumber {
|
||||
t.Fatalf("tokenNumber: got %d, want %d", gotTokenNum, tokenNumber)
|
||||
}
|
||||
|
||||
gotZero := bf.ReadUint16()
|
||||
if gotZero != 0 {
|
||||
t.Fatalf("hardcodedZero: got %d, want 0", gotZero)
|
||||
}
|
||||
|
||||
gotVersion := bf.ReadUint16()
|
||||
if gotVersion != 0xCAFE {
|
||||
t.Fatalf("requestVersion: got 0x%04X, want 0xCAFE", gotVersion)
|
||||
}
|
||||
|
||||
gotCharID1 := bf.ReadUint32()
|
||||
if gotCharID1 != charID {
|
||||
t.Fatalf("charID1: got %d, want %d", gotCharID1, charID)
|
||||
}
|
||||
|
||||
gotZeroed := bf.ReadUint16()
|
||||
if gotZeroed != 0 {
|
||||
t.Fatalf("zeroed: got %d, want 0", gotZeroed)
|
||||
}
|
||||
|
||||
gotEleven := bf.ReadUint16()
|
||||
if gotEleven != 11 {
|
||||
t.Fatalf("always11: got %d, want 11", gotEleven)
|
||||
}
|
||||
|
||||
gotToken := string(bf.ReadNullTerminatedBytes())
|
||||
if gotToken != tokenString {
|
||||
t.Fatalf("tokenString: got %q, want %q", gotToken, tokenString)
|
||||
}
|
||||
|
||||
// Verify terminator.
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildEnumerateStagePacket verifies binary layout matches Erupe's Parse.
|
||||
func TestBuildEnumerateStagePacket(t *testing.T) {
|
||||
ackHandle := uint32(5)
|
||||
prefix := "sl1Ns"
|
||||
|
||||
pkt := BuildEnumerateStagePacket(ackHandle, prefix)
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
opcode := bf.ReadUint16()
|
||||
if opcode != MSG_SYS_ENUMERATE_STAGE {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", opcode, MSG_SYS_ENUMERATE_STAGE)
|
||||
}
|
||||
|
||||
gotAck := bf.ReadUint32()
|
||||
if gotAck != ackHandle {
|
||||
t.Fatalf("ackHandle: got %d, want %d", gotAck, ackHandle)
|
||||
}
|
||||
|
||||
alwaysOne := bf.ReadUint8()
|
||||
if alwaysOne != 1 {
|
||||
t.Fatalf("alwaysOne: got %d, want 1", alwaysOne)
|
||||
}
|
||||
|
||||
prefixLen := bf.ReadUint8()
|
||||
if prefixLen != uint8(len(prefix)+1) {
|
||||
t.Fatalf("prefixLen: got %d, want %d", prefixLen, len(prefix)+1)
|
||||
}
|
||||
|
||||
gotPrefix := string(bf.ReadNullTerminatedBytes())
|
||||
if gotPrefix != prefix {
|
||||
t.Fatalf("prefix: got %q, want %q", gotPrefix, prefix)
|
||||
}
|
||||
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildEnterStagePacket verifies binary layout matches Erupe's Parse.
|
||||
func TestBuildEnterStagePacket(t *testing.T) {
|
||||
ackHandle := uint32(7)
|
||||
stageID := "sl1Ns200p0a0u0"
|
||||
|
||||
pkt := BuildEnterStagePacket(ackHandle, stageID)
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
opcode := bf.ReadUint16()
|
||||
if opcode != MSG_SYS_ENTER_STAGE {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", opcode, MSG_SYS_ENTER_STAGE)
|
||||
}
|
||||
|
||||
gotAck := bf.ReadUint32()
|
||||
if gotAck != ackHandle {
|
||||
t.Fatalf("ackHandle: got %d, want %d", gotAck, ackHandle)
|
||||
}
|
||||
|
||||
isQuest := bf.ReadUint8()
|
||||
if isQuest != 0 {
|
||||
t.Fatalf("isQuest: got %d, want 0", isQuest)
|
||||
}
|
||||
|
||||
stageLen := bf.ReadUint8()
|
||||
if stageLen != uint8(len(stageID)+1) {
|
||||
t.Fatalf("stageLen: got %d, want %d", stageLen, len(stageID)+1)
|
||||
}
|
||||
|
||||
gotStage := string(bf.ReadNullTerminatedBytes())
|
||||
if gotStage != stageID {
|
||||
t.Fatalf("stageID: got %q, want %q", gotStage, stageID)
|
||||
}
|
||||
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildPingPacket verifies MSG_SYS_PING binary layout.
|
||||
func TestBuildPingPacket(t *testing.T) {
|
||||
ackHandle := uint32(99)
|
||||
pkt := BuildPingPacket(ackHandle)
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
if op := bf.ReadUint16(); op != MSG_SYS_PING {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", op, MSG_SYS_PING)
|
||||
}
|
||||
if ack := bf.ReadUint32(); ack != ackHandle {
|
||||
t.Fatalf("ackHandle: got %d, want %d", ack, ackHandle)
|
||||
}
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildLogoutPacket verifies MSG_SYS_LOGOUT binary layout.
|
||||
func TestBuildLogoutPacket(t *testing.T) {
|
||||
pkt := BuildLogoutPacket()
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
if op := bf.ReadUint16(); op != MSG_SYS_LOGOUT {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", op, MSG_SYS_LOGOUT)
|
||||
}
|
||||
if lt := bf.ReadUint8(); lt != 1 {
|
||||
t.Fatalf("logoutType: got %d, want 1", lt)
|
||||
}
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildIssueLogkeyPacket verifies MSG_SYS_ISSUE_LOGKEY binary layout.
|
||||
func TestBuildIssueLogkeyPacket(t *testing.T) {
|
||||
ackHandle := uint32(10)
|
||||
pkt := BuildIssueLogkeyPacket(ackHandle)
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
if op := bf.ReadUint16(); op != MSG_SYS_ISSUE_LOGKEY {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", op, MSG_SYS_ISSUE_LOGKEY)
|
||||
}
|
||||
if ack := bf.ReadUint32(); ack != ackHandle {
|
||||
t.Fatalf("ackHandle: got %d, want %d", ack, ackHandle)
|
||||
}
|
||||
if v := bf.ReadUint16(); v != 0 {
|
||||
t.Fatalf("unk0: got %d, want 0", v)
|
||||
}
|
||||
if v := bf.ReadUint16(); v != 0 {
|
||||
t.Fatalf("unk1: got %d, want 0", v)
|
||||
}
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildRightsReloadPacket verifies MSG_SYS_RIGHTS_RELOAD binary layout.
|
||||
func TestBuildRightsReloadPacket(t *testing.T) {
|
||||
ackHandle := uint32(20)
|
||||
pkt := BuildRightsReloadPacket(ackHandle)
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
if op := bf.ReadUint16(); op != MSG_SYS_RIGHTS_RELOAD {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", op, MSG_SYS_RIGHTS_RELOAD)
|
||||
}
|
||||
if ack := bf.ReadUint32(); ack != ackHandle {
|
||||
t.Fatalf("ackHandle: got %d, want %d", ack, ackHandle)
|
||||
}
|
||||
if c := bf.ReadUint8(); c != 0 {
|
||||
t.Fatalf("count: got %d, want 0", c)
|
||||
}
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildLoaddataPacket verifies MSG_MHF_LOADDATA binary layout.
|
||||
func TestBuildLoaddataPacket(t *testing.T) {
|
||||
ackHandle := uint32(30)
|
||||
pkt := BuildLoaddataPacket(ackHandle)
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
if op := bf.ReadUint16(); op != MSG_MHF_LOADDATA {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", op, MSG_MHF_LOADDATA)
|
||||
}
|
||||
if ack := bf.ReadUint32(); ack != ackHandle {
|
||||
t.Fatalf("ackHandle: got %d, want %d", ack, ackHandle)
|
||||
}
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildCastBinaryPacket verifies MSG_SYS_CAST_BINARY binary layout.
|
||||
func TestBuildCastBinaryPacket(t *testing.T) {
|
||||
payload := []byte{0xDE, 0xAD, 0xBE, 0xEF}
|
||||
pkt := BuildCastBinaryPacket(0x03, 1, payload)
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
if op := bf.ReadUint16(); op != MSG_SYS_CAST_BINARY {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", op, MSG_SYS_CAST_BINARY)
|
||||
}
|
||||
if unk := bf.ReadUint32(); unk != 0 {
|
||||
t.Fatalf("unk: got %d, want 0", unk)
|
||||
}
|
||||
if bt := bf.ReadUint8(); bt != 0x03 {
|
||||
t.Fatalf("broadcastType: got %d, want 3", bt)
|
||||
}
|
||||
if mt := bf.ReadUint8(); mt != 1 {
|
||||
t.Fatalf("messageType: got %d, want 1", mt)
|
||||
}
|
||||
if ds := bf.ReadUint16(); ds != uint16(len(payload)) {
|
||||
t.Fatalf("dataSize: got %d, want %d", ds, len(payload))
|
||||
}
|
||||
gotPayload := bf.ReadBytes(uint(len(payload)))
|
||||
for i, b := range payload {
|
||||
if gotPayload[i] != b {
|
||||
t.Fatalf("payload[%d]: got 0x%02X, want 0x%02X", i, gotPayload[i], b)
|
||||
}
|
||||
}
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildChatPayload verifies the MsgBinChat inner binary layout and SJIS encoding.
|
||||
func TestBuildChatPayload(t *testing.T) {
|
||||
chatType := uint8(1)
|
||||
message := "Hello"
|
||||
senderName := "TestUser"
|
||||
|
||||
payload := BuildChatPayload(chatType, message, senderName)
|
||||
bf := byteframe.NewByteFrameFromBytes(payload)
|
||||
|
||||
if unk := bf.ReadUint8(); unk != 0 {
|
||||
t.Fatalf("unk0: got %d, want 0", unk)
|
||||
}
|
||||
if ct := bf.ReadUint8(); ct != chatType {
|
||||
t.Fatalf("chatType: got %d, want %d", ct, chatType)
|
||||
}
|
||||
if flags := bf.ReadUint16(); flags != 0 {
|
||||
t.Fatalf("flags: got %d, want 0", flags)
|
||||
}
|
||||
nameLen := bf.ReadUint16()
|
||||
msgLen := bf.ReadUint16()
|
||||
// "Hello" in ASCII/SJIS = 5 bytes + 1 null = 6
|
||||
if msgLen != 6 {
|
||||
t.Fatalf("messageLen: got %d, want 6", msgLen)
|
||||
}
|
||||
// "TestUser" in ASCII/SJIS = 8 bytes + 1 null = 9
|
||||
if nameLen != 9 {
|
||||
t.Fatalf("senderNameLen: got %d, want 9", nameLen)
|
||||
}
|
||||
|
||||
gotMsg := string(bf.ReadNullTerminatedBytes())
|
||||
if gotMsg != message {
|
||||
t.Fatalf("message: got %q, want %q", gotMsg, message)
|
||||
}
|
||||
gotName := string(bf.ReadNullTerminatedBytes())
|
||||
if gotName != senderName {
|
||||
t.Fatalf("senderName: got %q, want %q", gotName, senderName)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildEnumerateQuestPacket verifies MSG_MHF_ENUMERATE_QUEST binary layout.
|
||||
func TestBuildEnumerateQuestPacket(t *testing.T) {
|
||||
ackHandle := uint32(40)
|
||||
world := uint8(2)
|
||||
counter := uint16(100)
|
||||
offset := uint16(50)
|
||||
|
||||
pkt := BuildEnumerateQuestPacket(ackHandle, world, counter, offset)
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
if op := bf.ReadUint16(); op != MSG_MHF_ENUMERATE_QUEST {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", op, MSG_MHF_ENUMERATE_QUEST)
|
||||
}
|
||||
if ack := bf.ReadUint32(); ack != ackHandle {
|
||||
t.Fatalf("ackHandle: got %d, want %d", ack, ackHandle)
|
||||
}
|
||||
if u0 := bf.ReadUint8(); u0 != 0 {
|
||||
t.Fatalf("unk0: got %d, want 0", u0)
|
||||
}
|
||||
if w := bf.ReadUint8(); w != world {
|
||||
t.Fatalf("world: got %d, want %d", w, world)
|
||||
}
|
||||
if c := bf.ReadUint16(); c != counter {
|
||||
t.Fatalf("counter: got %d, want %d", c, counter)
|
||||
}
|
||||
if o := bf.ReadUint16(); o != offset {
|
||||
t.Fatalf("offset: got %d, want %d", o, offset)
|
||||
}
|
||||
if u1 := bf.ReadUint8(); u1 != 0 {
|
||||
t.Fatalf("unk1: got %d, want 0", u1)
|
||||
}
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildGetWeeklySchedulePacket verifies MSG_MHF_GET_WEEKLY_SCHEDULE binary layout.
|
||||
func TestBuildGetWeeklySchedulePacket(t *testing.T) {
|
||||
ackHandle := uint32(50)
|
||||
pkt := BuildGetWeeklySchedulePacket(ackHandle)
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
|
||||
if op := bf.ReadUint16(); op != MSG_MHF_GET_WEEKLY_SCHED {
|
||||
t.Fatalf("opcode: got 0x%04X, want 0x%04X", op, MSG_MHF_GET_WEEKLY_SCHED)
|
||||
}
|
||||
if ack := bf.ReadUint32(); ack != ackHandle {
|
||||
t.Fatalf("ackHandle: got %d, want %d", ack, ackHandle)
|
||||
}
|
||||
term := bf.ReadBytes(2)
|
||||
if term[0] != 0x00 || term[1] != 0x10 {
|
||||
t.Fatalf("terminator: got %02X %02X, want 00 10", term[0], term[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpcodeValues verifies opcode constants match Erupe's iota-based enum.
|
||||
func TestOpcodeValues(t *testing.T) {
|
||||
_ = binary.BigEndian // ensure import used
|
||||
tests := []struct {
|
||||
name string
|
||||
got uint16
|
||||
want uint16
|
||||
}{
|
||||
{"MSG_SYS_ACK", MSG_SYS_ACK, 0x0012},
|
||||
{"MSG_SYS_LOGIN", MSG_SYS_LOGIN, 0x0014},
|
||||
{"MSG_SYS_LOGOUT", MSG_SYS_LOGOUT, 0x0015},
|
||||
{"MSG_SYS_PING", MSG_SYS_PING, 0x0017},
|
||||
{"MSG_SYS_CAST_BINARY", MSG_SYS_CAST_BINARY, 0x0018},
|
||||
{"MSG_SYS_TIME", MSG_SYS_TIME, 0x001A},
|
||||
{"MSG_SYS_CASTED_BINARY", MSG_SYS_CASTED_BINARY, 0x001B},
|
||||
{"MSG_SYS_ISSUE_LOGKEY", MSG_SYS_ISSUE_LOGKEY, 0x001D},
|
||||
{"MSG_SYS_ENTER_STAGE", MSG_SYS_ENTER_STAGE, 0x0022},
|
||||
{"MSG_SYS_ENUMERATE_STAGE", MSG_SYS_ENUMERATE_STAGE, 0x002F},
|
||||
{"MSG_SYS_INSERT_USER", MSG_SYS_INSERT_USER, 0x0050},
|
||||
{"MSG_SYS_DELETE_USER", MSG_SYS_DELETE_USER, 0x0051},
|
||||
{"MSG_SYS_UPDATE_RIGHT", MSG_SYS_UPDATE_RIGHT, 0x0058},
|
||||
{"MSG_SYS_RIGHTS_RELOAD", MSG_SYS_RIGHTS_RELOAD, 0x005D},
|
||||
{"MSG_MHF_LOADDATA", MSG_MHF_LOADDATA, 0x0061},
|
||||
{"MSG_MHF_ENUMERATE_QUEST", MSG_MHF_ENUMERATE_QUEST, 0x009F},
|
||||
{"MSG_MHF_GET_WEEKLY_SCHED", MSG_MHF_GET_WEEKLY_SCHED, 0x00E1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if tt.got != tt.want {
|
||||
t.Errorf("%s: got 0x%04X, want 0x%04X", tt.name, tt.got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
105
cmd/protbot/protocol/sign.go
Normal file
105
cmd/protbot/protocol/sign.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"erupe-ce/common/byteframe"
|
||||
"erupe-ce/common/stringsupport"
|
||||
|
||||
"erupe-ce/cmd/protbot/conn"
|
||||
)
|
||||
|
||||
// SignResult holds the parsed response from a successful DSGN sign-in.
|
||||
type SignResult struct {
|
||||
TokenID uint32
|
||||
TokenString string // 16 raw bytes as string
|
||||
Timestamp uint32
|
||||
EntranceAddr string
|
||||
CharIDs []uint32
|
||||
}
|
||||
|
||||
// DoSign connects to the sign server and performs a DSGN login.
|
||||
// Reference: Erupe server/signserver/session.go (handleDSGN) and dsgn_resp.go (makeSignResponse).
|
||||
func DoSign(addr, username, password string) (*SignResult, error) {
|
||||
c, err := conn.DialWithInit(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign connect: %w", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
// Build DSGN request: "DSGN:\x00" + SJIS(user) + "\x00" + SJIS(pass) + "\x00" + "\x00"
|
||||
// The server reads: null-terminated request type, null-terminated user, null-terminated pass, null-terminated unk.
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteNullTerminatedBytes([]byte("DSGN:\x00")) // reqType (server strips last 3 chars to get "DSGN:")
|
||||
bf.WriteNullTerminatedBytes(stringsupport.UTF8ToSJIS(username))
|
||||
bf.WriteNullTerminatedBytes(stringsupport.UTF8ToSJIS(password))
|
||||
bf.WriteUint8(0) // Unk null-terminated empty string
|
||||
|
||||
if err := c.SendPacket(bf.Data()); err != nil {
|
||||
return nil, fmt.Errorf("sign send: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.ReadPacket()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign recv: %w", err)
|
||||
}
|
||||
|
||||
return parseSignResponse(resp)
|
||||
}
|
||||
|
||||
// parseSignResponse parses the binary response from the sign server.
|
||||
// Reference: Erupe server/signserver/dsgn_resp.go:makeSignResponse
|
||||
func parseSignResponse(data []byte) (*SignResult, error) {
|
||||
if len(data) < 1 {
|
||||
return nil, fmt.Errorf("empty sign response")
|
||||
}
|
||||
|
||||
rbf := byteframe.NewByteFrameFromBytes(data)
|
||||
|
||||
resultCode := rbf.ReadUint8()
|
||||
if resultCode != 1 { // SIGN_SUCCESS = 1
|
||||
return nil, fmt.Errorf("sign failed with code %d", resultCode)
|
||||
}
|
||||
|
||||
patchCount := rbf.ReadUint8() // patch server count (usually 2)
|
||||
_ = rbf.ReadUint8() // entrance server count (usually 1)
|
||||
charCount := rbf.ReadUint8() // character count
|
||||
|
||||
result := &SignResult{}
|
||||
result.TokenID = rbf.ReadUint32()
|
||||
result.TokenString = string(rbf.ReadBytes(16)) // 16 raw bytes
|
||||
result.Timestamp = rbf.ReadUint32()
|
||||
|
||||
// Skip patch server URLs (pascal strings with uint8 length prefix)
|
||||
for i := uint8(0); i < patchCount; i++ {
|
||||
strLen := rbf.ReadUint8()
|
||||
_ = rbf.ReadBytes(uint(strLen))
|
||||
}
|
||||
|
||||
// Read entrance server address (pascal string with uint8 length prefix)
|
||||
entranceLen := rbf.ReadUint8()
|
||||
result.EntranceAddr = string(rbf.ReadBytes(uint(entranceLen - 1)))
|
||||
_ = rbf.ReadUint8() // null terminator
|
||||
|
||||
// Read character entries
|
||||
for i := uint8(0); i < charCount; i++ {
|
||||
charID := rbf.ReadUint32()
|
||||
result.CharIDs = append(result.CharIDs, charID)
|
||||
|
||||
_ = rbf.ReadUint16() // HR
|
||||
_ = rbf.ReadUint16() // WeaponType
|
||||
_ = rbf.ReadUint32() // LastLogin
|
||||
_ = rbf.ReadUint8() // IsFemale
|
||||
_ = rbf.ReadUint8() // IsNewCharacter
|
||||
_ = rbf.ReadUint8() // Old GR
|
||||
_ = rbf.ReadUint8() // Use uint16 GR flag
|
||||
_ = rbf.ReadBytes(16) // Character name (padded)
|
||||
_ = rbf.ReadBytes(32) // Unk desc string (padded)
|
||||
// ZZ mode: additional fields
|
||||
_ = rbf.ReadUint16() // GR
|
||||
_ = rbf.ReadUint8() // Unk
|
||||
_ = rbf.ReadUint8() // Unk
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user