mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-05-06 14:24:15 +02:00
Adds non-destructive test scenarios for the #187 boost-time fix and the #175 / gacha-logging changes so regressions in those paths can be caught without a full game client. - boost: queries GET_BOOST_TIME_LIMIT, GET_BOOST_RIGHT, and GET_KEEP_LOGIN_BOOST_STATUS, flagging a zero boost_limit and all-zero login boost entries as the expected DisableBoostTime/DisableLoginBoost state. - gacha: snapshots GET_GACHA_POINT and RECEIVE_GACHA_ITEM (freeze=true, so temp storage is not cleared), with an opt-in --roll flag that exercises PLAY_NORMAL_GACHA end-to-end. Detects the post-#175 single-byte validation-failure ACK.
361 lines
11 KiB
Go
361 lines
11 KiB
Go
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()
|
|
}
|
|
|
|
// BuildGetAchievementPacket builds a MSG_MHF_GET_ACHIEVEMENT packet.
|
|
// Layout mirrors Erupe's MsgMhfGetAchievement.Parse:
|
|
//
|
|
// uint16 opcode
|
|
// uint32 ackHandle
|
|
// uint32 charID
|
|
// uint32 zeroed
|
|
// 0x00 0x10 terminator
|
|
func BuildGetAchievementPacket(ackHandle, charID uint32) []byte {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint16(MSG_MHF_GET_ACHIEVEMENT)
|
|
bf.WriteUint32(ackHandle)
|
|
bf.WriteUint32(charID)
|
|
bf.WriteUint32(0) // Zeroed
|
|
bf.WriteBytes([]byte{0x00, 0x10})
|
|
return bf.Data()
|
|
}
|
|
|
|
// BuildAddAchievementPacket builds a MSG_MHF_ADD_ACHIEVEMENT packet (fire-and-forget, no ACK).
|
|
// Layout mirrors Erupe's MsgMhfAddAchievement.Parse:
|
|
//
|
|
// uint16 opcode
|
|
// uint8 achievementID
|
|
// uint16 unk1
|
|
// uint16 unk2
|
|
// 0x00 0x10 terminator
|
|
func BuildAddAchievementPacket(achievementID uint8) []byte {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint16(MSG_MHF_ADD_ACHIEVEMENT)
|
|
bf.WriteUint8(achievementID)
|
|
bf.WriteUint16(0) // Unk1
|
|
bf.WriteUint16(0) // Unk2
|
|
bf.WriteBytes([]byte{0x00, 0x10})
|
|
return bf.Data()
|
|
}
|
|
|
|
// BuildDisplayedAchievementPacket builds a MSG_MHF_DISPLAYED_ACHIEVEMENT packet (fire-and-forget).
|
|
// Layout mirrors Erupe's MsgMhfDisplayedAchievement.Parse:
|
|
//
|
|
// uint16 opcode
|
|
// uint8 zeroed
|
|
// 0x00 0x10 terminator
|
|
func BuildDisplayedAchievementPacket() []byte {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint16(MSG_MHF_DISPLAYED_ACHIEVEMENT)
|
|
bf.WriteUint8(0) // Zeroed
|
|
bf.WriteBytes([]byte{0x00, 0x10})
|
|
return bf.Data()
|
|
}
|
|
|
|
// BuildSimpleAckPacket builds a packet whose body is just an ack handle.
|
|
// Used by several trivial query packets (boost time, gacha point, ...).
|
|
//
|
|
// uint16 opcode
|
|
// uint32 ackHandle
|
|
// 0x00 0x10 terminator
|
|
func BuildSimpleAckPacket(opcode uint16, ackHandle uint32) []byte {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint16(opcode)
|
|
bf.WriteUint32(ackHandle)
|
|
bf.WriteBytes([]byte{0x00, 0x10})
|
|
return bf.Data()
|
|
}
|
|
|
|
// BuildStartBoostTimePacket builds a MSG_MHF_START_BOOST_TIME packet.
|
|
func BuildStartBoostTimePacket(ackHandle uint32) []byte {
|
|
return BuildSimpleAckPacket(MSG_MHF_START_BOOST_TIME, ackHandle)
|
|
}
|
|
|
|
// BuildGetBoostTimeLimitPacket builds a MSG_MHF_GET_BOOST_TIME_LIMIT packet.
|
|
func BuildGetBoostTimeLimitPacket(ackHandle uint32) []byte {
|
|
return BuildSimpleAckPacket(MSG_MHF_GET_BOOST_TIME_LIMIT, ackHandle)
|
|
}
|
|
|
|
// BuildGetBoostRightPacket builds a MSG_MHF_GET_BOOST_RIGHT packet.
|
|
func BuildGetBoostRightPacket(ackHandle uint32) []byte {
|
|
return BuildSimpleAckPacket(MSG_MHF_GET_BOOST_RIGHT, ackHandle)
|
|
}
|
|
|
|
// BuildGetKeepLoginBoostStatusPacket builds a MSG_MHF_GET_KEEP_LOGIN_BOOST_STATUS packet.
|
|
func BuildGetKeepLoginBoostStatusPacket(ackHandle uint32) []byte {
|
|
return BuildSimpleAckPacket(MSG_MHF_GET_KEEP_LOGIN_BOOST_STATUS, ackHandle)
|
|
}
|
|
|
|
// BuildGetGachaPointPacket builds a MSG_MHF_GET_GACHA_POINT packet.
|
|
func BuildGetGachaPointPacket(ackHandle uint32) []byte {
|
|
return BuildSimpleAckPacket(MSG_MHF_GET_GACHA_POINT, ackHandle)
|
|
}
|
|
|
|
// BuildPlayNormalGachaPacket builds a MSG_MHF_PLAY_NORMAL_GACHA packet.
|
|
// Layout mirrors Erupe's MsgMhfPlayNormalGacha.Parse:
|
|
//
|
|
// uint16 opcode
|
|
// uint32 ackHandle
|
|
// uint32 gachaID
|
|
// uint8 rollType (0=single, 1=ten-pull)
|
|
// uint8 gachaType
|
|
// 0x00 0x10 terminator
|
|
func BuildPlayNormalGachaPacket(ackHandle, gachaID uint32, rollType, gachaType uint8) []byte {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint16(MSG_MHF_PLAY_NORMAL_GACHA)
|
|
bf.WriteUint32(ackHandle)
|
|
bf.WriteUint32(gachaID)
|
|
bf.WriteUint8(rollType)
|
|
bf.WriteUint8(gachaType)
|
|
bf.WriteBytes([]byte{0x00, 0x10})
|
|
return bf.Data()
|
|
}
|
|
|
|
// BuildReceiveGachaItemPacket builds a MSG_MHF_RECEIVE_GACHA_ITEM packet.
|
|
// Layout mirrors Erupe's MsgMhfReceiveGachaItem.Parse:
|
|
//
|
|
// uint16 opcode
|
|
// uint32 ackHandle
|
|
// uint8 max
|
|
// uint8 freeze (bool; if non-zero, server does not clear the stored items)
|
|
// 0x00 0x10 terminator
|
|
func BuildReceiveGachaItemPacket(ackHandle uint32, max uint8, freeze bool) []byte {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint16(MSG_MHF_RECEIVE_GACHA_ITEM)
|
|
bf.WriteUint32(ackHandle)
|
|
bf.WriteUint8(max)
|
|
if freeze {
|
|
bf.WriteUint8(1)
|
|
} else {
|
|
bf.WriteUint8(0)
|
|
}
|
|
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()
|
|
}
|