服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
425 lines
20 KiB
Go
425 lines
20 KiB
Go
package dnfbridge
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"time"
|
||
|
||
"longheng.io/server/internal/modules/dnf/onlineevent"
|
||
"longheng.io/server/internal/modules/dnf/packageitem"
|
||
)
|
||
|
||
const (
|
||
// The legacy command enum names both values from the opposite packet
|
||
// direction. Keep current-EXE notification semantics local to this owner.
|
||
currentSpendTimeEventInfoMsgID uint16 = 108
|
||
currentSpendTimeProgressMsgID uint16 = 1206
|
||
currentSpendTimeEventID uint32 = 2347
|
||
// currentSpendTimeActivityClosed=true 时不写「累计在线得好礼」(2347) 的
|
||
// 基础目录行,使该活动从客户端活动列表消失。注意:**不要**改成关闭整个
|
||
// 活动目录(让 currentSpendTimeRuntimeCatalogIfAvailable 返回 nil),
|
||
// 那会让团本日期校验失效,超时空之战报「使用日期不准确」。
|
||
currentSpendTimeActivityClosed = true
|
||
currentSpendTimeSumEventID uint32 = 2348
|
||
currentJoustEventID uint32 = 2365
|
||
currentJonathanEventID uint32 = 2395
|
||
currentLukeRaidEventID uint32 = 282
|
||
currentLukeRaidOpenEventID uint16 = 2417
|
||
currentFiendWarEventID uint32 = 2449
|
||
// currentAntonRaidEventID completes the family. The client's op1206 reader
|
||
// (file 0xA713E0) dispatches 181 / 282 / 2449 onto the mode-1 / -2 / -3 window
|
||
// elements -- `cmp eax,0xB5` is the Anton arm, and the 0x991 arm jumps into
|
||
// the very same setter call -- so omitting 181 leaves Anton's weekly counters
|
||
// with no writer at all.
|
||
currentAntonRaidEventID uint32 = 181
|
||
currentGuildHongbaoEventID uint16 = 2378
|
||
currentGuildWarEventID uint16 = 2400
|
||
currentDecisionEventID uint16 = 222
|
||
currentMileageShopEventID uint16 = 266
|
||
|
||
currentSpendTimeDescriptorParamCount = 12
|
||
currentSpendTimeMaxRewardItems = 10
|
||
currentSpendTimeProgressRawSize = 0x20
|
||
currentRaidScheduleInitialUsedCount = uint32(0)
|
||
currentRaidScheduleInitialDayMask = uint32(0)
|
||
currentSpendTimeUnusedParam = math.MaxUint32
|
||
currentSpendTimeBaseCatalogValue = 1
|
||
currentSpendTimeEventInfoCodec = "current_op108_spend_time_and_all_day_joust_catalog_zlib"
|
||
currentSpendTimeEventRoute = "spend time event/0/0"
|
||
)
|
||
|
||
// buildCurrentSpendTimeEventInfoBody matches the current NoPack key-9 op108
|
||
// body consumed by sub_20457F0:
|
||
//
|
||
// u16 base_catalog_count,
|
||
// base_catalog_count * { u16 event_id, u32 value, dstr title,
|
||
// dstr description, u8 has_extended, ... },
|
||
// u8 active_count, active_count * { u16 event_id, 12 * u32 param }
|
||
//
|
||
// sub_2043290 reads a retired list only when the already-installed base table
|
||
// contains special key 9. The seven native activity-calendar rows are part of
|
||
// this same final base catalog because the current client replaces the catalog
|
||
// whenever it receives op108. They remain base-only records, though: the
|
||
// verified calendar envelope terminates with reward_count=0, and calendar IDs
|
||
// are not consumed by the twelve-parameter active-state handlers. Synthetic
|
||
// calendar active rows make the native date grid open with no entries. Only
|
||
// the six live event families below belong in op108's active-state map.
|
||
//
|
||
// Event 2037 is the native Seria-luck HUD gate. sub_1FC6210 only makes the
|
||
// BOOSTER_GAGE control visible after it finds that event in the active table;
|
||
// a later class-0/op412 update alone is therefore intentionally invisible.
|
||
// Its first active parameter carries the durable stage ceiling shared with the
|
||
// package-item state machine. The control itself retains ownership of the
|
||
// display-unit maximum and animation.
|
||
//
|
||
// The base row is required even though event 2347 owns a fixed client UI.
|
||
// sub_2045130 inserts it into the global event table; without that row,
|
||
// sub_2045E30 never places 2347 in the active-event map and sub_F3EDF0 hides
|
||
// the HUD entry. Event 2347 also requires its exact extended action route.
|
||
// sub_2036D50 maps "spend time event" to action 0x6AD, and sub_2042550 marks
|
||
// the row clickable and stores its two route arguments. Without that route the
|
||
// compact HUD entry is visible, but clicking it cannot open the fixed
|
||
// four-stage ItemReward.xui panel.
|
||
//
|
||
// Event 2347 owns params 0..N-1 as reward item IDs, param 10 as N, and param
|
||
// 11 as the total duration of all four stages. Current sub_F3FDA0 treats -1,
|
||
// not zero, as the unused reward-slot sentinel.
|
||
//
|
||
// Event 2395 is the PVF [open visible event] gate on NPC 476 (Red-tail
|
||
// Jonathan) in Central Park. The NPC's ambient voice is map-owned and can play
|
||
// while the actor remains hidden; installing this schedule-free base/active
|
||
// row makes the native town loader instantiate the NPC after the Magic Rift
|
||
// access quest has made that town content reachable.
|
||
//
|
||
// Events 282 and 2449 are the current EXE's Luke and Fiend War ESC/team-raid
|
||
// entrance gates. The client can preconnect their authoritative type-32/type-42
|
||
// channels while still hiding the cards when these events are absent from the
|
||
// installed activity table. Both use the same schedule-free base/active grammar
|
||
// as event 2365; op1206 below supplies their separate schedule state. The
|
||
// following op1303 projection supplies each current PVF account daily limit;
|
||
// this activity-table owner remains separate from raid creation and settlement.
|
||
func buildCurrentSpendTimeEventInfoBody(
|
||
rewardItemIDs []uint32,
|
||
totalStageSeconds uint32,
|
||
) ([]byte, error) {
|
||
return buildCurrentSpendTimeEventInfoBodyWithSum(rewardItemIDs, nil, totalStageSeconds)
|
||
}
|
||
|
||
func buildCurrentSpendTimeEventInfoBodyWithSum(
|
||
rewardItemIDs []uint32,
|
||
sumRewardItemIDs []uint32,
|
||
totalStageSeconds uint32,
|
||
) ([]byte, error) {
|
||
return buildCurrentOnlineEventInfoBody(rewardItemIDs, sumRewardItemIDs, totalStageSeconds, false)
|
||
}
|
||
|
||
func buildCurrentOnlineEventInfoBody(rewardItemIDs, sumRewardItemIDs []uint32, totalStageSeconds uint32, nativePCRoom bool) ([]byte, error) {
|
||
return buildCurrentOnlineEventInfoBodyAt(rewardItemIDs, sumRewardItemIDs, totalStageSeconds, nativePCRoom, nil, time.Time{})
|
||
}
|
||
|
||
func buildCurrentOnlineEventInfoBodyAt(rewardItemIDs, sumRewardItemIDs []uint32, totalStageSeconds uint32, nativePCRoom bool, labor *onlineevent.LaborDayCatalog, now time.Time) ([]byte, error) {
|
||
laborActive := labor != nil && labor.Policy.ActiveAt(now)
|
||
laborBase := laborActive || (labor == nil && len(sumRewardItemIDs) > 0)
|
||
if len(rewardItemIDs) == 0 || len(rewardItemIDs) > currentSpendTimeMaxRewardItems {
|
||
return nil, fmt.Errorf("current spend-time reward count=%d is outside 1..%d", len(rewardItemIDs), currentSpendTimeMaxRewardItems)
|
||
}
|
||
if totalStageSeconds == 0 {
|
||
return nil, fmt.Errorf("current spend-time total stage seconds is zero")
|
||
}
|
||
params := [currentSpendTimeDescriptorParamCount]uint32{}
|
||
for index := 0; index < currentSpendTimeMaxRewardItems; index++ {
|
||
params[index] = currentSpendTimeUnusedParam
|
||
}
|
||
for index, itemID := range rewardItemIDs {
|
||
if itemID == 0 {
|
||
return nil, fmt.Errorf("current spend-time reward[%d] item id is zero", index)
|
||
}
|
||
params[index] = itemID
|
||
}
|
||
params[10] = uint32(len(rewardItemIDs))
|
||
params[11] = totalStageSeconds
|
||
|
||
var writer packetWriter
|
||
calendarRecords := currentActivityCalendarRecords()
|
||
// Event 9 is the native activity-table anchor. Event 181 is the Anton team
|
||
// tab; the client gates that tab with the completed q12829 bit from op356.
|
||
// Both rows must be present in the final post-op356 catalog before op1206
|
||
// and op1303 are replayed.
|
||
baseRows := len(calendarRecords) + 15
|
||
if laborBase {
|
||
baseRows++
|
||
}
|
||
writer.writeUint16(uint16(baseRows)) // event 9 + calendar + event rows
|
||
writer.writeUint16(9)
|
||
writer.writeUint32(0)
|
||
writer.writeRawDstr(nil)
|
||
writer.writeRawDstr(nil)
|
||
writer.writeByte(0)
|
||
for _, record := range calendarRecords {
|
||
if err := writeCurrentActivityCalendarRecord(&writer, record); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
// The guild window checks the native activity directory for 2378 before
|
||
// enabling its hongbao panel. Keep the gate in the base catalog; it has no
|
||
// active-state payload or synthetic reward data.
|
||
writer.writeUint16(currentGuildHongbaoEventID)
|
||
writer.writeUint32(currentSpendTimeBaseCatalogValue)
|
||
writer.writeRawDstr(nil)
|
||
writer.writeRawDstr(nil)
|
||
writer.writeByte(0)
|
||
if nativePCRoom {
|
||
// 0x2FDEADA and 0x1C08FC0 query active event 228. The model reads
|
||
// rewards and its complete gold-frame UI directly from PCRoomTimePoint.etc.
|
||
writer.writeUint16(currentPCRoomTimePointEventID)
|
||
writer.writeUint32(currentSpendTimeBaseCatalogValue)
|
||
writer.writeRawDstr(nil)
|
||
writer.writeRawDstr(nil)
|
||
writer.writeByte(0)
|
||
} else {
|
||
// 「累计在线得好礼」(event 2347) 的目录行与 active 行照常写入。
|
||
//
|
||
// 历史:曾尝试"删掉这一行"来关闭该活动,实测无效——该 HUD 在客户端 PVF
|
||
// `event/chn_event/chn_attendanceevent.evt` 里有定义,缺目录行时客户端
|
||
// 回落到本地定义,HUD 依然显示;而删掉 active 行/进度包会让「领取」失去
|
||
// 数据来源,变成"看得到但领不了"。所以这里保持完整下发。
|
||
writer.writeUint16(uint16(currentSpendTimeEventID))
|
||
writer.writeUint32(currentSpendTimeBaseCatalogValue)
|
||
writer.writeRawDstr(nil) // generic title; the fixed event UI owns visible text
|
||
writer.writeRawDstr(nil) // generic description
|
||
writer.writeByte(1) // has_extended: installs the native click action below
|
||
writer.writeByte(0) // generic category flags
|
||
writer.writeByte(0) // generic presentation flags
|
||
writer.writeRawDstr(nil) // optional title override
|
||
writer.writeRawDstr(nil) // optional subtitle override
|
||
writer.writeRawDstr(nil) // optional description override
|
||
writer.writeUint32(0) // optional start timestamp
|
||
writer.writeUint32(0) // optional end timestamp
|
||
writer.writeAsciiDstr(currentSpendTimeEventRoute)
|
||
writer.writeRawDstr(nil) // optional status text
|
||
writer.writeByte(0) // optional presentation tail
|
||
}
|
||
if laborBase {
|
||
writer.writeUint16(uint16(currentSpendTimeSumEventID))
|
||
writer.writeUint32(currentSpendTimeBaseCatalogValue)
|
||
writer.writeRawDstr(nil)
|
||
writer.writeRawDstr(nil)
|
||
writer.writeByte(0)
|
||
}
|
||
// 骑士马战(joust,事件 2365):基础目录行 + 下面的 active 行成对存在,
|
||
// 缺任一条客户端活动列表都会丢掉入口。
|
||
writer.writeUint16(uint16(currentJoustEventID))
|
||
writer.writeUint32(currentSpendTimeBaseCatalogValue)
|
||
writer.writeRawDstr(nil) // 可见名称与交互由原生 NPC/PVF 提供
|
||
writer.writeRawDstr(nil)
|
||
writer.writeByte(0) // schedule-free NPC visibility row
|
||
writer.writeUint16(uint16(currentJonathanEventID))
|
||
writer.writeUint32(currentSpendTimeBaseCatalogValue)
|
||
writer.writeRawDstr(nil) // native NPC/PVF owns visible name and interaction
|
||
writer.writeRawDstr(nil)
|
||
writer.writeByte(0) // schedule-free NPC visibility row
|
||
writer.writeUint16(uint16(currentLukeRaidEventID))
|
||
writer.writeUint32(currentSpendTimeBaseCatalogValue)
|
||
writer.writeRawDstr(nil) // native Luke entrance UI owns visible text
|
||
writer.writeRawDstr(nil) // native Luke entrance UI owns visible description
|
||
writer.writeByte(0) // schedule state arrives through op1206 below
|
||
writer.writeUint16(uint16(currentFiendWarEventID))
|
||
writer.writeUint32(currentSpendTimeBaseCatalogValue)
|
||
writer.writeRawDstr(nil) // native Fiend War entrance UI owns visible text
|
||
writer.writeRawDstr(nil) // native Fiend War entrance UI owns visible description
|
||
writer.writeByte(0) // schedule state arrives through op1206 below
|
||
writer.writeUint16(currentSeriaLuckEventID)
|
||
writer.writeUint32(currentSpendTimeBaseCatalogValue)
|
||
writer.writeRawDstr(nil) // native Seria-luck control owns its visible text
|
||
writer.writeRawDstr(nil)
|
||
writer.writeByte(0) // no schedule/action extension
|
||
// sub_17B1B70 accepts active event 2417 for the native open Luke entrance.
|
||
// Keep real quest/item records and the raid entry-cost transaction intact.
|
||
// 全职LV预约得好礼(活动 222,currentDecisionEventID)已按运营要求关闭:
|
||
// 从日历 Envelope 里摘掉它,客户端活动窗口不再显示该条目。
|
||
// 它是"命运/决策"类日历格子之一,摘除只影响这一格(baseRows 由
|
||
// len(calendarRecords) 动态计算,会自动同步)。
|
||
for _, eventID := range []uint16{currentLukeRaidOpenEventID, currentMonthlyAttendanceEventID, currentWeeklyAttendanceEventID} {
|
||
// EventListWindow.evt owns the native rows and popup routes.
|
||
record := activityCalendarRecord{eventID: eventID, field0: currentSpendTimeBaseCatalogValue}
|
||
if err := writeCurrentActivityCalendarRecord(&writer, record); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
// E8E2A0 requires event 2400 before consulting PVF [agitwar day] and
|
||
// [war limit time]. Its absence forced the waiting page on every day.
|
||
writer.writeUint16(currentGuildWarEventID)
|
||
writer.writeUint32(currentSpendTimeBaseCatalogValue)
|
||
writer.writeRawDstr(nil)
|
||
writer.writeRawDstr(nil)
|
||
writer.writeByte(0)
|
||
// Current EXE 0x2045237 recognizes base event 266; 0x204574C passes
|
||
// its presence to 0x16AB7B0, which retains the mileage category (4).
|
||
// No active row is needed for visibility: event 266's active parameters
|
||
// belong to a separate controller and are not a wallet balance.
|
||
if err := writeCurrentActivityCalendarRecord(&writer, activityCalendarRecord{
|
||
eventID: currentMileageShopEventID, field0: currentSpendTimeBaseCatalogValue,
|
||
}); err != nil {
|
||
return nil, err
|
||
}
|
||
// The ruins gate is a native NPC with an [open visible event] condition.
|
||
// Its full frame/glow assembly comes from PVF ANI/ALS and the existing NPK.
|
||
// Only one weekday belongs in the base directory; no active parameters or
|
||
// second op108 send are needed for this visibility predicate.
|
||
if err := writeCurrentActivityCalendarRecord(&writer, activityCalendarRecord{
|
||
eventID: currentMysteriousRuinsPortalEventID(now), field0: currentSpendTimeBaseCatalogValue,
|
||
}); err != nil {
|
||
return nil, err
|
||
}
|
||
// 原值 10 = 泡点 + 骑士马战(joust) + 乔纳森 + 卢克 + 恶魔之战 + 命运 +
|
||
// 卢克开放 + 月签到 + LVUP + 周签到。
|
||
// active 行计数必须与下面实际写入的行数严格一致,否则客户端解析会错位。
|
||
//
|
||
// 本部署不写泡点行(nativePCRoom=false),实际写入:
|
||
// 骑士马战 + 乔纳森 + 卢克 + 恶魔之战 + 命运 + 月签到 + 卢克开放
|
||
// + 公会战 + 周签到 = 9
|
||
activeRows := 9
|
||
if nativePCRoom {
|
||
activeRows++
|
||
}
|
||
if laborActive {
|
||
activeRows++
|
||
}
|
||
writer.writeByte(byte(activeRows))
|
||
// 累计在线得好礼(event 2347)active 行已移除:
|
||
// 它的 HUD 只在该事件出现在 active 表里时可见(不把 2347 放进
|
||
// active-event map 时,客户端会隐藏该 HUD 条目)。结算/历史实现保留为
|
||
// 死代码,便于日后按需恢复。
|
||
if nativePCRoom {
|
||
writer.writeUint16(currentPCRoomTimePointEventID)
|
||
for range params {
|
||
writer.writeUint32(0)
|
||
}
|
||
}
|
||
// IDA: 0x2043BCB routes 2348 to owner 0x41A / 0xF3F220.
|
||
// 0xF40D10 binds exactly three item controls. Sending a fourth item would
|
||
// overrun that native control array. param[10] is the item count.
|
||
if laborActive {
|
||
writer.writeUint16(uint16(currentSpendTimeSumEventID))
|
||
for i := 0; i < 12; i++ {
|
||
value := uint32(currentSpendTimeUnusedParam)
|
||
if i < 3 {
|
||
value = uint32(labor.Login[i].ItemID)
|
||
}
|
||
if i == 10 {
|
||
value = 3
|
||
}
|
||
if i == 11 {
|
||
value = 0
|
||
}
|
||
writer.writeUint32(value)
|
||
}
|
||
}
|
||
|
||
// 骑士马战(joust)active 行:与上面的基础目录行成对。
|
||
writer.writeUint16(uint16(currentJoustEventID))
|
||
for index := 0; index < currentSpendTimeDescriptorParamCount; index++ {
|
||
writer.writeUint32(0)
|
||
}
|
||
writer.writeUint16(uint16(currentJonathanEventID))
|
||
for index := 0; index < currentSpendTimeDescriptorParamCount; index++ {
|
||
writer.writeUint32(0)
|
||
}
|
||
writer.writeUint16(uint16(currentLukeRaidEventID))
|
||
for index := 0; index < currentSpendTimeDescriptorParamCount; index++ {
|
||
writer.writeUint32(0)
|
||
}
|
||
writer.writeUint16(uint16(currentFiendWarEventID))
|
||
for index := 0; index < currentSpendTimeDescriptorParamCount; index++ {
|
||
writer.writeUint32(0)
|
||
}
|
||
writer.writeUint16(currentSeriaLuckEventID)
|
||
writer.writeUint32(uint32(packageitem.SeriaLuckValueMax))
|
||
for index := 1; index < currentSpendTimeDescriptorParamCount; index++ {
|
||
writer.writeUint32(0)
|
||
}
|
||
writer.writeUint16(currentMonthlyAttendanceEventID)
|
||
// sub_2043290 -> sub_1829EE0 stores param[0] as required clear count.
|
||
writer.writeUint32(onlineevent.MonthlyRequiredClears)
|
||
for index := 1; index < currentSpendTimeDescriptorParamCount; index++ {
|
||
writer.writeUint32(0)
|
||
}
|
||
writer.writeUint16(currentLukeRaidOpenEventID)
|
||
for index := 0; index < currentSpendTimeDescriptorParamCount; index++ {
|
||
writer.writeUint32(0)
|
||
}
|
||
writer.writeUint16(currentGuildWarEventID)
|
||
for index := 0; index < currentSpendTimeDescriptorParamCount; index++ {
|
||
writer.writeUint32(0)
|
||
}
|
||
writer.writeUint16(currentWeeklyAttendanceEventID)
|
||
for index := 0; index < currentSpendTimeDescriptorParamCount; index++ {
|
||
writer.writeUint32(0)
|
||
}
|
||
return writer.bytes(), nil
|
||
}
|
||
|
||
// currentRaidScheduleCounters is the live state one raid-schedule record carries.
|
||
//
|
||
// The three u32s are NOT free-form. The client hands raw[0]/raw[1]/raw[2] to
|
||
// sub_C39140 (file 0xC39140) in that order, and that setter -- the ONLY writer of
|
||
// the per-mode window element's counters in the whole EXE -- stores them as:
|
||
//
|
||
// raw[0] -> element+0x30 account WEEKLY used (rendered as 账号周入场次数)
|
||
// raw[1] -> element+0x08 character WEEKLY used (rendered as 角色周入场次数)
|
||
// raw[2] -> element+0x04 seven-day mask, and the 7 cells at +0xc..+0x24
|
||
//
|
||
// Both weekly readouts print (limit - used), which is why byte 3 of the op1303
|
||
// row can only ever move the limit and the probe of 2026-09-30 saw 43/43 and
|
||
// 3/3: those were used == 0, not "the weekly counter is limit-only".
|
||
type currentRaidScheduleCounters struct {
|
||
AccountWeeklyUsed uint32
|
||
CharacterWeeklyUsed uint32
|
||
DayMask uint32
|
||
}
|
||
|
||
// buildCurrentRaidScheduleProgressBody matches the current narrow op1206
|
||
// RaidScheduleEvent records for the Anton, Luke and Fiend War entrance events.
|
||
// raw[0]/raw[1] are the account and character weekly USED counts and raw[2] is
|
||
// the seven-day mask; a player who has not entered this week is therefore all
|
||
// zero, but a player who has must see their own number or the entry window's
|
||
// 角色周入场次数 shows limit/limit while the start command refuses on the same
|
||
// counter (raid_admission_counter.go charges exactly these two policies).
|
||
func buildCurrentRaidScheduleProgressBody(
|
||
eventID uint32,
|
||
counters currentRaidScheduleCounters,
|
||
) ([]byte, error) {
|
||
switch eventID {
|
||
case currentAntonRaidEventID, currentLukeRaidEventID, currentFiendWarEventID:
|
||
default:
|
||
return nil, fmt.Errorf("current raid schedule event id=%d is unsupported", eventID)
|
||
}
|
||
var writer packetWriter
|
||
writer.writeUint32(eventID)
|
||
writer.writeByte(0) // narrow 0x20-byte record
|
||
writer.writeUint32(counters.AccountWeeklyUsed)
|
||
writer.writeUint32(counters.CharacterWeeklyUsed)
|
||
writer.writeUint32(counters.DayMask)
|
||
writer.writeBytes(make([]byte, currentSpendTimeProgressRawSize-12))
|
||
return writer.bytes(), nil
|
||
}
|
||
|
||
// buildCurrentSpendTimeProgressBody matches current NoPack op1206 dispatch:
|
||
//
|
||
// u32 event_id, u8 wide_flag, raw[wide_flag ? 0xA0 : 0x20]
|
||
//
|
||
// Event 2347 uses the narrow record. Its first two u32 values are authoritative
|
||
// elapsed seconds and the number of automatically completed reward stages.
|
||
func buildCurrentSpendTimeProgressBody(elapsedSeconds uint64, completedStages uint32) ([]byte, error) {
|
||
if elapsedSeconds > math.MaxUint32 {
|
||
return nil, fmt.Errorf("current spend-time elapsed seconds=%d overflows u32", elapsedSeconds)
|
||
}
|
||
var writer packetWriter
|
||
writer.writeUint32(currentSpendTimeEventID)
|
||
writer.writeByte(0) // narrow 0x20-byte record
|
||
writer.writeUint32(uint32(elapsedSeconds))
|
||
writer.writeUint32(completedStages)
|
||
writer.writeBytes(make([]byte, currentSpendTimeProgressRawSize-8))
|
||
return writer.bytes(), nil
|
||
}
|