Files
90CNServer/server/internal/services/dnfbridge/death_tower.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

420 lines
13 KiB
Go

package dnfbridge
import (
"context"
"errors"
"fmt"
"math/rand"
"strconv"
"time"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfquest "longheng.io/server/internal/modules/dnf/quest"
)
var (
errDeathTowerUnavailable = errors.New("dnf death tower is unavailable")
errDeathTowerNotInTower = errors.New("dnf death tower session is not active")
errDeathTowerAdvance = errors.New("dnf death tower stage advance rejected")
)
const (
currentDeathTowerDungeonID = 11000
currentDeathTowerTotalStages = 45
currentDeathTowerFirstMapID = 30001
currentDeathTowerBasisLevel = 60
currentDeathTowerInfoMsgID = 0x008E
currentDeathTowerStageMapMsgID = 0x008F
currentDeathTowerRankingMsgID = 0x0090
currentDeathTowerRewardMsgID = 0x0091
currentDeathTowerEplpMsgID = 0x0092
currentDeathTowerNormalMode = 0
currentDeathTowerBuffType = 11
)
// deathTowerRuntime is the per-session tower state.
type deathTowerRuntime struct {
DungeonID int
CurrentStage int // 0-based
TotalStages int
State int // 0=init, 1=fighting, 2=cleared
MonsterSeq uint16
StageMapIDs []int
BasisLevel int
}
func newDeathTowerRuntime(stageMapIDs []int, basisLevel int) *deathTowerRuntime {
return &deathTowerRuntime{
DungeonID: currentDeathTowerDungeonID,
CurrentStage: 0,
TotalStages: len(stageMapIDs),
State: 0,
MonsterSeq: 1,
StageMapIDs: stageMapIDs,
BasisLevel: basisLevel,
}
}
func (rt *deathTowerRuntime) currentMapID() int {
if rt == nil || rt.CurrentStage < 0 || rt.CurrentStage >= len(rt.StageMapIDs) {
return 0
}
return rt.StageMapIDs[rt.CurrentStage]
}
func (rt *deathTowerRuntime) isLastStage() bool {
return rt != nil && rt.CurrentStage >= rt.TotalStages-1
}
func (rt *deathTowerRuntime) tryAdvance() bool {
if rt == nil || rt.State < 1 || rt.CurrentStage >= rt.TotalStages-1 {
return false
}
rt.CurrentStage++
rt.State = 0
return true
}
// --- Entry ---
func (s *Service) handleCurrentDeathTowerEntry(session *gameSession, difficulty byte) error {
if session == nil || session.selectedCharacterID == 0 {
return errDeathTowerUnavailable
}
stageMapIDs, basisLevel, err := s.currentDeathTowerPVFConfig()
if err != nil {
s.logGameEvent(session, "game-death-tower-entry-rejected", "reason", err)
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketSelectDungeon), 22)
}
rt := newDeathTowerRuntime(stageMapIDs, basisLevel)
session.dungeon.deathTower = rt
s.logGameEvent(session, "game-death-tower-entry",
"dungeon_id", rt.DungeonID,
"total_stages", rt.TotalStages,
"basis_level", rt.BasisLevel,
"first_map", rt.currentMapID())
// 0x008E DEATH_TOWER_INFO (8B)
infoBody := buildCurrentDeathTowerInfo(rt.DungeonID, uint16(rt.TotalStages))
if err := s.sendGameUpperRawClass(session, currentDeathTowerInfoMsgID, infoBody, dnfproto.DefaultChannelClassification); err != nil {
return err
}
// 0x008F first stage map
if err := s.sendCurrentDeathTowerStageMap(session, rt); err != nil {
return err
}
// 0x001E FINISH_LOADING (empty)
return s.sendGameUpperRawClass(session, csharpNotiFinishLoadingMsgID, nil, dnfproto.DefaultChannelClassification)
}
// --- Stage Map ---
func (s *Service) sendCurrentDeathTowerStageMap(session *gameSession, rt *deathTowerRuntime) error {
monsters, err := s.currentDeathTowerStageMonsters(rt)
if err != nil {
s.logGameEvent(session, "game-death-tower-stage-map-failed",
"stage", rt.CurrentStage, "map_id", rt.currentMapID(), "reason", err)
monsters = nil
}
body := buildCurrentDeathTowerStageMap(rt, monsters)
return s.sendGameUpperRawClass(session, currentDeathTowerStageMapMsgID, body, dnfproto.DefaultChannelClassification)
}
// --- Stage Command (op159 / CmdPacketDeathTowerStageCmd) ---
func (s *Service) handleCurrentDeathTowerStageCmd(session *gameSession, body []byte) error {
rt := session.dungeon.deathTower
if rt == nil {
return nil
}
if len(body) < 1 {
return nil
}
switch body[0] {
case 1:
rt.State = 1
s.logGameEvent(session, "game-death-tower-fight-start", "stage", rt.CurrentStage)
case 2:
rt.State = 2
s.logGameEvent(session, "game-death-tower-stage-clear", "stage", rt.CurrentStage, "map_id", rt.currentMapID(), "is_last", rt.isLastStage())
s.syncCurrentDeathTowerClearMap(session, rt, "tower_stage_cmd")
if rt.isLastStage() {
return s.sendCurrentDeathTowerSettlement(session, rt)
}
}
return nil
}
// --- MOVE_MAP advance ---
func (s *Service) handleCurrentDeathTowerMoveMap(session *gameSession) (bool, error) {
rt := session.dungeon.deathTower
if rt == nil {
return false, nil
}
if rt.State >= 1 {
s.syncCurrentDeathTowerClearMap(session, rt, "tower_move_map")
}
if !rt.tryAdvance() {
s.logGameEvent(session, "game-death-tower-advance-rejected",
"stage", rt.CurrentStage, "state", rt.State, "is_last", rt.isLastStage())
return true, nil
}
s.logGameEvent(session, "game-death-tower-advance",
"stage", rt.CurrentStage, "map_id", rt.currentMapID())
if err := s.sendCurrentDeathTowerStageMap(session, rt); err != nil {
return true, err
}
return true, s.sendGameUpperRawClass(session, csharpNotiFinishLoadingMsgID, nil, dnfproto.DefaultChannelClassification)
}
// --- Settlement ---
func (s *Service) sendCurrentDeathTowerSettlement(session *gameSession, rt *deathTowerRuntime) error {
s.logGameEvent(session, "game-death-tower-settlement", "dungeon_id", rt.DungeonID, "total_stages", rt.TotalStages)
// 0x0090 ranking (empty)
if err := s.sendGameUpperRawClass(session, currentDeathTowerRankingMsgID, buildCurrentDeathTowerEmptyRanking(rt.DungeonID), dnfproto.DefaultChannelClassification); err != nil {
return err
}
// 0x0091 reward (empty)
if err := s.sendGameUpperRawClass(session, currentDeathTowerRewardMsgID, buildCurrentDeathTowerEmptyReward(), dnfproto.DefaultChannelClassification); err != nil {
return err
}
// 0x0092 eplp
if err := s.sendGameUpperRawClass(session, currentDeathTowerEplpMsgID, []byte{1}, dnfproto.DefaultChannelClassification); err != nil {
return err
}
session.dungeon.deathTower = nil
return nil
}
// --- Quest sync ---
func (s *Service) syncCurrentDeathTowerClearMap(session *gameSession, rt *deathTowerRuntime, source string) {
mapID := rt.currentMapID()
if mapID <= 0 {
return
}
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
defer cancel()
characterID := strconv.FormatUint(uint64(session.selectedCharacterID), 10)
repos, ok := s.repositoryGroup()
if !ok || repos.Quest == nil || repos.Character == nil {
return
}
if _, found, err := repos.Quest.Load(ctx, characterID); err != nil || !found {
return
}
catalog, err := s.loadQuestCatalog(ctx)
if err != nil {
return
}
owner, err := dnfquest.NewOwner(repos)
if err != nil {
return
}
completionKey := fmt.Sprintf("death-tower/%s/%d/%d", characterID, rt.CurrentStage, mapID)
result, err := owner.ApplyClearMapCompletion(ctx, catalog, characterID, dnfquest.ClearMapCompletionInput{
DungeonID: int64(rt.DungeonID),
MapID: int64(mapID),
CompletionKey: completionKey,
CompletedAt: time.Now().UTC(),
})
if err != nil {
s.logGameEvent(session, "game-death-tower-quest-sync-failed",
"stage", rt.CurrentStage, "map_id", mapID, "source", source, "reason", err)
return
}
if len(result.Completions) > 0 {
s.logGameEvent(session, "game-death-tower-quest-synced",
"stage", rt.CurrentStage, "map_id", mapID, "source", source, "completions", len(result.Completions))
}
}
// --- Packet builders ---
func buildCurrentDeathTowerInfo(dungeonID int, endStage uint16) []byte {
var w packetWriter
w.writeUint32(uint32(dungeonID))
w.writeUint16(endStage)
w.writeByte(currentDeathTowerNormalMode)
w.writeByte(currentDeathTowerBuffType)
return w.bytes()
}
type currentDeathTowerStageMonster struct {
ListIndex uint32
MonsterUnique uint16
MonsterID uint32
Level byte
MonsterType byte // 0=normal, 5=APC
IsBox byte
BoxIndex byte
}
func buildCurrentDeathTowerStageMap(rt *deathTowerRuntime, monsters []currentDeathTowerStageMonster) []byte {
var w packetWriter
w.writeUint16(uint16(rt.CurrentStage + 1)) // 1-based display
w.writeUint32(rand.Uint32())
w.writeUint16(uint16(rt.currentMapID()))
w.writeByte(byte(len(monsters)))
for _, m := range monsters {
w.writeUint32(m.ListIndex)
w.writeUint16(m.MonsterUnique)
w.writeUint32(m.MonsterID)
w.writeByte(m.Level)
w.writeByte(m.MonsterType)
w.writeByte(m.IsBox)
w.writeByte(m.BoxIndex)
}
w.writeByte(0) // itemCount = 0
return w.bytes()
}
func buildCurrentDeathTowerEmptyRanking(dungeonID int) []byte {
var w packetWriter
w.writeByte(0) // flag0
w.writeUint32(0) // clearTime
w.writeUint32(0) // playTime
w.writeByte(0) // flag3
w.writeUint32(uint32(dungeonID))
w.writeByte(0) // hasMyBestRecord = false
for g := 0; g < 5; g++ {
for r := 0; r < 8; r++ {
w.writeUint32(0) // empty dstr
w.writeByte(0)
w.writeByte(0)
}
w.writeUint16(0)
w.writeUint32(0)
w.writeUint32(0)
}
return w.bytes()
}
func buildCurrentDeathTowerEmptyReward() []byte {
var w packetWriter
w.writeUint32(0) // summaryValue
w.writeByte(0) // group0 count
w.writeByte(0) // group1 count
w.writeByte(0) // group2 count
w.writeByte(0) // group3 count
return w.bytes()
}
// --- PVF config ---
func (s *Service) currentDeathTowerPVFConfig() ([]int, int, error) {
s.deathTowerMu.Lock()
defer s.deathTowerMu.Unlock()
if s.deathTowerStageMapIDs != nil {
return s.deathTowerStageMapIDs, s.deathTowerBasisLevel, nil
}
s.initialEquipmentMu.Lock()
archive, err := s.initialEquipmentArchiveLocked()
s.initialEquipmentMu.Unlock()
if err != nil {
return nil, 0, fmt.Errorf("%w: %v", errDeathTowerUnavailable, err)
}
text, err := archive.ReadText("dungeon/Towers/TowerOfDeath.dgn")
if err != nil {
return nil, 0, fmt.Errorf("%w: read dgn: %v", errDeathTowerUnavailable, err)
}
doc, err := dnfpvf.Parse("dungeon/Towers/TowerOfDeath.dgn", text)
if err != nil {
return nil, 0, fmt.Errorf("%w: parse dgn: %v", errDeathTowerUnavailable, err)
}
mapIDs, err := parseDeathTowerMapIndexes(doc)
if err != nil {
return nil, 0, err
}
basisLevel := 1
if lv, found := doc.Int("basis level"); found && lv > 0 {
basisLevel = int(lv)
}
s.deathTowerStageMapIDs = mapIDs
s.deathTowerBasisLevel = basisLevel
s.logPacketEvent("dnf-death-tower-pvf-loaded", "stages", len(mapIDs), "basis_level", basisLevel, "first_map", mapIDs[0], "last_map", mapIDs[len(mapIDs)-1])
return mapIDs, basisLevel, nil
}
func parseDeathTowerMapIndexes(doc *dnfpvf.Document) ([]int, error) {
values := doc.Ints("death tower map indexes")
if len(values) < 3 {
return nil, fmt.Errorf("%w: death tower map indexes missing or too short (%d values)", errDeathTowerUnavailable, len(values))
}
totalStages := int(values[0])
if totalStages <= 0 || len(values) < 1+totalStages*2 {
return nil, fmt.Errorf("%w: death tower map indexes malformed: total=%d values=%d", errDeathTowerUnavailable, totalStages, len(values))
}
mapIDs := make([]int, totalStages)
for i := 0; i < totalStages; i++ {
mapIDs[i] = int(values[1+i*2+1])
}
return mapIDs, nil
}
// --- Stage monster loading ---
func (s *Service) currentDeathTowerStageMonsters(rt *deathTowerRuntime) ([]currentDeathTowerStageMonster, error) {
mapID := rt.currentMapID()
if mapID <= 0 {
return nil, errDeathTowerNotInTower
}
s.initialEquipmentMu.Lock()
archive, err := s.initialEquipmentArchiveLocked()
s.initialEquipmentMu.Unlock()
if err != nil {
return nil, err
}
mapPath := fmt.Sprintf("map/DeadTower/%03dF.map", rt.CurrentStage+1)
text, err := archive.ReadText(mapPath)
if err != nil {
return nil, fmt.Errorf("read %s: %w", mapPath, err)
}
doc, err := dnfpvf.Parse(mapPath, text)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", mapPath, err)
}
return parseDeathTowerMapMonsters(doc, rt)
}
// parseDeathTowerMapMonsters reads the [monster] section from a tower map file.
// Format per monster: monsterId groupIndex level x y direction countPerAI unk aiType rank
// (9 tokens per entry based on PVF observation).
func parseDeathTowerMapMonsters(doc *dnfpvf.Document, rt *deathTowerRuntime) ([]currentDeathTowerStageMonster, error) {
values := doc.Ints("monster")
if len(values) == 0 {
return nil, nil
}
const stride = 9
var monsters []currentDeathTowerStageMonster
for offset := 0; offset+stride <= len(values); offset += stride {
monsterID := values[offset]
level := values[offset+2]
if monsterID <= 0 {
continue
}
if level <= 0 {
level = int64(rt.BasisLevel)
}
if level > 255 {
level = 255
}
monsters = append(monsters, currentDeathTowerStageMonster{
ListIndex: uint32(len(monsters)),
MonsterUnique: rt.MonsterSeq,
MonsterID: uint32(monsterID),
Level: byte(level),
MonsterType: 0,
IsBox: 0,
BoxIndex: 0,
})
rt.MonsterSeq++
}
return monsters, nil
}