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
@@ -0,0 +1,472 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
"longheng.io/server/internal/modules/dnf/raid"
|
||||
)
|
||||
|
||||
var errRuntimeRaidRejoinSessionChanged = errors.New("runtime raid rejoin session character changed")
|
||||
|
||||
func normalizeRuntimeRaidRejoinError(err error) error {
|
||||
if errors.Is(err, errRuntimeRaidRejoinSessionChanged) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) runtimeRaidRejoinNow() time.Time {
|
||||
return runtimeRaidNormalizeTime(s.gameplayNow())
|
||||
}
|
||||
|
||||
// sendRuntimeRaidSceneRejoinOnce restores the server-owned attack-party state
|
||||
// after the current EXE has finished its character-select handler. The current
|
||||
// client does not issue C2S RejoinRaid when it reconnects directly into an
|
||||
// Anton channel, so waiting for that request leaves the native manager empty
|
||||
// even though the raid and its progress are still alive in the bridge.
|
||||
//
|
||||
// This is a projection only: it never admits a member, replaces a raid, resets
|
||||
// lifecycle progress, charges entry cost, or replays the phase-start title.
|
||||
func (s *Service) sendRuntimeRaidSceneRejoinOnce(session *gameSession, source string) error {
|
||||
if s == nil || session == nil || !isCurrentRaidDirectoryChannel(session) {
|
||||
return nil
|
||||
}
|
||||
identity, ok := s.boundGameSessionCharacterSnapshot(session)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
state, ok := s.runtimeRaidForMember(identity.character)
|
||||
if !ok {
|
||||
clearRuntimeRaidRejoinProjection(session)
|
||||
return nil
|
||||
}
|
||||
// The character is back on a live session: release any disconnect
|
||||
// reservation and show the row as online again before restoring state.
|
||||
s.restoreRuntimeRaidPresence(session, source)
|
||||
if runtimeRaidRejoinProjectionMatches(session, state.RaidKey, state.Lifecycle.Revision) {
|
||||
return nil
|
||||
}
|
||||
if err := normalizeRuntimeRaidRejoinError(s.sendRuntimeRaidRejoinRecovery(
|
||||
identity,
|
||||
state,
|
||||
s.runtimeRaidRejoinNow(),
|
||||
)); err != nil {
|
||||
// This is a UI restore, not a session prerequisite: the player is already
|
||||
// back in the raid channel, and a stale raid window is recoverable (the
|
||||
// client re-queries, or the next roster change refreshes it), while a
|
||||
// closed socket is not. Returning the error would travel up through
|
||||
// handleGameUpper into the connection read loop, which treats it as fatal
|
||||
// and closes the connection -- the client shows "网络连接中断". Report it
|
||||
// and carry on.
|
||||
s.logGameEvent(session, "game-raid-scene-rejoin-failed",
|
||||
"source", source,
|
||||
"raid_key", state.RaidKey,
|
||||
"char_id", identity.character,
|
||||
"lifecycle_status", byte(state.Lifecycle.Status),
|
||||
"error", err)
|
||||
return nil
|
||||
}
|
||||
if !s.boundGameSessionCharacterCurrent(identity) {
|
||||
return nil
|
||||
}
|
||||
storeRuntimeRaidRejoinProjection(session, state.RaidKey, state.Lifecycle.Revision)
|
||||
s.logGameEvent(session, "game-raid-scene-rejoin-recovery-sent",
|
||||
"source", source,
|
||||
"raid_key", state.RaidKey,
|
||||
"leader_id", state.LeaderID,
|
||||
"member_count", len(state.Members),
|
||||
"frozen_member_count", len(state.FrozenMembers),
|
||||
"tail_flag", state.TailFlag,
|
||||
"single_raid_type", state.SingleRaidType,
|
||||
"started", state.Started,
|
||||
"entry_cost_paid", state.EntryCostPaid,
|
||||
"lifecycle_status", byte(state.Lifecycle.Status),
|
||||
"lifecycle_phase", state.Lifecycle.Phase,
|
||||
"lifecycle_revision", state.Lifecycle.Revision,
|
||||
"reason", "current_exe_does_not_emit_rejoin_raid_after_direct_anton_channel_login")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinPacket(
|
||||
identity boundGameSessionCharacter,
|
||||
msgID uint16,
|
||||
body []byte,
|
||||
classification byte,
|
||||
) error {
|
||||
if !s.boundGameSessionCharacterCurrent(identity) {
|
||||
s.logPacketEvent("game-raid-rejoin-projection-stopped",
|
||||
"char_id", identity.character,
|
||||
"msg_id", msgID,
|
||||
"reason", "session_character_or_generation_changed")
|
||||
return errRuntimeRaidRejoinSessionChanged
|
||||
}
|
||||
return s.sendGameUpperRawClassCodec(identity.session, msgID, body, classification, true)
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinRecovery(
|
||||
identity boundGameSessionCharacter,
|
||||
state runtimeRaidState,
|
||||
now time.Time,
|
||||
) error {
|
||||
if identity.character == 0 || runtimeRaidMemberIndex(state, identity.character) < 0 {
|
||||
return errRuntimeRaidNotMember
|
||||
}
|
||||
// A single Raid never owns an ordinary party, before or after START_RAID.
|
||||
// Clear stale projections from older processes before restoring the
|
||||
// server-owned Raid object.
|
||||
//
|
||||
// 超时空同理,但多一个条件:只有人不在副本里时才清 —— 他要是正在区域里打,
|
||||
// 那份普通队伍投影正是他此刻战斗用的,清掉会把队友一起打散。
|
||||
identity.session.dungeon.mu.Lock()
|
||||
inDungeon := identity.session.dungeon.runtime != nil
|
||||
identity.session.dungeon.mu.Unlock()
|
||||
if state.TailFlag == 1 || (state.RaidMode == runtimeRaidOwnerFiendWar && !inDungeon) {
|
||||
if err := s.clearSingleRaidOrdinaryPartyProjection(identity.session, "single_raid_rejoin"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.sendRuntimeRaidRejoinBaseSnapshot(identity, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch state.Lifecycle.Status {
|
||||
case runtimeRaidLifecyclePreparing:
|
||||
return s.sendRuntimeRaidRejoinPreparing(identity, state, now)
|
||||
case runtimeRaidLifecyclePhaseActive:
|
||||
return s.sendRuntimeRaidRejoinPhaseActive(identity, state, now)
|
||||
case runtimeRaidLifecycleAwaitingMovie:
|
||||
return s.sendRuntimeRaidRejoinAwaitingMovie(identity, state)
|
||||
case runtimeRaidLifecycleAwaitingRewards:
|
||||
return s.sendRuntimeRaidRejoinAwaitingRewards(identity, state, now)
|
||||
case runtimeRaidLifecycleStandby:
|
||||
return s.sendRuntimeRaidRejoinStandby(identity, state, now)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinBaseSnapshot(
|
||||
identity boundGameSessionCharacter,
|
||||
state runtimeRaidState,
|
||||
) error {
|
||||
objectBody, err := raid.BuildRaidObjectMode0Body(runtimeRaidObjectMode0(state))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refreshBody, err := raid.BuildMemberRefreshMode3Body(runtimeRaidMemberRefresh(state))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buffBody, err := s.runtimeRaidBuffStatusBody(state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notifications := []runtimeRaidWireNotification{
|
||||
{msgID: raid.RaidObjectMsgID, body: objectBody},
|
||||
{msgID: raid.RaidMemberRefreshMsgID, body: refreshBody},
|
||||
{msgID: raid.RaidBuffMsgID, body: buffBody},
|
||||
{msgID: raid.RaidWaitingListMsgID, body: runtimeRaidWaitingListBody(state)},
|
||||
{msgID: raid.RaidEntryCostMsgID, body: s.runtimeRaidEntryCostSnapshotBody(state)},
|
||||
}
|
||||
if err := s.sendRuntimeRaidRejoinSequence(identity, notifications...); err != nil {
|
||||
return err
|
||||
}
|
||||
// 超时空不投影普通队伍的"重连快照":它的小队是由 encounter 路径建的托管队伍,
|
||||
// 重连时交给战况表去表达,别用单人那条路再塑一次。
|
||||
if !state.Started || state.TailFlag == 1 || state.RaidMode == runtimeRaidOwnerFiendWar {
|
||||
return nil
|
||||
}
|
||||
return s.sendRuntimeRaidRejoinPartySnapshot(identity, state)
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinPartySnapshot(
|
||||
identity boundGameSessionCharacter,
|
||||
state runtimeRaidState,
|
||||
) error {
|
||||
partyState, ok := runtimeRaidRejoinPartyState(state, identity.character)
|
||||
if !ok {
|
||||
// An unassigned member is a first-class state, not an error: a joiner
|
||||
// lands in the standby group (GroupIndex 0) and stays there until the
|
||||
// leader assigns a squad, and runtimeRaidPartyGroups deliberately skips
|
||||
// that group. Such a member has no party to restore -- the ordinary town
|
||||
// route already published the partyless op9 row (party_id=0) -- so the
|
||||
// party step is skipped. Returning errRuntimeRaidNotMember here used to
|
||||
// travel all the way up through handleGameUpper into the connection read
|
||||
// loop and close the socket, which is what made a rejoining standby
|
||||
// member see "网络连接中断" the moment they entered the raid channel.
|
||||
s.logGameEvent(identity.session, "game-raid-rejoin-party-skipped",
|
||||
"char_id", identity.character,
|
||||
"raid_key", state.RaidKey,
|
||||
"reason", "member_is_in_the_standby_group")
|
||||
return nil
|
||||
}
|
||||
if !s.boundGameSessionCharacterCurrent(identity) {
|
||||
return errRuntimeRaidRejoinSessionChanged
|
||||
}
|
||||
storeRuntimePartyState(identity.session, partyState)
|
||||
// Rebuild the central party graph as well as the per-session client cache.
|
||||
// Disconnecting retires the old session generation from RuntimePartyManager;
|
||||
// leaving only the cached op13 projection would make later dungeon/leave
|
||||
// mutations treat the reconnected raid member as partyless.
|
||||
if managed, restored := s.bootstrapManagedRuntimePartyForSession(identity.session); restored {
|
||||
partyState = managed.StateFor(identity.character)
|
||||
}
|
||||
return s.sendRuntimePartySnapshot(identity.session, partyState)
|
||||
}
|
||||
|
||||
func runtimeRaidRejoinPartyState(state runtimeRaidState, characterID uint16) (alignedcmd.PartyState, bool) {
|
||||
for _, group := range runtimeRaidPartyGroups(state) {
|
||||
for _, member := range group {
|
||||
if member.CharID == characterID {
|
||||
return runtimeRaidGroupPartyState(state, group), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return alignedcmd.PartyState{}, false
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinPreparing(
|
||||
identity boundGameSessionCharacter,
|
||||
state runtimeRaidState,
|
||||
now time.Time,
|
||||
) error {
|
||||
if state.Lifecycle.ReadyDeadline.IsZero() {
|
||||
return errRuntimeRaidLifecycleState
|
||||
}
|
||||
seconds := runtimeRaidRejoinRemainingSeconds(state.Lifecycle.ReadyDeadline, now)
|
||||
timer := raid.RaidTimer{
|
||||
TimerType: 0,
|
||||
Value0: runtimeRaidTopTimerKey,
|
||||
Value1: uint32(state.Lifecycle.ReadyDeadline.Unix()),
|
||||
Value2: runtimeRaidDurationSeconds(state.Lifecycle.ReadyDeadline.Sub(state.Lifecycle.StartedAt)),
|
||||
}
|
||||
if err := s.sendRuntimeRaidRejoinSequence(identity,
|
||||
runtimeRaidWireNotification{msgID: raid.RaidTimerMsgID, body: raid.BuildRaidTimerBody(timer)},
|
||||
runtimeRaidWireNotification{
|
||||
msgID: raid.RaidRemainTimeMsgID,
|
||||
body: raid.BuildRaidRemainTimeBody(raid.RaidRemainTime{TimerType: 0, Seconds: seconds}),
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.scheduleRuntimeRaidReady(state)
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinPhaseActive(
|
||||
identity boundGameSessionCharacter,
|
||||
state runtimeRaidState,
|
||||
now time.Time,
|
||||
) error {
|
||||
// 超时空重连恢复就是"按当前战场状态重发一遍战况表";0x0247/0x0249 那一套是
|
||||
// 安徒恩/卢克的格式,这里整条绕开。
|
||||
if state.RaidMode == runtimeRaidOwnerFiendWar {
|
||||
return s.sendRuntimeFiendWarBoard(identity.session, state, false, now)
|
||||
}
|
||||
symbolBody, dungeonBody, _, err := runtimeRaidPhaseSnapshotBodies(state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seconds := runtimeRaidDurationSeconds(runtimeRaidPhaseSnapshotRemaining(state, now))
|
||||
phaseDuration := state.Lifecycle.PhaseDeadline.Sub(state.Lifecycle.PhaseStartedAt)
|
||||
if phaseDuration < 0 {
|
||||
phaseDuration = 0
|
||||
}
|
||||
// Current EXE sub_F176A0 initializes the selector on op587 state=2 and
|
||||
// sub_F163C0 reads the dungeon manager during that initialization. Rejoin
|
||||
// recovery must therefore preload op583/op585 before materializing the view.
|
||||
notifications := []runtimeRaidWireNotification{
|
||||
{msgID: raid.RaidSymbolMsgID, body: symbolBody},
|
||||
{msgID: raid.RaidDungeonStateMsgID, body: dungeonBody},
|
||||
{
|
||||
msgID: raid.RaidStateMsgID,
|
||||
body: raid.BuildRaidStateBody(raid.RaidState{
|
||||
State: runtimeRaidPhaseStartState,
|
||||
Argument: int8(state.Lifecycle.Phase - 1),
|
||||
}),
|
||||
},
|
||||
}
|
||||
notifications = append(notifications,
|
||||
runtimeRaidWireNotification{
|
||||
msgID: raid.RaidTimerMsgID,
|
||||
body: raid.BuildRaidTimerBody(raid.RaidTimer{
|
||||
TimerType: 0,
|
||||
Value0: runtimeRaidTopTimerKey,
|
||||
Value1: uint32(state.Lifecycle.PhaseDeadline.Unix()),
|
||||
Value2: runtimeRaidDurationSeconds(phaseDuration),
|
||||
}),
|
||||
},
|
||||
runtimeRaidWireNotification{
|
||||
msgID: raid.RaidRemainTimeMsgID,
|
||||
body: raid.BuildRaidRemainTimeBody(raid.RaidRemainTime{TimerType: 0, Seconds: seconds}),
|
||||
},
|
||||
)
|
||||
notifications = append(notifications, runtimeRaidMechanismTimerNotifications(state, now)...)
|
||||
if err := s.sendRuntimeRaidRejoinSequence(identity, notifications...); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.scheduleRuntimeRaidPhaseExpiry(state)
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinAwaitingMovie(
|
||||
identity boundGameSessionCharacter,
|
||||
state runtimeRaidState,
|
||||
) error {
|
||||
wirePhase, err := runtimeRaidSettlementWirePhase(state.Lifecycle.Phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clearDuration := state.Lifecycle.MovieOpenedAt.Sub(state.Lifecycle.PhaseStartedAt)
|
||||
if state.Lifecycle.MovieOpenedAt.IsZero() || state.Lifecycle.PhaseStartedAt.IsZero() || clearDuration < 0 {
|
||||
clearDuration = 0
|
||||
}
|
||||
resultBody, err := raid.BuildRaidSettlementResultBody(raid.RaidSettlementResult{
|
||||
ResultType: 0,
|
||||
PhaseIndex: wirePhase,
|
||||
ClearSeconds: runtimeRaidDurationSeconds(clearDuration),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.sendRuntimeRaidRejoinSequence(identity,
|
||||
runtimeRaidWireNotification{msgID: raid.RaidResultMsgID, body: resultBody},
|
||||
runtimeRaidWireNotification{
|
||||
msgID: raid.RaidStateMsgID,
|
||||
body: raid.BuildRaidStateBody(raid.RaidState{
|
||||
State: runtimeRaidSettlementStateResult,
|
||||
Argument: int8(wirePhase),
|
||||
}),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinAwaitingRewards(
|
||||
identity boundGameSessionCharacter,
|
||||
state runtimeRaidState,
|
||||
now time.Time,
|
||||
) error {
|
||||
wirePhase, err := runtimeRaidSettlementWirePhase(state.Lifecycle.Phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deadline := state.Lifecycle.RewardOpenedAt.Add(runtimeRaidRewardOpenCountdown)
|
||||
if !state.Lifecycle.RewardOpenedAt.IsZero() && now.Before(deadline) {
|
||||
seconds := runtimeRaidRejoinRemainingSeconds(deadline, now)
|
||||
if err := s.sendRuntimeRaidRejoinSequence(identity,
|
||||
runtimeRaidWireNotification{
|
||||
msgID: raid.RaidStateMsgID,
|
||||
body: raid.BuildRaidStateBody(raid.RaidState{
|
||||
State: runtimeRaidSettlementStateMovieDone,
|
||||
Argument: int8(wirePhase),
|
||||
}),
|
||||
},
|
||||
runtimeRaidWireNotification{
|
||||
msgID: raid.RaidTimerMsgID,
|
||||
body: raid.BuildRaidTimerBody(raid.RaidTimer{
|
||||
TimerType: 0,
|
||||
Value0: runtimeRaidTopTimerKey,
|
||||
Value1: uint32(deadline.Unix()),
|
||||
Value2: runtimeRaidDurationSeconds(runtimeRaidRewardOpenCountdown),
|
||||
}),
|
||||
},
|
||||
runtimeRaidWireNotification{
|
||||
msgID: raid.RaidRemainTimeMsgID,
|
||||
body: raid.BuildRaidRemainTimeBody(raid.RaidRemainTime{TimerType: 0, Seconds: seconds}),
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.scheduleRuntimeRaidCardsOpened(state, deadline)
|
||||
}
|
||||
if err := s.sendRuntimeRaidRejoinPacket(
|
||||
identity,
|
||||
raid.RaidStateMsgID,
|
||||
raid.BuildRaidStateBody(raid.RaidState{State: runtimeRaidSettlementStateCardsOpened, Argument: int8(wirePhase)}),
|
||||
0,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.sendRuntimeRaidRejoinCachedReward(identity, state)
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinStandby(
|
||||
identity boundGameSessionCharacter,
|
||||
state runtimeRaidState,
|
||||
now time.Time,
|
||||
) error {
|
||||
wirePhase, err := runtimeRaidSettlementWirePhase(state.Lifecycle.Phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendRuntimeRaidRejoinPacket(
|
||||
identity,
|
||||
raid.RaidStateMsgID,
|
||||
raid.BuildRaidStateBody(raid.RaidState{State: runtimeRaidSettlementStateCardsOpened, Argument: int8(wirePhase)}),
|
||||
0,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendRuntimeRaidRejoinCachedReward(identity, state); err != nil {
|
||||
return err
|
||||
}
|
||||
if state.Lifecycle.StandbyDeadline.IsZero() {
|
||||
return errRuntimeRaidLifecycleState
|
||||
}
|
||||
seconds := runtimeRaidRejoinRemainingSeconds(state.Lifecycle.StandbyDeadline, now)
|
||||
timer := raid.RaidTimer{
|
||||
TimerType: 1,
|
||||
Value0: state.RaidKey,
|
||||
Value1: uint32(state.Lifecycle.StandbyDeadline.Unix()),
|
||||
Value2: seconds,
|
||||
}
|
||||
if err := s.sendRuntimeRaidRejoinSequence(identity,
|
||||
runtimeRaidWireNotification{msgID: raid.RaidTimerMsgID, body: raid.BuildRaidTimerBody(timer)},
|
||||
runtimeRaidWireNotification{
|
||||
msgID: raid.RaidRemainTimeMsgID,
|
||||
body: raid.BuildRaidRemainTimeBody(raid.RaidRemainTime{TimerType: 1, Seconds: seconds}),
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.scheduleRuntimeRaidStandby(state)
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinCachedReward(
|
||||
identity boundGameSessionCharacter,
|
||||
state runtimeRaidState,
|
||||
) error {
|
||||
cache := state.Lifecycle.RewardDelivery
|
||||
if cache == nil {
|
||||
return nil
|
||||
}
|
||||
body := append([]byte(nil), cache.RewardBody...)
|
||||
if len(body) == 0 {
|
||||
var err error
|
||||
body, err = raid.BuildRaidSettlementRewardsBody(cache.Rewards)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.sendRuntimeRaidRejoinPacket(identity, raid.RaidRewardListMsgID, body, 0)
|
||||
}
|
||||
|
||||
func (s *Service) sendRuntimeRaidRejoinSequence(
|
||||
identity boundGameSessionCharacter,
|
||||
notifications ...runtimeRaidWireNotification,
|
||||
) error {
|
||||
for _, notification := range notifications {
|
||||
if err := s.sendRuntimeRaidRejoinPacket(identity, notification.msgID, notification.body, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runtimeRaidRejoinRemainingSeconds(deadline time.Time, now time.Time) uint32 {
|
||||
now = runtimeRaidNormalizeTime(now)
|
||||
if deadline.IsZero() || !deadline.After(now) {
|
||||
return 0
|
||||
}
|
||||
return runtimeRaidDurationSeconds(deadline.Sub(now))
|
||||
}
|
||||
Reference in new issue
Block a user