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,364 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dnffatigue "longheng.io/server/internal/modules/dnf/fatigue"
|
||||
"longheng.io/server/internal/modules/dnf/premium"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const currentDailyFatigueResetRetryDelay = time.Minute
|
||||
|
||||
var currentDailyFatigueResetLocation = time.FixedZone("DNF-Beijing", 8*60*60)
|
||||
|
||||
type currentDailyFatigueResetClock struct {
|
||||
mu sync.Mutex
|
||||
generation uint64
|
||||
timerName string
|
||||
dueAt time.Time
|
||||
characterID uint16
|
||||
characterGeneration uint64
|
||||
}
|
||||
|
||||
func (s *Service) reconcileCurrentDailyFatigueSelection(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
) (dnffatigue.ReconcileDailyResult, error) {
|
||||
return s.reconcileCurrentDailyFatigueSelectionAt(ctx, session, s.gameplayNow().UTC())
|
||||
}
|
||||
|
||||
func (s *Service) reconcileCurrentDailyFatigueSelectionAt(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
now time.Time,
|
||||
) (dnffatigue.ReconcileDailyResult, error) {
|
||||
if s == nil || session == nil || session.selectedCharacterID == 0 {
|
||||
return dnffatigue.ReconcileDailyResult{}, nil
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok {
|
||||
return dnffatigue.ReconcileDailyResult{}, nil
|
||||
}
|
||||
owner, err := dnffatigue.NewOwner(repositories)
|
||||
if err != nil {
|
||||
return dnffatigue.ReconcileDailyResult{}, err
|
||||
}
|
||||
result, err := owner.ReconcileDaily(ctx, dnffatigue.ReconcileDailyCommand{
|
||||
AccountID: s.accountIDForSession(session),
|
||||
CharacterID: strconv.FormatUint(uint64(session.selectedCharacterID), 10),
|
||||
UpdatedAt: now.UTC(),
|
||||
})
|
||||
if err != nil {
|
||||
return dnffatigue.ReconcileDailyResult{}, fmt.Errorf("reconcile daily fatigue selection: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) reconcileCurrentDailyFatigueSelectionIfNeeded(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
character dnfrepo.CharacterRecord,
|
||||
) (dnfrepo.CharacterRecord, bool, error) {
|
||||
if character.Stats == nil {
|
||||
return character, false, nil
|
||||
}
|
||||
_, recoveryInitialized := character.Stats[dnffatigue.DailyRecoveryDayStatKey]
|
||||
_, potionInitialized := character.Stats[dnffatigue.PotionUsageDayStatKey]
|
||||
if !recoveryInitialized && !potionInitialized {
|
||||
return character, false, nil
|
||||
}
|
||||
dayID := premium.ServiceDayID(s.gameplayNow())
|
||||
if character.Stats[dnffatigue.DailyRecoveryDayStatKey] == dayID &&
|
||||
character.Stats[dnffatigue.PotionUsageDayStatKey] == dayID {
|
||||
return character, false, nil
|
||||
}
|
||||
result, err := s.reconcileCurrentDailyFatigueSelection(ctx, session)
|
||||
if err != nil || result.Character.CharacterID == "" {
|
||||
return character, false, err
|
||||
}
|
||||
character.Stats = result.Character.Stats
|
||||
character.UpdatedAt = result.Character.UpdatedAt
|
||||
return character, result.Changed, nil
|
||||
}
|
||||
|
||||
func nextCurrentDailyFatigueResetBoundary(now time.Time) time.Time {
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
local := now.In(currentDailyFatigueResetLocation)
|
||||
boundary := time.Date(local.Year(), local.Month(), local.Day(), 6, 0, 0, 0, currentDailyFatigueResetLocation)
|
||||
if !local.Before(boundary) {
|
||||
boundary = boundary.AddDate(0, 0, 1)
|
||||
}
|
||||
return boundary.UTC()
|
||||
}
|
||||
|
||||
func (s *Service) armCurrentDailyFatigueReset(session *gameSession) error {
|
||||
queue := s.ensureGameplayTimeQueue()
|
||||
if queue == nil {
|
||||
return errGameplayTimeQueueUnavailable
|
||||
}
|
||||
return s.scheduleCurrentDailyFatigueResetAt(
|
||||
session,
|
||||
nextCurrentDailyFatigueResetBoundary(queue.Now()),
|
||||
"next_beijing_0600",
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) retryCurrentDailyFatigueReset(session *gameSession) error {
|
||||
queue := s.ensureGameplayTimeQueue()
|
||||
if queue == nil {
|
||||
return errGameplayTimeQueueUnavailable
|
||||
}
|
||||
return s.scheduleCurrentDailyFatigueResetAt(
|
||||
session,
|
||||
queue.Now().Add(currentDailyFatigueResetRetryDelay),
|
||||
"repository_retry",
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) scheduleCurrentDailyFatigueResetAt(
|
||||
session *gameSession,
|
||||
dueAt time.Time,
|
||||
source string,
|
||||
) error {
|
||||
if s == nil || session == nil || session.selectedCharacterID == 0 {
|
||||
return nil
|
||||
}
|
||||
queue := s.ensureGameplayTimeQueue()
|
||||
if queue == nil {
|
||||
return errGameplayTimeQueueUnavailable
|
||||
}
|
||||
characterID, characterGeneration, err := gameSessionCharacterEventIdentity(session, session.selectedCharacterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := queue.Now().UTC()
|
||||
dueAt = dueAt.UTC()
|
||||
if dueAt.Before(now) {
|
||||
dueAt = now
|
||||
}
|
||||
delay := dueAt.Sub(now)
|
||||
|
||||
clock := &session.dailyFatigueReset
|
||||
clock.mu.Lock()
|
||||
oldTimerName := clock.timerName
|
||||
clock.generation++
|
||||
if clock.generation == 0 {
|
||||
clock.generation++
|
||||
}
|
||||
timerGeneration := clock.generation
|
||||
timerName := fmt.Sprintf(
|
||||
"dnf-daily-fatigue:%s:char:%d:session:%d:timer:%d",
|
||||
session.connID,
|
||||
characterID,
|
||||
characterGeneration,
|
||||
timerGeneration,
|
||||
)
|
||||
clock.timerName = timerName
|
||||
clock.dueAt = dueAt
|
||||
clock.characterID = characterID
|
||||
clock.characterGeneration = characterGeneration
|
||||
clock.mu.Unlock()
|
||||
if oldTimerName != "" {
|
||||
queue.Cancel(oldTimerName)
|
||||
}
|
||||
|
||||
err = queue.ScheduleAfter(timerName, delay, func(callbackTime time.Time) {
|
||||
if callbackTime.IsZero() || callbackTime.Before(dueAt) {
|
||||
callbackTime = dueAt
|
||||
}
|
||||
postErr := s.postGameSessionCharacterEvent(
|
||||
session,
|
||||
"daily-fatigue-reset-timequeue",
|
||||
characterID,
|
||||
characterGeneration,
|
||||
func() error {
|
||||
s.runCurrentDailyFatigueReset(session, timerGeneration, callbackTime.UTC())
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if postErr != nil && !isClosedGameSessionEventError(postErr) {
|
||||
s.logPacketEvent("game-session-event-submit-failed",
|
||||
"conn_id", session.connID,
|
||||
"source", "daily-fatigue-reset-timequeue",
|
||||
"char_id", characterID,
|
||||
"character_generation", characterGeneration,
|
||||
"error", postErr)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
clock.mu.Lock()
|
||||
if clock.generation == timerGeneration {
|
||||
clock.timerName = ""
|
||||
clock.dueAt = time.Time{}
|
||||
clock.characterID = 0
|
||||
clock.characterGeneration = 0
|
||||
}
|
||||
clock.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
s.logGameEvent(session, "game-daily-fatigue-reset-timer-scheduled",
|
||||
"char_id", characterID,
|
||||
"character_generation", characterGeneration,
|
||||
"timer_name", timerName,
|
||||
"due_at_utc", dueAt.Format(time.RFC3339Nano),
|
||||
"source", source)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) runCurrentDailyFatigueReset(
|
||||
session *gameSession,
|
||||
timerGeneration uint64,
|
||||
now time.Time,
|
||||
) {
|
||||
if s == nil || session == nil {
|
||||
return
|
||||
}
|
||||
clock := &session.dailyFatigueReset
|
||||
clock.mu.Lock()
|
||||
if clock.generation != timerGeneration ||
|
||||
clock.characterID == 0 ||
|
||||
clock.characterID != session.selectedCharacterID ||
|
||||
clock.characterGeneration != session.characterGeneration {
|
||||
clock.mu.Unlock()
|
||||
return
|
||||
}
|
||||
clock.timerName = ""
|
||||
clock.dueAt = time.Time{}
|
||||
clock.mu.Unlock()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
_, _, selected, found := s.characterForEnter(ctx, session, session.selectedCharacterID)
|
||||
if !found {
|
||||
cancel()
|
||||
s.deferCurrentDailyFatigueReset(session, "selected_character_not_found", nil)
|
||||
return
|
||||
}
|
||||
if selected.Stats != nil {
|
||||
limit := selected.Stats["fatigue_limit"]
|
||||
if limit == dnffatigue.DefaultLimit || limit == dnffatigue.BlackDiamondLimit {
|
||||
if _, err := s.reconcileCurrentBlackDiamondSelection(ctx, session, selected); err != nil {
|
||||
cancel()
|
||||
s.deferCurrentDailyFatigueReset(session, "black_diamond_reconcile_failed", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
result, err := s.reconcileCurrentDailyFatigueSelectionAt(ctx, session, now)
|
||||
if err != nil || result.Character.CharacterID == "" {
|
||||
cancel()
|
||||
s.deferCurrentDailyFatigueReset(session, "daily_reconcile_failed", err)
|
||||
return
|
||||
}
|
||||
dailyDungeonResult, dailyDungeonErr := s.reconcileCurrentDailyDungeonSelectionAt(ctx, session, now)
|
||||
cancel()
|
||||
if dailyDungeonErr != nil {
|
||||
s.deferCurrentDailyFatigueReset(session, "daily_dungeon_reconcile_failed", dailyDungeonErr)
|
||||
return
|
||||
}
|
||||
if result.Changed {
|
||||
if err := s.sendGameUpperRawClass(
|
||||
session,
|
||||
currentFatigueMsgID,
|
||||
buildCurrentFatigueBody(result.Character, true),
|
||||
0,
|
||||
); err != nil {
|
||||
s.logGameEvent(session, "game-daily-fatigue-reset-projection-deferred",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"service_day_id", result.ServiceDayID,
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
if refreshes := currentDailyDungeonInvitationRefreshes(dailyDungeonResult); len(refreshes) != 0 {
|
||||
if err := s.sendSelectedIncrementalItemSlotRefreshes(session, "daily_dungeon_invitation_grant", refreshes); err != nil {
|
||||
s.logGameEvent(session, "game-daily-dungeon-invitation-projection-deferred",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"service_window_start", dailyDungeonResult.Window.Start.Format(time.RFC3339),
|
||||
"refresh_count", len(refreshes),
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
if !dailyDungeonResult.Window.Start.IsZero() {
|
||||
if err := s.sendCurrentRaidDailyAvailability(session, "beijing_0600_daily_reset"); err != nil {
|
||||
s.logGameEvent(session, "game-daily-dungeon-count-projection-deferred",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"service_window_start", dailyDungeonResult.Window.Start.Format(time.RFC3339),
|
||||
"error", err)
|
||||
}
|
||||
// The same service-window boundary rolls the account AND character weekly
|
||||
// admission counters back to zero (dungeonadmission resets the weekly
|
||||
// policies on this window), and the client keeps whatever the last op1206
|
||||
// record wrote. Re-push it, or a player who is already online at 06:00
|
||||
// keeps reading last week's number -- the mirror image of the bug this
|
||||
// packet fixes, and just as misleading.
|
||||
if err := s.sendCurrentRaidWeeklySchedules(session, "beijing_0600_daily_reset"); err != nil {
|
||||
s.logGameEvent(session, "game-raid-weekly-schedule-projection-deferred",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"service_window_start", dailyDungeonResult.Window.Start.Format(time.RFC3339),
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
s.logGameEvent(session, "game-daily-fatigue-reset-timequeue-reconciled",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"service_day_id", result.ServiceDayID,
|
||||
"changed", result.Changed,
|
||||
"fatigue_restored", result.FatigueRestored,
|
||||
"potion_usage_reset", result.PotionUsageReset,
|
||||
"fatigue", result.Character.Stats["fatigue"],
|
||||
"fatigue_limit", result.Character.Stats["fatigue_limit"],
|
||||
"daily_dungeon_counters_changed", dailyDungeonResult.CountersChanged,
|
||||
"daily_dungeon_reset_policy_count", len(dailyDungeonResult.ResetPolicyKeys),
|
||||
"daily_invitation_inventory_changed", dailyDungeonResult.InventoryChanged)
|
||||
if err := s.armCurrentDailyFatigueReset(session); err != nil {
|
||||
s.logGameEvent(session, "game-daily-fatigue-reset-timer-rearm-deferred",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) deferCurrentDailyFatigueReset(session *gameSession, reason string, reconcileErr error) {
|
||||
s.logGameEvent(session, "game-daily-fatigue-reset-timequeue-deferred",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"reason", reason,
|
||||
"error", reconcileErr)
|
||||
if err := s.retryCurrentDailyFatigueReset(session); err != nil {
|
||||
s.logGameEvent(session, "game-daily-fatigue-reset-retry-schedule-failed",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"reason", reason,
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) stopCurrentDailyFatigueReset(session *gameSession, source string) {
|
||||
if s == nil || session == nil {
|
||||
return
|
||||
}
|
||||
clock := &session.dailyFatigueReset
|
||||
clock.mu.Lock()
|
||||
timerName := clock.timerName
|
||||
characterID := clock.characterID
|
||||
clock.generation++
|
||||
if clock.generation == 0 {
|
||||
clock.generation++
|
||||
}
|
||||
clock.timerName = ""
|
||||
clock.dueAt = time.Time{}
|
||||
clock.characterID = 0
|
||||
clock.characterGeneration = 0
|
||||
clock.mu.Unlock()
|
||||
if timerName != "" {
|
||||
if queue := s.ensureGameplayTimeQueue(); queue != nil {
|
||||
queue.Cancel(timerName)
|
||||
}
|
||||
s.logGameEvent(session, "game-daily-fatigue-reset-timer-stopped",
|
||||
"char_id", characterID,
|
||||
"timer_name", timerName,
|
||||
"source", source)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user