DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
+969
@@ -0,0 +1,969 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfonlineevent "longheng.io/server/internal/modules/dnf/onlineevent"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
currentSpendTimeStageCount = 4
|
||||
currentSpendTimeObserveTick = 30 * time.Second
|
||||
currentSpendTimeResetHour = 6
|
||||
currentSpendTimeWriteTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// The established 2347 definition retains the durable daily observation clock.
|
||||
// Production loads nativePCRoom from its separate ETC file and activates 228,
|
||||
// whose manual mail rewards replace the old automatic inventory awards.
|
||||
type currentSpendTimeRuntimeCatalog struct {
|
||||
laborDay *dnfonlineevent.LaborDayCatalog
|
||||
nativePCRoom *dnfonlineevent.PCRoomTimePointCatalog
|
||||
definition dnfonlineevent.Definition
|
||||
rewardItemIDs []uint32
|
||||
sumRewardItemIDs []uint32
|
||||
totalStageSeconds uint32
|
||||
}
|
||||
|
||||
// currentSpendTimeClockState owns one selected-character observation cursor.
|
||||
// Its timer callback is routed through the per-session event loop and also
|
||||
// carries characterGeneration, so a stale callback cannot credit a replacement
|
||||
// character or write to its socket.
|
||||
type currentSpendTimeClockState struct {
|
||||
laborDayActive bool
|
||||
mu sync.Mutex
|
||||
eventInfoSent bool
|
||||
eventInfoDescriptorGeneration uint64
|
||||
eventInfoReplayAfterTeamReturn bool
|
||||
characterID uint16
|
||||
characterGeneration uint64
|
||||
initializedCharacterGeneration uint64
|
||||
accountID string
|
||||
anchor time.Time
|
||||
generation uint64
|
||||
timerName string
|
||||
onlineSeconds uint64
|
||||
completedStages uint32
|
||||
catalog *currentSpendTimeRuntimeCatalog
|
||||
}
|
||||
|
||||
type currentSpendTimeSettlementResult struct {
|
||||
changed bool
|
||||
rewardItems []dnfonlineevent.CommittedItem
|
||||
claimErr error
|
||||
}
|
||||
|
||||
func buildCurrentSpendTimeRuntimeCatalog(catalog *dnfonlineevent.AttendancePVFCatalog) (*currentSpendTimeRuntimeCatalog, error) {
|
||||
if catalog == nil {
|
||||
return nil, fmt.Errorf("current spend-time attendance catalog is nil")
|
||||
}
|
||||
snapshot := catalog.Snapshot()
|
||||
if len(snapshot.ProcessDurationsSeconds) != currentSpendTimeStageCount ||
|
||||
len(snapshot.RewardItems) != currentSpendTimeStageCount {
|
||||
return nil, fmt.Errorf(
|
||||
"current spend-time PVF shape durations=%d rewards=%d want=%d/%d",
|
||||
len(snapshot.ProcessDurationsSeconds),
|
||||
len(snapshot.RewardItems),
|
||||
currentSpendTimeStageCount,
|
||||
currentSpendTimeStageCount,
|
||||
)
|
||||
}
|
||||
|
||||
definition := dnfonlineevent.Definition{
|
||||
ID: strconv.FormatUint(uint64(currentSpendTimeEventID), 10),
|
||||
Stages: make([]dnfonlineevent.Stage, 0, currentSpendTimeStageCount),
|
||||
}
|
||||
rewardItemIDs := make([]uint32, 0, currentSpendTimeStageCount)
|
||||
sumRewardItemIDs := make([]uint32, 0, currentSpendTimeStageCount)
|
||||
var cumulative uint64
|
||||
for index := 0; index < currentSpendTimeStageCount; index++ {
|
||||
duration := snapshot.ProcessDurationsSeconds[index]
|
||||
reward := snapshot.RewardItems[index]
|
||||
if duration <= 0 || reward.ItemID <= 0 || reward.ItemID > math.MaxUint32 || reward.Count <= 0 {
|
||||
return nil, fmt.Errorf(
|
||||
"current spend-time PVF stage=%d duration=%d item=%d count=%d",
|
||||
index,
|
||||
duration,
|
||||
reward.ItemID,
|
||||
reward.Count,
|
||||
)
|
||||
}
|
||||
cumulative += uint64(duration)
|
||||
if cumulative > math.MaxUint32 {
|
||||
return nil, fmt.Errorf("current spend-time cumulative seconds=%d overflows u32", cumulative)
|
||||
}
|
||||
definition.Stages = append(definition.Stages, dnfonlineevent.Stage{
|
||||
ID: fmt.Sprintf("stage-%d", index+1),
|
||||
RequiredSeconds: cumulative,
|
||||
Items: []dnfonlineevent.ItemReward{{
|
||||
ItemID: reward.ItemID,
|
||||
Count: reward.Count,
|
||||
}},
|
||||
})
|
||||
rewardItemIDs = append(rewardItemIDs, uint32(reward.ItemID))
|
||||
}
|
||||
for _, reward := range snapshot.RewardItemsForSum {
|
||||
if reward.ItemID <= 0 || reward.ItemID > math.MaxUint32 || reward.Count <= 0 {
|
||||
return nil, fmt.Errorf("current spend-time sum reward item=%d count=%d", reward.ItemID, reward.Count)
|
||||
}
|
||||
sumRewardItemIDs = append(sumRewardItemIDs, uint32(reward.ItemID))
|
||||
}
|
||||
if err := definition.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("validate current spend-time definition: %w", err)
|
||||
}
|
||||
return ¤tSpendTimeRuntimeCatalog{
|
||||
definition: definition,
|
||||
rewardItemIDs: append([]uint32(nil), rewardItemIDs...),
|
||||
sumRewardItemIDs: append([]uint32(nil), sumRewardItemIDs...),
|
||||
totalStageSeconds: uint32(cumulative),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// currentSpendTimeRuntimeCatalogIfAvailable lazily projects the same archive
|
||||
// already loaded by the startup PVF stages. Hand-assembled unit-test services
|
||||
// without an archive keep the feature absent unless the test injects a proved
|
||||
// projection; production Start always establishes the archive first.
|
||||
func (s *Service) currentSpendTimeRuntimeCatalogIfAvailable(ctx context.Context) (*currentSpendTimeRuntimeCatalog, bool, error) {
|
||||
if s == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
// 「累计在线得好礼」(event 2347,AttendanceEventPVFPath)已按运营要求关闭:
|
||||
// 这里直接声明"活动运行时目录不可用",于是:
|
||||
// - 结算时钟(settleCurrentSpendTimeClockLocked)永不启动,不会计入在线时长;
|
||||
// - 不会领取/发放任何阶段奖励,也不会发奖励邮件;
|
||||
// - 进度包(op1206)与目录行都不再下发,客户端不显示该活动 HUD;
|
||||
// - 同一目录也被「全职LV预约得好礼」(level_aiming)复用,因此后者一并停用。
|
||||
// 解析实现(buildCurrentSpendTimeRuntimeCatalog 等)保留为死代码,便于日后恢复。
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// 以下为活动关闭前的原始实现,保留备查(unused;恢复活动时把上面函数体的
|
||||
// 提前 return 去掉、并把本函数改回 currentSpendTimeRuntimeCatalogIfAvailable 即可)。
|
||||
// 因为整个函数体被注释掉,Go 不会编译它。
|
||||
/*
|
||||
func (s *Service) currentSpendTimeRuntimeCatalogLoaderDisabledForClosedActivity(ctx context.Context) (*currentSpendTimeRuntimeCatalog, bool, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
s.spendTimeCatalogMu.Lock()
|
||||
defer s.spendTimeCatalogMu.Unlock()
|
||||
if s.spendTimeCatalogState != nil {
|
||||
return s.spendTimeCatalogState, true, nil
|
||||
}
|
||||
if s.spendTimeCatalogLoadErr != nil {
|
||||
return nil, true, s.spendTimeCatalogLoadErr
|
||||
}
|
||||
|
||||
s.initialEquipmentMu.Lock()
|
||||
archive := s.initialEquipmentArchive
|
||||
s.initialEquipmentMu.Unlock()
|
||||
if archive == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
rawCatalog, err := dnfonlineevent.LoadAttendancePVFCatalog(ctx, archive)
|
||||
if err == nil {
|
||||
s.spendTimeCatalogState, err = buildCurrentSpendTimeRuntimeCatalog(rawCatalog)
|
||||
}
|
||||
if err == nil {
|
||||
s.spendTimeCatalogState.nativePCRoom, err = dnfonlineevent.LoadPCRoomTimePointCatalog(ctx, archive)
|
||||
if err != nil {
|
||||
s.spendTimeCatalogState = nil
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
s.spendTimeCatalogState.laborDay, err = s.currentLaborDayCatalog(ctx)
|
||||
}
|
||||
s.spendTimeCatalogLoadErr = err
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
s.logPacketEvent("dnf-spend-time-catalog-loaded",
|
||||
"event_id", currentSpendTimeEventID,
|
||||
"reward_count", len(s.spendTimeCatalogState.rewardItemIDs),
|
||||
"total_stage_seconds", s.spendTimeCatalogState.totalStageSeconds,
|
||||
"source", dnfonlineevent.AttendanceEventPVFPath)
|
||||
return s.spendTimeCatalogState, true, nil
|
||||
}
|
||||
*/
|
||||
|
||||
func (s *Service) currentSpendTimeOwner() (*dnfonlineevent.Owner, error) {
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok {
|
||||
return nil, dnfonlineevent.ErrOwnerUnavailable
|
||||
}
|
||||
return dnfonlineevent.NewOwner(repositories)
|
||||
}
|
||||
|
||||
// Publish the activity directory once per actor generation, followed by the
|
||||
// active owner's state (561 for native 228), then start the durable clock.
|
||||
func (s *Service) sendCurrentSpendTimeInitialStateOnce(
|
||||
session *gameSession,
|
||||
source string,
|
||||
) error {
|
||||
if s == nil || session == nil || session.selectedCharacterID == 0 {
|
||||
return nil
|
||||
}
|
||||
characterID := session.selectedCharacterID
|
||||
characterGeneration := session.characterGeneration
|
||||
|
||||
session.spendTime.mu.Lock()
|
||||
alreadyInitialized := session.spendTime.characterID == characterID &&
|
||||
session.spendTime.initializedCharacterGeneration == characterGeneration &&
|
||||
session.spendTime.catalog != nil && !session.spendTime.anchor.IsZero()
|
||||
session.spendTime.mu.Unlock()
|
||||
if alreadyInitialized {
|
||||
// The descriptor is a one-shot activity-table replacement. Replaying
|
||||
// op108 from the scene-ready tail resets the native owner and makes the
|
||||
// same online-time entries render a second time. The first town route
|
||||
// already sends op108 followed by op1206; later callbacks only refresh
|
||||
// their authoritative state through the normal progress path.
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), currentSpendTimeWriteTimeout)
|
||||
defer cancel()
|
||||
catalog, available, err := s.currentSpendTimeRuntimeCatalogIfAvailable(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load current spend-time catalog: %w", err)
|
||||
}
|
||||
if !available || catalog == nil {
|
||||
s.logGameEvent(session, "game-spend-time-initialization-deferred",
|
||||
"source", source,
|
||||
"char_id", characterID,
|
||||
"reason", "runtime_pvf_archive_not_loaded")
|
||||
return nil
|
||||
}
|
||||
owner, err := s.currentSpendTimeOwner()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create current spend-time owner: %w", err)
|
||||
}
|
||||
now := s.gameplayNow()
|
||||
accountID := s.accountIDForSession(session)
|
||||
characterIDText := strconv.Itoa(int(characterID))
|
||||
|
||||
session.spendTime.mu.Lock()
|
||||
defer session.spendTime.mu.Unlock()
|
||||
if session.spendTime.characterID == characterID &&
|
||||
session.spendTime.initializedCharacterGeneration == characterGeneration &&
|
||||
session.spendTime.catalog != nil && !session.spendTime.anchor.IsZero() {
|
||||
return nil
|
||||
}
|
||||
if session.spendTime.characterID != 0 && session.spendTime.characterID != characterID {
|
||||
return fmt.Errorf("current spend-time clock still owns character %d", session.spendTime.characterID)
|
||||
}
|
||||
snapshot, err := owner.Status(ctx, dnfonlineevent.StatusCommand{
|
||||
AccountID: accountID,
|
||||
CharacterID: characterIDText,
|
||||
Definition: catalog.definition,
|
||||
ObservedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("load current spend-time status: %w", err)
|
||||
}
|
||||
snapshot, rewardItems, claimErr := s.claimCurrentSpendTimeUnlockedStages(
|
||||
ctx,
|
||||
owner,
|
||||
accountID,
|
||||
characterIDText,
|
||||
catalog,
|
||||
snapshot,
|
||||
now,
|
||||
)
|
||||
if claimErr != nil {
|
||||
s.logGameEvent(session, "game-spend-time-initial-auto-award-deferred",
|
||||
"source", source,
|
||||
"char_id", characterID,
|
||||
"online_seconds", snapshot.OnlineSeconds,
|
||||
"error", claimErr)
|
||||
}
|
||||
completedStages := currentSpendTimeClaimedStages(catalog.definition, snapshot)
|
||||
if err := s.settleCurrentLaborDay(ctx, session, catalog.laborDay, time.Time{}, time.Time{}, true); err != nil {
|
||||
return err
|
||||
}
|
||||
// op108 is not merely process-global metadata: it creates the two floating
|
||||
// activity controls in the selected actor's town HUD. Rebuild it once for
|
||||
// every new actor generation. Keep the legacy registry only for a retry in
|
||||
// the same generation, where a descriptor may already have reached the EXE
|
||||
// before a later packet write failed.
|
||||
sceneDescriptorRequired := !session.spendTime.eventInfoSent ||
|
||||
session.spendTime.eventInfoDescriptorGeneration != characterGeneration
|
||||
registrySendDescriptor, descriptorReady := false, true
|
||||
if !sceneDescriptorRequired {
|
||||
registrySendDescriptor, descriptorReady = s.beginCurrentSpendTimeEventInfo(session)
|
||||
if !descriptorReady {
|
||||
return fmt.Errorf("current spend-time descriptor retry is being sent by another session for client pid=%d", session.clientPID)
|
||||
}
|
||||
}
|
||||
forceDescriptorReplay := session.spendTime.eventInfoReplayAfterTeamReturn
|
||||
sendDescriptor := sceneDescriptorRequired || registrySendDescriptor || forceDescriptorReplay
|
||||
descriptorSent, sendErr := s.sendCurrentSpendTimeProtocolState(
|
||||
session,
|
||||
catalog,
|
||||
snapshot.OnlineSeconds,
|
||||
completedStages,
|
||||
sendDescriptor,
|
||||
)
|
||||
if registrySendDescriptor || descriptorSent {
|
||||
// Commit immediately after a successful op108 write, even when this
|
||||
// scene-generation send bypassed the legacy registry. A later op1206
|
||||
// progress or joust snapshot failure must not let a retry rediscover an
|
||||
// unsent process-level descriptor and duplicate it in this same scene.
|
||||
s.finishCurrentSpendTimeEventInfo(session, descriptorSent)
|
||||
}
|
||||
if descriptorSent {
|
||||
session.spendTime.eventInfoSent = true
|
||||
session.spendTime.eventInfoDescriptorGeneration = characterGeneration
|
||||
}
|
||||
if forceDescriptorReplay && descriptorSent {
|
||||
// The full op108 write is the commit boundary. A later op1206 failure is
|
||||
// retried progress-only because replaying the base catalog twice after the
|
||||
// team-return repair would repeat the same client-side owner reset.
|
||||
session.spendTime.eventInfoReplayAfterTeamReturn = false
|
||||
s.logGameEvent(session, "game-spend-time-descriptor-replayed-after-team-return",
|
||||
"source", source,
|
||||
"char_id", characterID,
|
||||
"client_pid", session.clientPID,
|
||||
"reason", "team_channel_return_rebuilds_client_activity_table")
|
||||
}
|
||||
if sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
if !sendDescriptor {
|
||||
s.logGameEvent(session, "game-spend-time-descriptor-skipped",
|
||||
"source", source,
|
||||
"char_id", characterID,
|
||||
"client_pid", session.clientPID,
|
||||
"already_sent", session.spendTime.eventInfoSent,
|
||||
"reason", "descriptor_already_written_for_current_actor_generation_progress_only")
|
||||
}
|
||||
if err := s.sendCurrentSpendTimeRewardItemUpdates(session, rewardItems, source+"_initial_auto_award"); err != nil {
|
||||
return err
|
||||
}
|
||||
// 参考实现这里调 cera_shop_buycount.go 的 sendCurrentCeraShopBuyCountInitialState。
|
||||
// 本树把同一件事实现成了 sendCurrentCeraShopBuyCountGiftProjection(同一个
|
||||
// 0x040B opcode、同一个 u32 实购买次数体),只是计数落在角色 Stats 而不是账号
|
||||
// 元数据上,所以这里直接复用本树那一个,不引入第二份"已购买次数"下发。
|
||||
if err := s.sendCurrentCeraShopBuyCountGiftProjection(ctx, session, "spend_time_initial_state"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendCurrentMileageInitialState(ctx, session); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.spendTime.characterID = characterID
|
||||
session.spendTime.characterGeneration = characterGeneration
|
||||
session.spendTime.initializedCharacterGeneration = characterGeneration
|
||||
session.spendTime.accountID = accountID
|
||||
// Keep the exact observation start. onlineevent.Owner conservatively
|
||||
// ceil(from)/floor(to); truncating here would manufacture up to one second.
|
||||
session.spendTime.anchor = now
|
||||
session.spendTime.generation++
|
||||
session.spendTime.onlineSeconds = snapshot.OnlineSeconds
|
||||
session.spendTime.completedStages = completedStages
|
||||
session.spendTime.catalog = catalog
|
||||
session.spendTime.laborDayActive = catalog.laborDay != nil && catalog.laborDay.Policy.ActiveAt(now)
|
||||
if err := s.armCurrentSpendTimeTickLocked(session); err != nil {
|
||||
session.spendTime.characterID = 0
|
||||
session.spendTime.characterGeneration = 0
|
||||
session.spendTime.initializedCharacterGeneration = 0
|
||||
session.spendTime.accountID = ""
|
||||
session.spendTime.anchor = time.Time{}
|
||||
session.spendTime.timerName = ""
|
||||
session.spendTime.onlineSeconds = 0
|
||||
session.spendTime.completedStages = 0
|
||||
session.spendTime.catalog = nil
|
||||
return err
|
||||
}
|
||||
s.logGameEvent(session, "game-spend-time-initial-state-sent",
|
||||
"source", source,
|
||||
"char_id", characterID,
|
||||
"event_id", currentSpendTimeEventID,
|
||||
"online_seconds", snapshot.OnlineSeconds,
|
||||
"completed_stages", completedStages,
|
||||
"reward_item_updates", len(rewardItems),
|
||||
"sequence", "class0_op108_descriptor_then_class0_op1206_progress_then_class0_op1206_luke_schedule_then_class0_op1206_fiendwar_schedule_then_class0_op1303_raid_availability_then_atomic_reward_op14_before_typed_op24")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentSpendTimeProtocolState(
|
||||
session *gameSession,
|
||||
catalog *currentSpendTimeRuntimeCatalog,
|
||||
onlineSeconds uint64,
|
||||
completedStages uint32,
|
||||
includeFirstProcessDescriptor bool,
|
||||
) (bool, error) {
|
||||
if s == nil || session == nil || catalog == nil {
|
||||
return false, nil
|
||||
}
|
||||
var eventInfoTransport []byte
|
||||
if includeFirstProcessDescriptor {
|
||||
eventInfoBody, err := buildCurrentOnlineEventInfoBodyAt(
|
||||
catalog.rewardItemIDs,
|
||||
catalog.sumRewardItemIDs,
|
||||
catalog.totalStageSeconds,
|
||||
catalog.nativePCRoom != nil,
|
||||
catalog.laborDay, s.gameplayNow(),
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
eventInfoTransport, err = zlibCompress(eventInfoBody)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("compress current spend-time op108: %w", err)
|
||||
}
|
||||
}
|
||||
progressBody, err := buildCurrentSpendTimeProgressBody(onlineSeconds, completedStages)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
descriptorSent := false
|
||||
if includeFirstProcessDescriptor {
|
||||
if err := s.sendCurrentProtectedClass0Packet(
|
||||
session,
|
||||
currentSpendTimeEventInfoMsgID,
|
||||
eventInfoTransport,
|
||||
currentSpendTimeEventInfoCodec,
|
||||
"current_op108_spend_time_catalog",
|
||||
); err != nil {
|
||||
return false, err
|
||||
}
|
||||
descriptorSent = true
|
||||
// Replacing the native event table invalidates its attendance snapshot.
|
||||
session.monthlyAttendance = currentMonthlyAttendanceProjection{}
|
||||
session.weeklyAttendance = currentWeeklyAttendanceProjection{}
|
||||
}
|
||||
// Install the directory before the selected online owner's state. The
|
||||
// original four-plus-four panel is 228/561; 2347/1206 is a different HUD.
|
||||
if catalog.nativePCRoom != nil {
|
||||
if err := s.sendCurrentPCRoomTimePoint(session, catalog, 0, -1); err != nil {
|
||||
return descriptorSent, err
|
||||
}
|
||||
} else if err := s.sendGameUpperRawClass(session, currentSpendTimeProgressMsgID, progressBody, 0); err != nil {
|
||||
return descriptorSent, err
|
||||
}
|
||||
// Events 181/282/2449 own the Anton, Luke and Fiend War cards in the native
|
||||
// ESC/team-raid window. Their authoritative channels may already be
|
||||
// connected, but the cards remain hidden until the matching narrow schedule
|
||||
// records are applied after the final op108 catalog.
|
||||
//
|
||||
// 本树刻意比参考实现多推一条:参考版只发 282/2449 两档,而 181 的记录里
|
||||
// raw[0]/raw[1] 是账号/角色**本周已用**次数——客户端把这两项渲染成
|
||||
// "账号周入场次数 / 角色周入场次数",且显示的是 (上限 - 已用)。少了 181
|
||||
// 这条记录,已经进过本的玩家会看到"上限/上限",而 op1303 的入场门禁却按
|
||||
// 同一个计数拒绝他。sendCurrentRaidWeeklySchedules 一次把三档都发出去,
|
||||
// 计数直接取自 raid_admission_counter 的费用账本。
|
||||
if err := s.sendCurrentRaidWeeklySchedules(session, "spend_time_after_op108"); err != nil {
|
||||
return descriptorSent, err
|
||||
}
|
||||
// The current client consumes the activity collection and weekly-state
|
||||
// projection before it reads the per-raid daily availability table. Keep
|
||||
// this immediately after all op1206 records so Anton, Luke and Fiend War
|
||||
// observe the final post-op356 event table before evaluating availability.
|
||||
if err := s.sendCurrentRaidDailyAvailability(session, "spend_time_after_op1206"); err != nil {
|
||||
return descriptorSent, err
|
||||
}
|
||||
// 骑士马战(joust)活动已取消:登录快照不再推送 op1240 活动状态与
|
||||
// op1241 骑手名单;客户端因此不会把 owner 609 的竞猜界面置为可用。
|
||||
// 结算/历史实现保留为死代码,便于日后按需恢复。
|
||||
if err := s.sendCurrentMonthlyAttendance(session, false); err != nil {
|
||||
return descriptorSent, err
|
||||
}
|
||||
if err := s.sendCurrentWeeklyAttendance(session, false, false); err != nil {
|
||||
return descriptorSent, err
|
||||
}
|
||||
if err := s.sendCurrentReturneeState(session, false); err != nil {
|
||||
return descriptorSent, err
|
||||
}
|
||||
// 全职LV预约得好礼(等级预约,活动 222)已按运营要求关闭:
|
||||
// 不再下发 op623 的等级预约状态包,客户端不显示该活动。
|
||||
// 结算实现(level_aiming.go / onlineevent.LevelAiming*)保留为死代码,
|
||||
// 便于日后按需恢复。
|
||||
return descriptorSent, nil
|
||||
}
|
||||
|
||||
func currentSpendTimeReachedStages(definition dnfonlineevent.Definition, onlineSeconds uint64) uint32 {
|
||||
var completed uint32
|
||||
for _, stage := range definition.Stages {
|
||||
if onlineSeconds < stage.RequiredSeconds {
|
||||
break
|
||||
}
|
||||
completed++
|
||||
}
|
||||
return completed
|
||||
}
|
||||
|
||||
func currentSpendTimeClaimedStages(definition dnfonlineevent.Definition, snapshot dnfonlineevent.Snapshot) uint32 {
|
||||
claimed := make(map[string]struct{}, len(snapshot.ClaimedStageIDs))
|
||||
for _, stageID := range snapshot.ClaimedStageIDs {
|
||||
claimed[stageID] = struct{}{}
|
||||
}
|
||||
var completed uint32
|
||||
for _, stage := range definition.Stages {
|
||||
if _, ok := claimed[stage.ID]; !ok {
|
||||
break
|
||||
}
|
||||
completed++
|
||||
}
|
||||
return completed
|
||||
}
|
||||
|
||||
func (s *Service) claimCurrentSpendTimeUnlockedStages(
|
||||
ctx context.Context,
|
||||
owner *dnfonlineevent.Owner,
|
||||
accountID string,
|
||||
characterID string,
|
||||
catalog *currentSpendTimeRuntimeCatalog,
|
||||
snapshot dnfonlineevent.Snapshot,
|
||||
claimedAt time.Time,
|
||||
) (dnfonlineevent.Snapshot, []dnfonlineevent.CommittedItem, error) {
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
characterID = strings.TrimSpace(characterID)
|
||||
if s == nil || owner == nil || accountID == "" || characterID == "" || catalog == nil {
|
||||
return snapshot, nil, dnfonlineevent.ErrOwnerUnavailable
|
||||
}
|
||||
if catalog.nativePCRoom != nil {
|
||||
// Preserve the existing durable observation clock; native 228 owns
|
||||
// manual mail rewards and cumulative completions from this point on.
|
||||
_, err := s.settleCurrentPCRoomTimePoint(ctx, accountID, characterID, catalog, claimedAt, 0, -1)
|
||||
return snapshot, nil, err
|
||||
}
|
||||
claimed := make(map[string]struct{}, len(snapshot.ClaimedStageIDs))
|
||||
for _, stageID := range snapshot.ClaimedStageIDs {
|
||||
claimed[stageID] = struct{}{}
|
||||
}
|
||||
itemCatalog, err := s.currentPVFItemCatalog()
|
||||
if err != nil {
|
||||
return snapshot, nil, err
|
||||
}
|
||||
items := make([]dnfonlineevent.CommittedItem, 0, currentSpendTimeStageCount)
|
||||
for _, stage := range catalog.definition.Stages {
|
||||
if snapshot.OnlineSeconds < stage.RequiredSeconds {
|
||||
break
|
||||
}
|
||||
if _, alreadyClaimed := claimed[stage.ID]; alreadyClaimed {
|
||||
continue
|
||||
}
|
||||
claim, claimErr := owner.Claim(ctx, dnfonlineevent.ClaimCommand{
|
||||
AccountID: accountID,
|
||||
CharacterID: characterID,
|
||||
Definition: catalog.definition,
|
||||
StageID: stage.ID,
|
||||
ClaimedAt: claimedAt,
|
||||
Allocate: currentSpendTimeItemAllocator(itemCatalog, claimedAt, stage.ID),
|
||||
})
|
||||
if claimErr != nil {
|
||||
return snapshot, items, claimErr
|
||||
}
|
||||
snapshot = claim.PostSnapshot
|
||||
claimed[stage.ID] = struct{}{}
|
||||
items = append(items, currentSpendTimeClaimItemsForCharacter(claim, characterID)...)
|
||||
}
|
||||
return snapshot, items, nil
|
||||
}
|
||||
|
||||
func currentSpendTimeClaimItemsForCharacter(
|
||||
claim dnfonlineevent.ClaimResult,
|
||||
characterID string,
|
||||
) []dnfonlineevent.CommittedItem {
|
||||
if claim.Replayed || claim.CharacterID != characterID {
|
||||
return nil
|
||||
}
|
||||
items := make([]dnfonlineevent.CommittedItem, len(claim.Items))
|
||||
for index, item := range claim.Items {
|
||||
items[index] = item
|
||||
items[index].RawEntry = append([]byte(nil), item.RawEntry...)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func currentSpendTimeItemAllocator(
|
||||
catalog *pvfDungeonDropCatalog,
|
||||
grantedAt time.Time,
|
||||
stageID string,
|
||||
) dnfonlineevent.ItemAllocator {
|
||||
return func(record *dnfrepo.InventoryRecord, reward dnfonlineevent.ItemReward) (dnfonlineevent.CommittedItem, error) {
|
||||
if catalog == nil || record == nil || reward.ItemID <= 0 || reward.ItemID > math.MaxUint32 ||
|
||||
reward.Count <= 0 || reward.Count > math.MaxUint32 {
|
||||
return dnfonlineevent.CommittedItem{}, errDungeonPickupItemInvalid
|
||||
}
|
||||
definition, err := catalog.ResolveItem(uint32(reward.ItemID))
|
||||
if err != nil {
|
||||
return dnfonlineevent.CommittedItem{}, err
|
||||
}
|
||||
definition, err = currentPVFItemDefinitionForGrantAt(definition, grantedAt.UTC())
|
||||
if err != nil {
|
||||
return dnfonlineevent.CommittedItem{}, err
|
||||
}
|
||||
slot, err := addCurrentDungeonPickupToInventory(record, definition, uint32(reward.Count))
|
||||
if err != nil {
|
||||
return dnfonlineevent.CommittedItem{}, err
|
||||
}
|
||||
key := currentCeraShopInventorySlotKey(definition.InventoryListType, int16(slot))
|
||||
stack, found := record.Slots[key]
|
||||
if !found || stack.ItemID != reward.ItemID || stack.Count < reward.Count {
|
||||
return dnfonlineevent.CommittedItem{}, errDungeonPickupItemInvalid
|
||||
}
|
||||
entry := currentItemListEntryFromStack(definition.InventoryListType, int16(slot), stack)
|
||||
stack.RawEntry = append([]byte(nil), entry.data[:]...)
|
||||
if stack.Extra == nil {
|
||||
stack.Extra = make(map[string]string, 5)
|
||||
}
|
||||
if strings.TrimSpace(stack.Extra["source"]) == "" {
|
||||
stack.Extra["source"] = "online_event_spend_time"
|
||||
}
|
||||
stack.Extra["last_grant_source"] = "online_event_spend_time"
|
||||
stack.Extra["last_grant_event_id"] = strconv.FormatUint(uint64(currentSpendTimeEventID), 10)
|
||||
stack.Extra["last_grant_stage_id"] = stageID
|
||||
record.Slots[key] = stack
|
||||
return dnfonlineevent.CommittedItem{
|
||||
SlotKey: key, SlotIndex: slot, ItemID: reward.ItemID,
|
||||
Delta: reward.Count, PostCount: stack.Count,
|
||||
RawEntry: append([]byte(nil), entry.data[:]...),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentSpendTimeRewardItemUpdates(
|
||||
session *gameSession,
|
||||
items []dnfonlineevent.CommittedItem,
|
||||
source string,
|
||||
) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
entries := make([]currentItemListEntry, 0, len(items))
|
||||
refreshMedals := false
|
||||
for _, item := range items {
|
||||
if item.ItemID <= 0 || item.ItemID > math.MaxUint32 || item.PostCount < item.Delta ||
|
||||
len(item.RawEntry) != currentItemListEntryWireSize ||
|
||||
binary.LittleEndian.Uint16(item.RawEntry[:2]) != item.SlotIndex ||
|
||||
binary.LittleEndian.Uint32(item.RawEntry[2:6]) != uint32(item.ItemID) {
|
||||
return fmt.Errorf("current spend-time committed item has invalid wire receipt: %+v", item)
|
||||
}
|
||||
if listType, _, ok := parseSceneInventorySlotKey(item.SlotKey); ok && listType == currentGuildMedalInventoryListType {
|
||||
refreshMedals = true
|
||||
continue
|
||||
}
|
||||
var entry currentItemListEntry
|
||||
copy(entry.data[:], item.RawEntry)
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
body := buildCurrentItemUpdateBody(dnfrepo.MainInventoryListType, entries)
|
||||
s.logGameEvent(session, "game-spend-time-reward-item-update-send",
|
||||
"source", source,
|
||||
"event_id", currentSpendTimeEventID,
|
||||
"entry_count", len(entries),
|
||||
"body_len", len(body),
|
||||
"body_source", "online_event_atomic_claim_receipts_op14_raw77")
|
||||
if len(entries) > 0 {
|
||||
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketWalkoutPartyMember), body, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if refreshMedals {
|
||||
return s.sendCurrentGuardianGemList38Refresh(session, source)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) armCurrentSpendTimeTickLocked(session *gameSession) error {
|
||||
if s == nil || session == nil || session.spendTime.characterID == 0 || session.spendTime.catalog == nil {
|
||||
return nil
|
||||
}
|
||||
queue := s.ensureGameplayTimeQueue()
|
||||
if queue == nil {
|
||||
return errGameplayTimeQueueUnavailable
|
||||
}
|
||||
characterID, characterGeneration, err := gameSessionCharacterEventIdentity(session, session.spendTime.characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
timerName := fmt.Sprintf("dnf-spend-time:%s:%d", session.connID, characterID)
|
||||
generation := session.spendTime.generation
|
||||
session.spendTime.timerName = timerName
|
||||
return queue.ScheduleAfter(timerName, currentSpendTimeObserveTick, func(time.Time) {
|
||||
err := s.postGameSessionCharacterEvent(
|
||||
session,
|
||||
"spend-time-observation-tick",
|
||||
characterID,
|
||||
characterGeneration,
|
||||
func() error {
|
||||
s.runCurrentSpendTimeTick(session, generation)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil && !isClosedGameSessionEventError(err) {
|
||||
s.logPacketEvent("game-session-event-submit-failed",
|
||||
"conn_id", session.connID,
|
||||
"source", "spend-time-observation-tick",
|
||||
"char_id", characterID,
|
||||
"character_generation", characterGeneration,
|
||||
"error", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) runCurrentSpendTimeTick(session *gameSession, generation uint64) {
|
||||
if s == nil || session == nil {
|
||||
return
|
||||
}
|
||||
now := s.gameplayNow()
|
||||
session.spendTime.mu.Lock()
|
||||
defer session.spendTime.mu.Unlock()
|
||||
if session.spendTime.characterID == 0 || session.spendTime.catalog == nil ||
|
||||
session.spendTime.generation != generation {
|
||||
return
|
||||
}
|
||||
settlement, err := s.settleCurrentSpendTimeClockLocked(session, now)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-spend-time-observation-deferred",
|
||||
"char_id", session.spendTime.characterID,
|
||||
"generation", generation,
|
||||
"error", err)
|
||||
} else if session.spendTime.catalog.nativePCRoom != nil {
|
||||
if settlement.claimErr == nil {
|
||||
if sendErr := s.sendCurrentPCRoomTimePoint(session, session.spendTime.catalog, 0, -1); sendErr != nil {
|
||||
s.logGameEvent(session, "game-pcroom-timepoint-refresh-failed", "error", sendErr)
|
||||
}
|
||||
}
|
||||
} else if settlement.changed {
|
||||
body, buildErr := buildCurrentSpendTimeProgressBody(
|
||||
session.spendTime.onlineSeconds,
|
||||
session.spendTime.completedStages,
|
||||
)
|
||||
if buildErr != nil {
|
||||
s.logGameEvent(session, "game-spend-time-progress-build-failed", "error", buildErr)
|
||||
} else if sendErr := s.sendGameUpperRawClass(session, currentSpendTimeProgressMsgID, body, 0); sendErr != nil {
|
||||
s.logGameEvent(session, "game-spend-time-progress-send-failed", "error", sendErr)
|
||||
}
|
||||
if sendErr := s.sendCurrentSpendTimeRewardItemUpdates(
|
||||
session,
|
||||
settlement.rewardItems,
|
||||
"online_observation_tick",
|
||||
); sendErr != nil {
|
||||
s.logGameEvent(session, "game-spend-time-reward-item-update-failed", "error", sendErr)
|
||||
}
|
||||
}
|
||||
if settlement.claimErr != nil {
|
||||
s.logGameEvent(session, "game-spend-time-auto-award-deferred",
|
||||
"char_id", session.spendTime.characterID,
|
||||
"online_seconds", session.spendTime.onlineSeconds,
|
||||
"error", settlement.claimErr)
|
||||
}
|
||||
if err := s.sendCurrentMonthlyAttendance(session, false); err != nil {
|
||||
s.logGameEvent(session, "game-monthly-attendance-refresh-deferred", "error", err)
|
||||
}
|
||||
if err := s.sendCurrentWeeklyAttendance(session, false, false); err != nil {
|
||||
s.logGameEvent(session, "game-weekly-attendance-refresh-deferred", "error", err)
|
||||
}
|
||||
if err := s.sendCurrentReturneeState(session, false); err != nil {
|
||||
s.logGameEvent(session, "game-returnee-refresh-deferred", "error", err)
|
||||
}
|
||||
// 参考实现在这里调 item_lock_snapshot.go 的 refreshCurrentExpiredItemLocks。
|
||||
// 本树的 itemlock 是**另一套锁模型**(排序锁 + 账号金库锁,`sort_lock.go` /
|
||||
// `sort_lock_account_cargo.go`),没有参考版那套 72 小时 `UnlockWait` /
|
||||
// `equipment_lock_unlock_at` / HasExpiredLocks / ReconcileExpired,所以这里
|
||||
// 刻意不调用任何等价物:在活动 tick 里凭空造一个"过期解锁"语义,会把本树
|
||||
// 已有的排序锁状态机搅乱。真要那条语义,应当整套移植 itemlock 的 expiry.go
|
||||
// 并在 game_aligned.go 的登录路径上调用,而不是挂在活动时钟上。
|
||||
//
|
||||
// 全职LV预约得好礼(等级预约,活动 222)已关闭,故此处不再下发 op623。
|
||||
if err := s.armCurrentSpendTimeTickLocked(session); err != nil {
|
||||
s.logGameEvent(session, "game-spend-time-observation-rearm-deferred",
|
||||
"char_id", session.spendTime.characterID,
|
||||
"generation", generation,
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) settleCurrentSpendTimeClockLocked(
|
||||
session *gameSession,
|
||||
now time.Time,
|
||||
) (currentSpendTimeSettlementResult, error) {
|
||||
if session == nil || session.spendTime.characterID == 0 || session.spendTime.catalog == nil ||
|
||||
session.spendTime.anchor.IsZero() {
|
||||
return currentSpendTimeSettlementResult{}, nil
|
||||
}
|
||||
intervalTo := now.Truncate(time.Second)
|
||||
if !session.spendTime.anchor.Before(intervalTo) {
|
||||
return currentSpendTimeSettlementResult{}, nil
|
||||
}
|
||||
owner, err := s.currentSpendTimeOwner()
|
||||
if err != nil {
|
||||
return currentSpendTimeSettlementResult{}, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), currentSpendTimeWriteTimeout)
|
||||
defer cancel()
|
||||
// Labor Day has its own interval union and mail receipts. Settle before
|
||||
// advancing the shared observation cursor, including logout and 06:00.
|
||||
if err := s.settleCurrentLaborDay(ctx, session, session.spendTime.catalog.laborDay, session.spendTime.anchor, intervalTo, false); err != nil {
|
||||
return currentSpendTimeSettlementResult{}, err
|
||||
}
|
||||
definition := session.spendTime.catalog.definition
|
||||
characterID := strconv.Itoa(int(session.spendTime.characterID))
|
||||
cursor := session.spendTime.anchor
|
||||
previousCompleted := session.spendTime.completedStages
|
||||
changed := false
|
||||
rewardItems := make([]dnfonlineevent.CommittedItem, 0, currentSpendTimeStageCount)
|
||||
var snapshot dnfonlineevent.Snapshot
|
||||
var claimErr error
|
||||
for cursor.Before(intervalTo) {
|
||||
nextBoundary := currentSpendTimeNextServiceBoundary(definition, cursor)
|
||||
segmentEnd := intervalTo
|
||||
endsAtBoundary := false
|
||||
if !nextBoundary.After(segmentEnd) {
|
||||
segmentEnd = nextBoundary
|
||||
endsAtBoundary = true
|
||||
}
|
||||
result, err := owner.Observe(ctx, dnfonlineevent.ObserveCommand{
|
||||
AccountID: session.spendTime.accountID,
|
||||
CharacterID: characterID,
|
||||
Definition: definition,
|
||||
IntervalFrom: cursor,
|
||||
IntervalTo: segmentEnd,
|
||||
})
|
||||
if err != nil {
|
||||
return currentSpendTimeSettlementResult{}, err
|
||||
}
|
||||
changed = changed || result.Changed
|
||||
claimAt := segmentEnd
|
||||
if endsAtBoundary {
|
||||
// Claim the just-finished service day before a following Observe or
|
||||
// Status resets its daily ledger at exactly 06:00.
|
||||
claimAt = segmentEnd.Add(-time.Nanosecond)
|
||||
}
|
||||
var segmentItems []dnfonlineevent.CommittedItem
|
||||
snapshot, segmentItems, claimErr = s.claimCurrentSpendTimeUnlockedStages(
|
||||
ctx,
|
||||
owner,
|
||||
session.spendTime.accountID,
|
||||
characterID,
|
||||
session.spendTime.catalog,
|
||||
result.Snapshot,
|
||||
claimAt,
|
||||
)
|
||||
rewardItems = append(rewardItems, segmentItems...)
|
||||
session.spendTime.onlineSeconds = snapshot.OnlineSeconds
|
||||
session.spendTime.completedStages = currentSpendTimeClaimedStages(definition, snapshot)
|
||||
if claimErr != nil {
|
||||
// Do not cross the reset boundary until the old day's unlocked
|
||||
// rewards are durably settled. Re-observing the retained interval is
|
||||
// idempotent because the owner stores an interval union.
|
||||
if !endsAtBoundary {
|
||||
session.spendTime.anchor = segmentEnd
|
||||
}
|
||||
return currentSpendTimeSettlementResult{
|
||||
changed: changed || previousCompleted != session.spendTime.completedStages || len(rewardItems) != 0,
|
||||
rewardItems: rewardItems,
|
||||
claimErr: claimErr,
|
||||
}, nil
|
||||
}
|
||||
session.spendTime.anchor = segmentEnd
|
||||
cursor = segmentEnd
|
||||
|
||||
if endsAtBoundary && !cursor.Before(intervalTo) {
|
||||
priorDate := snapshot.CalendarDate
|
||||
snapshot, err = owner.Status(ctx, dnfonlineevent.StatusCommand{
|
||||
AccountID: session.spendTime.accountID,
|
||||
CharacterID: characterID,
|
||||
Definition: definition,
|
||||
ObservedAt: segmentEnd,
|
||||
})
|
||||
if err != nil {
|
||||
return currentSpendTimeSettlementResult{}, err
|
||||
}
|
||||
changed = changed || snapshot.CalendarDate != priorDate
|
||||
session.spendTime.onlineSeconds = snapshot.OnlineSeconds
|
||||
session.spendTime.completedStages = currentSpendTimeClaimedStages(definition, snapshot)
|
||||
}
|
||||
}
|
||||
return currentSpendTimeSettlementResult{
|
||||
changed: changed || previousCompleted != session.spendTime.completedStages || len(rewardItems) != 0,
|
||||
rewardItems: rewardItems,
|
||||
claimErr: claimErr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func currentSpendTimeNextServiceBoundary(
|
||||
definition dnfonlineevent.Definition,
|
||||
at time.Time,
|
||||
) time.Time {
|
||||
location := definition.Calendar
|
||||
if location == nil {
|
||||
location = dnfonlineevent.ChinaCalendar
|
||||
}
|
||||
boundary := dnfonlineevent.DailyBoundary{Hour: currentSpendTimeResetHour}
|
||||
if definition.Boundary != nil {
|
||||
boundary = *definition.Boundary
|
||||
}
|
||||
local := at.In(location)
|
||||
next := time.Date(
|
||||
local.Year(),
|
||||
local.Month(),
|
||||
local.Day(),
|
||||
boundary.Hour,
|
||||
boundary.Minute,
|
||||
boundary.Second,
|
||||
0,
|
||||
location,
|
||||
)
|
||||
if !at.Before(next) {
|
||||
next = next.AddDate(0, 0, 1)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func (s *Service) stopCurrentSpendTimeClock(session *gameSession, source string) {
|
||||
if s == nil || session == nil {
|
||||
return
|
||||
}
|
||||
session.spendTime.mu.Lock()
|
||||
if session.spendTime.characterID == 0 || session.spendTime.catalog == nil {
|
||||
session.spendTime.mu.Unlock()
|
||||
return
|
||||
}
|
||||
settlement, settleErr := s.settleCurrentSpendTimeClockLocked(session, s.gameplayNow())
|
||||
characterID := session.spendTime.characterID
|
||||
timerName := session.spendTime.timerName
|
||||
session.spendTime.generation++
|
||||
session.spendTime.characterID = 0
|
||||
session.spendTime.characterGeneration = 0
|
||||
session.spendTime.initializedCharacterGeneration = 0
|
||||
session.spendTime.accountID = ""
|
||||
session.spendTime.anchor = time.Time{}
|
||||
session.spendTime.timerName = ""
|
||||
session.spendTime.onlineSeconds = 0
|
||||
session.spendTime.completedStages = 0
|
||||
session.spendTime.catalog = nil
|
||||
session.spendTime.mu.Unlock()
|
||||
if timerName != "" {
|
||||
if queue := s.ensureGameplayTimeQueue(); queue != nil {
|
||||
queue.Cancel(timerName)
|
||||
}
|
||||
}
|
||||
if settleErr != nil {
|
||||
s.logGameEvent(session, "game-spend-time-stop-settlement-failed",
|
||||
"char_id", characterID,
|
||||
"source", source,
|
||||
"error", settleErr)
|
||||
}
|
||||
if settlement.claimErr != nil {
|
||||
s.logGameEvent(session, "game-spend-time-stop-auto-award-deferred",
|
||||
"char_id", characterID,
|
||||
"source", source,
|
||||
"error", settlement.claimErr)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user