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,253 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||||
)
|
||||
|
||||
// The current EXE registers opcode 0x025b dynamically for Forgotten Land.
|
||||
// sub_1D39CE0 consumes exactly two u32 values and stores them in the runtime
|
||||
// fields read by sub_17EBEB0 before its countdown UI is initialized. The same
|
||||
// numeric opcode has a different meaning in other client modules, so keep this
|
||||
// use local to the Lost Land entry path.
|
||||
const currentForgottenLandTimeInfoMsgID uint16 = 0x025b
|
||||
|
||||
func isCurrentForgottenLandDungeon(runtime *runtimeDungeonState) bool {
|
||||
return runtime != nil && strings.EqualFold(strings.TrimSpace(runtime.Dungeon.Metadata.DungeonType), "lost land")
|
||||
}
|
||||
|
||||
func currentForgottenLandTimeLimits(runtime *runtimeDungeonState) (uint32, uint32, time.Duration, error) {
|
||||
if !isCurrentForgottenLandDungeon(runtime) {
|
||||
return 0, 0, 0, nil
|
||||
}
|
||||
limits := runtime.Dungeon.Metadata.LimitTime
|
||||
const maxUint32 = int64(^uint32(0))
|
||||
if !limits.Set || limits.PrimarySeconds <= 0 || limits.SecondarySeconds <= 0 ||
|
||||
limits.PrimarySeconds > maxUint32 || limits.SecondarySeconds > maxUint32 {
|
||||
return 0, 0, 0, fmt.Errorf(
|
||||
"forgotten land PVF limit time is missing or invalid: set=%t primary=%d secondary=%d",
|
||||
limits.Set,
|
||||
limits.PrimarySeconds,
|
||||
limits.SecondarySeconds,
|
||||
)
|
||||
}
|
||||
primary := uint32(limits.PrimarySeconds)
|
||||
secondary := uint32(limits.SecondarySeconds)
|
||||
return primary, secondary, time.Duration(primary) * time.Second, nil
|
||||
}
|
||||
|
||||
func buildCurrentForgottenLandTimeInfoBody(primarySeconds, secondarySeconds uint32) []byte {
|
||||
body := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint32(body[0:4], primarySeconds)
|
||||
binary.LittleEndian.PutUint32(body[4:8], secondarySeconds)
|
||||
return body
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentForgottenLandTimeProjection(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
) error {
|
||||
primary, secondary, _, err := currentForgottenLandTimeLimits(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isCurrentForgottenLandDungeon(runtime) {
|
||||
return nil
|
||||
}
|
||||
body := buildCurrentForgottenLandTimeInfoBody(primary, secondary)
|
||||
s.logGameEvent(session, "game-forgotten-land-time-op603-send",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"msg_id", currentForgottenLandTimeInfoMsgID,
|
||||
"classification", 0,
|
||||
"primary_seconds", primary,
|
||||
"secondary_seconds", secondary,
|
||||
"body_len", len(body),
|
||||
"body_source", "current_exe_sub_1D39CE0_and_pvf_limit_time")
|
||||
return s.sendGameUpperRawClass(session, currentForgottenLandTimeInfoMsgID, body, 0)
|
||||
}
|
||||
|
||||
// armCurrentForgottenLandTimer freezes the town return before entering the
|
||||
// queue. The deadline belongs to the whole dungeon runtime and is intentionally
|
||||
// idempotent, so room/warp transitions can never reset it.
|
||||
func (s *Service) armCurrentForgottenLandTimer(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
) error {
|
||||
_, _, delay, err := currentForgottenLandTimeLimits(runtime)
|
||||
if err != nil || !isCurrentForgottenLandDungeon(runtime) {
|
||||
return err
|
||||
}
|
||||
if s == nil || session == nil || runtime == nil {
|
||||
return fmt.Errorf("forgotten land timer runtime owner is unavailable")
|
||||
}
|
||||
queue := s.ensureGameplayTimeQueue()
|
||||
if queue == nil {
|
||||
return errGameplayTimeQueueUnavailable
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
transition, err := s.prepareCurrentDungeonTownTransitionForSession(ctx, session, session.selectedCharacterID)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare forgotten land timeout return: %w", err)
|
||||
}
|
||||
|
||||
session.dungeon.mu.Lock()
|
||||
defer session.dungeon.mu.Unlock()
|
||||
return s.armCurrentForgottenLandTimerLocked(session, runtime, transition, delay, queue)
|
||||
}
|
||||
|
||||
func (s *Service) armCurrentForgottenLandTimerLocked(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
transition currentDungeonTownTransition,
|
||||
delay time.Duration,
|
||||
queue gameplayTimeQueue,
|
||||
) error {
|
||||
if session.dungeon.runtime != runtime || runtime.lifecycleToken == 0 || queue == nil {
|
||||
return errGameplayTimeQueueUnavailable
|
||||
}
|
||||
if runtime.forgottenLandTimerActive {
|
||||
return nil
|
||||
}
|
||||
runtime.forgottenLandTimerGeneration = nextCurrentForgottenLandTimerGeneration(runtime.forgottenLandTimerGeneration)
|
||||
generation := runtime.forgottenLandTimerGeneration
|
||||
timerName := fmt.Sprintf("dnf-forgotten-land:%s:run:%d", session.connID, runtime.lifecycleToken)
|
||||
runtime.forgottenLandTimerActive = true
|
||||
runtime.forgottenLandTimerName = timerName
|
||||
runtime.forgottenLandDeadline = queue.Now().Add(delay)
|
||||
runtime.forgottenLandTownTransition = cloneCurrentDungeonTownTransition(transition)
|
||||
characterID, characterGeneration, err := gameSessionCharacterEventIdentity(session, session.selectedCharacterID)
|
||||
if err != nil {
|
||||
s.cancelCurrentForgottenLandTimerLocked(session, runtime, "timer_character_identity_changed")
|
||||
return err
|
||||
}
|
||||
dungeonID := runtime.Dungeon.ID
|
||||
runToken := runtime.lifecycleToken
|
||||
if err := queue.ScheduleAfter(timerName, delay, func(due time.Time) {
|
||||
postErr := s.postGameSessionCharacterEvent(
|
||||
session,
|
||||
"forgotten-land-timequeue",
|
||||
characterID,
|
||||
characterGeneration,
|
||||
func() error {
|
||||
s.fireCurrentForgottenLandTimer(session, runtime, generation, due)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if postErr != nil && !isClosedGameSessionEventError(postErr) {
|
||||
s.logPacketEvent("game-session-event-submit-failed",
|
||||
"conn_id", session.connID,
|
||||
"source", "forgotten-land-timequeue",
|
||||
"char_id", characterID,
|
||||
"character_generation", characterGeneration,
|
||||
"dungeon_id", dungeonID,
|
||||
"run_token", runToken,
|
||||
"error", postErr)
|
||||
}
|
||||
}); err != nil {
|
||||
s.cancelCurrentForgottenLandTimerLocked(session, runtime, "timer_schedule_failed")
|
||||
return err
|
||||
}
|
||||
s.logGameEvent(session, "game-forgotten-land-timer-scheduled",
|
||||
"char_id", characterID,
|
||||
"dungeon_id", dungeonID,
|
||||
"run_token", runToken,
|
||||
"timer_generation", generation,
|
||||
"timer_name", timerName,
|
||||
"due_at_utc", runtime.forgottenLandDeadline.UTC().Format(time.RFC3339Nano),
|
||||
"delay_seconds", int64(delay/time.Second),
|
||||
"source", "gameplay_time_queue_one_shot")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) fireCurrentForgottenLandTimer(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
generation uint64,
|
||||
due time.Time,
|
||||
) {
|
||||
if s == nil || session == nil || runtime == nil {
|
||||
return
|
||||
}
|
||||
session.dungeon.mu.Lock()
|
||||
if session.dungeon.runtime != runtime || !runtime.forgottenLandTimerActive ||
|
||||
runtime.forgottenLandTimerGeneration != generation || runtime.lifecycleToken == 0 {
|
||||
session.dungeon.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if runtime.Session == nil || runtime.Session.Snapshot().Run.Status != worldmap.DungeonRunActive {
|
||||
s.cancelCurrentForgottenLandTimerLocked(session, runtime, "timer_fired_after_runtime_state_changed")
|
||||
session.dungeon.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.logGameEvent(session, "game-forgotten-land-timer-fired",
|
||||
"char_id", runtime.Character.CharacterID,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"run_token", runtime.lifecycleToken,
|
||||
"timer_generation", generation,
|
||||
"scheduled_due_utc", due.UTC().Format(time.RFC3339Nano))
|
||||
transition := cloneCurrentDungeonTownTransition(runtime.forgottenLandTownTransition)
|
||||
// This one-shot has now fired. Consume its queue ownership before asking
|
||||
// the current run to transition; an unconfirmed user-initiated return keeps
|
||||
// a not-yet-fired timer, while an expired timer must not remain active.
|
||||
s.cancelCurrentForgottenLandTimerLocked(session, runtime, "timer_fired")
|
||||
session.dungeon.mu.Unlock()
|
||||
if err := s.sendCurrentActiveDungeonReturnToTown(
|
||||
session,
|
||||
runtime,
|
||||
transition,
|
||||
currentForgottenLandTimeInfoMsgID,
|
||||
"current_exe_forgotten_land_pvf_limit_time_expired",
|
||||
); err != nil {
|
||||
s.logGameEvent(session, "game-forgotten-land-timeout-return-failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// cancelCurrentForgottenLandTimerLocked invalidates an already-dequeued
|
||||
// callback before removing the named queue item. The caller holds dungeon.mu.
|
||||
func (s *Service) cancelCurrentForgottenLandTimerLocked(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
source string,
|
||||
) {
|
||||
if runtime == nil {
|
||||
return
|
||||
}
|
||||
wasActive := runtime.forgottenLandTimerActive
|
||||
timerName := runtime.forgottenLandTimerName
|
||||
previousGeneration := runtime.forgottenLandTimerGeneration
|
||||
runtime.forgottenLandTimerGeneration = nextCurrentForgottenLandTimerGeneration(previousGeneration)
|
||||
runtime.forgottenLandTimerActive = false
|
||||
runtime.forgottenLandTimerName = ""
|
||||
runtime.forgottenLandDeadline = time.Time{}
|
||||
runtime.forgottenLandTownTransition = currentDungeonTownTransition{}
|
||||
cancelled := false
|
||||
if timerName != "" && s != nil && s.gameplayTimers != nil {
|
||||
cancelled = s.gameplayTimers.Cancel(timerName)
|
||||
}
|
||||
if wasActive || timerName != "" {
|
||||
s.logGameEvent(session, "game-forgotten-land-timer-cancelled",
|
||||
"char_id", runtime.Character.CharacterID,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"run_token", runtime.lifecycleToken,
|
||||
"previous_generation", previousGeneration,
|
||||
"current_generation", runtime.forgottenLandTimerGeneration,
|
||||
"timer_name", timerName,
|
||||
"queue_item_cancelled", cancelled,
|
||||
"source", source)
|
||||
}
|
||||
}
|
||||
|
||||
func nextCurrentForgottenLandTimerGeneration(current uint64) uint64 {
|
||||
next := current + 1
|
||||
if next == 0 {
|
||||
next++
|
||||
}
|
||||
return next
|
||||
}
|
||||
Reference in new issue
Block a user