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,194 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// currentDailyQuestResetHour/currentDailyQuestResetMinute are the 06:00
|
||||
// Beijing boundary premium.ServiceDayID already rolls fatigue, daily-dungeon
|
||||
// and daily-quest state on. nextDaily resolves the moment in the queue's own
|
||||
// location, which defaults to a fixed UTC+8, so the timer and the service day
|
||||
// cannot drift apart.
|
||||
currentDailyQuestResetHour = 6
|
||||
currentDailyQuestResetMinute = 0
|
||||
|
||||
currentDailyQuestResetTimerName = "dnf-daily-quest-reset"
|
||||
currentDailyQuestResetTimerKind = "daily_quest_reset"
|
||||
)
|
||||
|
||||
// registerDailyQuestResetTimer arms the once-a-service-day notification that
|
||||
// tells online clients a daily boundary passed.
|
||||
//
|
||||
// It deliberately owns no state. The reset itself stays authoritative in the
|
||||
// read path - ReconcileDailyReset runs from the selected-character read and
|
||||
// again per character in the callback below - because this deployment is started
|
||||
// and stopped by hand, so a timer that only fires while the process is alive
|
||||
// silently skips every boundary the server slept through. The timer exists for
|
||||
// the one thing a read cannot do: an idle client keeps the list it was last
|
||||
// sent, so a player online across 06:00 would not see yesterday's dailies return
|
||||
// until they relogged.
|
||||
func (s *Service) registerDailyQuestResetTimer() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
queue := s.ensureGameplayTimeQueue()
|
||||
if queue == nil {
|
||||
return
|
||||
}
|
||||
if err := queue.RegisterDaily(
|
||||
currentDailyQuestResetTimerName,
|
||||
currentDailyQuestResetHour,
|
||||
currentDailyQuestResetMinute,
|
||||
func(time.Time) { s.refreshOnlineQuestListsAtDailyReset() },
|
||||
); err != nil {
|
||||
s.logWarn("dnfbridge daily quest reset timer registration failed",
|
||||
"timer_name", currentDailyQuestResetTimerName,
|
||||
"hour", currentDailyQuestResetHour,
|
||||
"minute", currentDailyQuestResetMinute,
|
||||
"error", err)
|
||||
return
|
||||
}
|
||||
s.logPacketEvent("dnf-daily-quest-reset-timer-registered",
|
||||
"timer_name", currentDailyQuestResetTimerName,
|
||||
"hour", currentDailyQuestResetHour,
|
||||
"minute", currentDailyQuestResetMinute,
|
||||
"reason", "online clients keep the pre-reset task list until it is republished")
|
||||
}
|
||||
|
||||
// refreshOnlineQuestListsAtDailyReset republishes every online character's task
|
||||
// projection once the 06:00 boundary has passed.
|
||||
func (s *Service) refreshOnlineQuestListsAtDailyReset() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
sessions := s.onlineQuestRefreshSessions()
|
||||
if len(sessions) == 0 {
|
||||
return
|
||||
}
|
||||
s.logPacketEvent("dnf-daily-quest-reset-refresh-started",
|
||||
"sessions", len(sessions),
|
||||
"timer_name", currentDailyQuestResetTimerName)
|
||||
for _, session := range sessions {
|
||||
s.refreshSessionQuestListAtDailyReset(session)
|
||||
}
|
||||
}
|
||||
|
||||
// onlineQuestRefreshSessions snapshots the sessions that currently have a
|
||||
// selected character. The snapshot is taken under s.mu and every packet is sent
|
||||
// after it is released, so a slow or stalled client can never hold the service
|
||||
// lock.
|
||||
func (s *Service) onlineQuestRefreshSessions() []*gameSession {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
s.mu.Lock()
|
||||
sessions := make([]*gameSession, 0, len(s.gameSessions))
|
||||
for _, session := range s.gameSessions {
|
||||
if session == nil {
|
||||
continue
|
||||
}
|
||||
if session.selectedCharacterID == 0 {
|
||||
continue
|
||||
}
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return sessions
|
||||
}
|
||||
|
||||
// refreshSessionQuestListAtDailyReset submits one session's refresh to its own
|
||||
// event loop. The character identity is captured first and re-checked by the
|
||||
// loop, so a relog or character switch between the boundary and the submission
|
||||
// turns the refresh into a no-op instead of pushing yesterday's list onto the
|
||||
// wrong character.
|
||||
func (s *Service) refreshSessionQuestListAtDailyReset(session *gameSession) {
|
||||
if s == nil || session == nil {
|
||||
return
|
||||
}
|
||||
session.townMu.Lock()
|
||||
characterID := session.selectedCharacterID
|
||||
generation := session.characterGeneration
|
||||
session.townMu.Unlock()
|
||||
if characterID == 0 {
|
||||
return
|
||||
}
|
||||
err := s.postGameSessionCharacterEvent(
|
||||
session,
|
||||
"daily_quest_reset_refresh",
|
||||
characterID,
|
||||
generation,
|
||||
func() error {
|
||||
return s.sendDailyResetQuestRefresh(session, characterID)
|
||||
},
|
||||
)
|
||||
if err != nil && !isClosedGameSessionEventError(err) {
|
||||
s.logGameEvent(session, "game-daily-quest-reset-refresh-failed",
|
||||
"char_id", characterID,
|
||||
"character_generation", generation,
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// sendDailyResetQuestRefresh settles the new service day for one online
|
||||
// character and then republishes the authoritative task trio the accept path
|
||||
// publishes (op574 active -> op356 completed -> op21 acceptable), which is the
|
||||
// sequence the current client actually reacts to.
|
||||
//
|
||||
// The settlement must run before the projections are built: op356 is generated
|
||||
// from the stored row, so reading it first would re-publish yesterday's
|
||||
// completed daily as still completed.
|
||||
func (s *Service) sendDailyResetQuestRefresh(session *gameSession, characterID uint16) error {
|
||||
if s == nil || session == nil || characterID == 0 {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Authoritative, and idempotent: it clears the daily rows that belong to the
|
||||
// service day that just ended, and the per-character/per-day marker on the
|
||||
// session makes a repeat call a no-op.
|
||||
s.reconcileCurrentDailyQuestSelectionIfNeeded(ctx, session, characterID)
|
||||
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.Quest == nil {
|
||||
return nil
|
||||
}
|
||||
source := "daily_quest_reset"
|
||||
if err := s.sendCurrentActiveQuestSnapshotForSession(session, source+"_active_op574"); err != nil {
|
||||
return err
|
||||
}
|
||||
// op356 goes out even when the table becomes empty. The client still holds
|
||||
// the pre-reset row for the daily that was just cleared and the table is a
|
||||
// full replacement, so an all-zero table is exactly what removes it - which is
|
||||
// why this path sends the committed body directly instead of
|
||||
// sendCurrentPersistedClearQuestListIfCompleted, whose "no completed quests,
|
||||
// nothing to restore" shortcut is right for a town transition and wrong here.
|
||||
characterKey := strconv.Itoa(int(characterID))
|
||||
record, found, err := repositories.Quest.Load(ctx, characterKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if found {
|
||||
if err := s.sendCurrentClearQuestListFromCommittedQuest(session, record, source+"_completed_op356"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
s.logGameEvent(session, "game-daily-quest-reset-refresh-op356-skipped",
|
||||
"char_id", characterID,
|
||||
"source", source,
|
||||
"reason", "character_has_no_quest_record")
|
||||
}
|
||||
if err := s.sendCurrentAcceptableQuestListOnlyForSession(session, source+"_acceptable_op21"); err != nil {
|
||||
return err
|
||||
}
|
||||
s.logGameEvent(session, "game-daily-quest-reset-refresh-sent",
|
||||
"char_id", characterID,
|
||||
"timer_kind", currentDailyQuestResetTimerKind,
|
||||
"quest_record_found", found,
|
||||
"sequence", "active_op574_then_completed_op356_then_acceptable_op21",
|
||||
"reason", "daily_boundary_passed_while_the_client_was_online")
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user