Files
Erupe/cmd/protbot/scenario/boost.go
Houmgaor 3e9f3d1b62 feat(protbot): add boost and gacha inspection scenarios
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.
2026-04-06 17:58:04 +02:00

114 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package scenario
import (
"fmt"
"time"
"erupe-ce/cmd/protbot/protocol"
"erupe-ce/common/byteframe"
)
// BoostTimeStatus holds the parsed response of MSG_MHF_GET_BOOST_TIME_LIMIT.
// When boost time is disabled server-side, or has not been started yet,
// BoostLimitUnix is 0. Prior to the #187 fix, unset boost_time columns
// wrapped to a large uint32 the client interpreted as permanently active.
type BoostTimeStatus struct {
BoostLimitUnix uint32
}
// BoostRight holds the parsed response of MSG_MHF_GET_BOOST_RIGHT.
// 0 = disabled, 1 = active, 2 = available.
type BoostRight struct {
State uint32
}
// LoginBoostEntry holds a single entry of the 5-entry MSG_MHF_GET_KEEP_LOGIN_BOOST_STATUS response.
type LoginBoostEntry struct {
WeekReq uint8
Active bool
WeekCount uint8
Expiration uint32
}
// LoginBoostStatus holds the full parsed keep login boost status response.
type LoginBoostStatus struct {
Entries []LoginBoostEntry
}
// GetBoostTimeLimit sends MSG_MHF_GET_BOOST_TIME_LIMIT and parses the response.
func GetBoostTimeLimit(ch *protocol.ChannelConn) (*BoostTimeStatus, error) {
ack := ch.NextAckHandle()
pkt := protocol.BuildGetBoostTimeLimitPacket(ack)
fmt.Printf("[boost] Sending GET_BOOST_TIME_LIMIT (ackHandle=%d)...\n", ack)
if err := ch.SendPacket(pkt); err != nil {
return nil, fmt.Errorf("get boost time limit send: %w", err)
}
resp, err := ch.WaitForAck(ack, 10*time.Second)
if err != nil {
return nil, fmt.Errorf("get boost time limit ack: %w", err)
}
if resp.ErrorCode != 0 {
return nil, fmt.Errorf("get boost time limit failed: error code %d", resp.ErrorCode)
}
if len(resp.Data) < 4 {
return nil, fmt.Errorf("get boost time limit response too short: %d bytes", len(resp.Data))
}
bf := byteframe.NewByteFrameFromBytes(resp.Data)
return &BoostTimeStatus{BoostLimitUnix: bf.ReadUint32()}, nil
}
// GetBoostRight sends MSG_MHF_GET_BOOST_RIGHT and parses the response.
func GetBoostRight(ch *protocol.ChannelConn) (*BoostRight, error) {
ack := ch.NextAckHandle()
pkt := protocol.BuildGetBoostRightPacket(ack)
fmt.Printf("[boost] Sending GET_BOOST_RIGHT (ackHandle=%d)...\n", ack)
if err := ch.SendPacket(pkt); err != nil {
return nil, fmt.Errorf("get boost right send: %w", err)
}
resp, err := ch.WaitForAck(ack, 10*time.Second)
if err != nil {
return nil, fmt.Errorf("get boost right ack: %w", err)
}
if resp.ErrorCode != 0 {
return nil, fmt.Errorf("get boost right failed: error code %d", resp.ErrorCode)
}
if len(resp.Data) < 4 {
return nil, fmt.Errorf("get boost right response too short: %d bytes", len(resp.Data))
}
bf := byteframe.NewByteFrameFromBytes(resp.Data)
return &BoostRight{State: bf.ReadUint32()}, nil
}
// GetKeepLoginBoostStatus sends MSG_MHF_GET_KEEP_LOGIN_BOOST_STATUS and parses the response.
// The server returns either 35 bytes (5 entries × 7 bytes) or 35 zero bytes
// when DisableLoginBoost is set.
func GetKeepLoginBoostStatus(ch *protocol.ChannelConn) (*LoginBoostStatus, error) {
ack := ch.NextAckHandle()
pkt := protocol.BuildGetKeepLoginBoostStatusPacket(ack)
fmt.Printf("[boost] Sending GET_KEEP_LOGIN_BOOST_STATUS (ackHandle=%d)...\n", ack)
if err := ch.SendPacket(pkt); err != nil {
return nil, fmt.Errorf("get login boost status send: %w", err)
}
resp, err := ch.WaitForAck(ack, 10*time.Second)
if err != nil {
return nil, fmt.Errorf("get login boost status ack: %w", err)
}
if resp.ErrorCode != 0 {
return nil, fmt.Errorf("get login boost status failed: error code %d", resp.ErrorCode)
}
if len(resp.Data) < 35 {
return nil, fmt.Errorf("login boost status response too short: %d bytes", len(resp.Data))
}
bf := byteframe.NewByteFrameFromBytes(resp.Data)
status := &LoginBoostStatus{}
for i := 0; i < 5; i++ {
status.Entries = append(status.Entries, LoginBoostEntry{
WeekReq: bf.ReadUint8(),
Active: bf.ReadBool(),
WeekCount: bf.ReadUint8(),
Expiration: bf.ReadUint32(),
})
}
return status, nil
}