DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,331 @@
package dnfbridge
import (
"errors"
"fmt"
"strings"
"longheng.io/server/internal/modules/dnf/worldmap"
)
var (
errDungeonExtendedActorCodeRange = errors.New("dnf dungeon extended actor code exceeds current EXE range")
errDungeonExtendedActorIndexRange = errors.New("dnf dungeon extended actor index exceeds current EXE range")
errDungeonExtendedActorObjectKeyRange = errors.New("dnf dungeon extended actor object key exceeds current EXE range")
errDungeonSpecialSpawnKind = errors.New("dnf dungeon special object spawn kind is unsupported")
errDungeonSpecialSpawnLevel = errors.New("dnf dungeon special object monster level is invalid")
errDungeonAICharacterDefinitionMiss = errors.New("dnf dungeon AI character definition is missing")
errDungeonAICharacterType = errors.New("dnf dungeon AI character type is unsupported")
errDungeonAICharacterFaction = errors.New("dnf dungeon AI character faction is unsupported")
)
type runtimeDungeonExtendedActorKind string
const (
runtimeDungeonActorSpecialObject runtimeDungeonExtendedActorKind = "special_object"
runtimeDungeonActorSpecialMonster runtimeDungeonExtendedActorKind = "special_monster"
runtimeDungeonActorAICharacter runtimeDungeonExtendedActorKind = "ai_character"
runtimeDungeonActorHellParty runtimeDungeonExtendedActorKind = "hell_party"
runtimeDungeonActorTournament runtimeDungeonExtendedActorKind = "tournament"
)
type runtimeDungeonRetainedSpecialSpawn struct {
ObjectIndex int
SpawnIndex int
Spawn worldmap.SpecialObjectSpawn
}
type runtimeDungeonExtendedActor struct {
Kind runtimeDungeonExtendedActorKind
ObjectKey uint32
Packet currentDungeonStartMapActor
HostileReference *worldmap.HostileReference
AICharacter *worldmap.AICharacter
AICharacterMetadata *pvfDungeonAICharacterDefinition
SpecialObjectIndex int
SpecialSpawnIndex int
State runtimeDungeonMonsterState
}
type runtimeDungeonExtendedActorPlan struct {
Actors []runtimeDungeonExtendedActor
RetainedSpecialSpawns []runtimeDungeonRetainedSpecialSpawn
Diagnostics []string
NextObjectKey uint32
SpecialObjectCount int
AICharacterCount int
}
func planRuntimeDungeonExtendedActors(
scene worldmap.DungeonRoomScene,
monsterCatalog *pvfDungeonMonsterCatalog,
aiCharacterCatalog *pvfDungeonAICharacterCatalog,
basis worldmap.OptionalInt,
firstObjectKey uint32,
companionEnabled ...bool,
) (runtimeDungeonExtendedActorPlan, error) {
plan := runtimeDungeonExtendedActorPlan{
NextObjectKey: firstObjectKey,
SpecialObjectCount: len(scene.SpecialPassiveObjects),
AICharacterCount: len(scene.AICharacters),
}
for objectIndex, object := range scene.SpecialPassiveObjects {
// Inline [hellparty] metadata belongs to the dedicated Hell runtime.
// Keep the pillar in the room's passive-object metadata, but do not
// project a duplicate ordinary type-9 actor for the same object.
if object.ObjectID <= 0 {
plan.Diagnostics = append(plan.Diagnostics, fmt.Sprintf("special object index %d has non-positive object id %d", objectIndex, object.ObjectID))
} else if len(object.HellParty) == 0 {
compatible, listedPath, err := monsterCatalog.specialObjectStartActorCompatibility(object.ObjectID)
if err != nil {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("resolve special object index %d id %d: %w", objectIndex, object.ObjectID, err)
}
if !compatible {
plan.Diagnostics = append(plan.Diagnostics, fmt.Sprintf(
"retain map-owned special object index %d id %d path %q outside op29 actor list",
objectIndex,
object.ObjectID,
listedPath,
))
} else {
actor, err := newRuntimeDungeonExtendedActor(
runtimeDungeonActorSpecialObject,
plan.NextObjectKey,
object.ObjectID,
currentDungeonStartMapActor{PacketIndex: uint32(objectIndex), Level: 0, Type: 9},
)
if err != nil {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("special object index %d: %w", objectIndex, err)
}
actor.SpecialObjectIndex = objectIndex
plan.Actors = append(plan.Actors, actor)
plan.NextObjectKey++
}
}
for spawnIndex, spawn := range object.Spawns {
kind := normalizeDungeonPVFSymbol(spawn.Kind)
if kind != "monster" {
plan.RetainedSpecialSpawns = append(plan.RetainedSpecialSpawns, runtimeDungeonRetainedSpecialSpawn{
ObjectIndex: objectIndex,
SpawnIndex: spawnIndex,
Spawn: spawn,
})
switch kind {
case "item", "trap", "hellparty", "quest":
continue
default:
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf(
"%w: object_index=%d spawn_index=%d kind=%q",
errDungeonSpecialSpawnKind,
objectIndex,
spawnIndex,
spawn.Kind,
)
}
}
if monsterCatalog == nil {
return runtimeDungeonExtendedActorPlan{}, errDungeonMonsterCatalogUnavailable
}
if objectIndex > int(^uint16(0)) || objectIndex > int(^uint8(0)) {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: special_object_index=%d", errDungeonExtendedActorIndexRange, objectIndex)
}
if spawnIndex > int(^uint32(0)) {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: special_spawn_index=%d", errDungeonExtendedActorIndexRange, spawnIndex)
}
if spawn.Code <= 0 || spawn.Code > int64(^uint32(0)) {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: special_object_index=%d spawn_index=%d code=%d", errDungeonExtendedActorCodeRange, objectIndex, spawnIndex, spawn.Code)
}
if _, found, err := monsterCatalog.Find(spawn.Code); err != nil {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("resolve special monster code=%d: %w", spawn.Code, err)
} else if !found {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: special monster code=%d", errDungeonMonsterDefinitionMiss, spawn.Code)
}
level, err := currentDungeonSpecialMonsterLevel(spawn, basis)
if err != nil {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("special object index %d spawn index %d: %w", objectIndex, spawnIndex, err)
}
actor, err := newRuntimeDungeonExtendedActor(
runtimeDungeonActorSpecialMonster,
plan.NextObjectKey,
spawn.Code,
currentDungeonStartMapActor{
TemplateOrder: uint16(objectIndex),
PacketIndex: uint32(spawnIndex),
Level: level,
Type: 0,
Flag0: 1,
Flag1: byte(objectIndex),
},
)
if err != nil {
return runtimeDungeonExtendedActorPlan{}, err
}
actor.SpecialObjectIndex = objectIndex
actor.SpecialSpawnIndex = spawnIndex
plan.Actors = append(plan.Actors, actor)
plan.NextObjectKey++
}
}
for actorIndex, actorValue := range scene.AICharacters {
if aiCharacterCatalog == nil {
return runtimeDungeonExtendedActorPlan{}, errDungeonAICharacterUnavailable
}
if actorValue.Code <= 0 || actorValue.Code > int64(^uint32(0)) {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: AI_character_index=%d code=%d", errDungeonExtendedActorCodeRange, actorIndex, actorValue.Code)
}
hostile, err := currentDungeonAICharacterHostile(actorValue.Faction)
if err != nil {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("AI character index %d code %d: %w", actorIndex, actorValue.Code, err)
}
definition, found, err := aiCharacterCatalog.Find(actorValue.Code)
if err != nil {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("resolve AI character index=%d code=%d: %w", actorIndex, actorValue.Code, err)
}
if !found {
if !hostile {
plan.Diagnostics = append(plan.Diagnostics, fmt.Sprintf(
"skip non-hostile AI character index %d code %d because its definition is missing",
actorIndex,
actorValue.Code,
))
continue
}
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: index=%d code=%d", errDungeonAICharacterDefinitionMiss, actorIndex, actorValue.Code)
}
actorType, err := currentDungeonAICharacterType(actorValue.AIType)
if err != nil {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("AI character index %d code %d: %w", actorIndex, actorValue.Code, err)
}
blocking, err := currentDungeonAICharacterBlocking(actorValue.Faction)
if err != nil {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("AI character index %d code %d: %w", actorIndex, actorValue.Code, err)
}
actor, err := newRuntimeDungeonExtendedActor(
runtimeDungeonActorAICharacter,
plan.NextObjectKey,
actorValue.Code,
currentDungeonStartMapActor{
PacketIndex: uint32(actorIndex),
Level: definition.Level,
Type: actorType,
Blocking: blocking,
},
)
if err != nil {
return runtimeDungeonExtendedActorPlan{}, err
}
actor.AICharacter = cloneRuntimeDungeonAICharacter(actorValue)
definitionCopy := cloneDungeonAICharacterDefinition(definition)
actor.AICharacterMetadata = &definitionCopy
if hostile {
reference := worldmap.HostileReference{Kind: worldmap.HostileAICharacter, Index: actorIndex}
actor.HostileReference = &reference
}
plan.Actors = append(plan.Actors, actor)
plan.NextObjectKey++
}
// 支援兵:把官方 APC(code 5405,阵营 "[character]")作为一条 start-map
// 扩展 actor 行注入。它走的是「AI 角色」通道而不是「远端玩家 actor」通道 —
// 后者只接受真实在线玩家,塞离线角色记录会让客户端原生处理器崩。
//
// 变参形式(companionEnabled ...bool)是有意的:现有全部调用点无需改动,
// 行为保持原样;只有副本运行时那条路径会显式传入冻结后的 enabled。
injectCompanion := currentDungeonCompanionP0StartMapEnabled()
if len(companionEnabled) != 0 {
injectCompanion = companionEnabled[0]
}
if err := appendCurrentDungeonCompanionP0StartMapActorForRun(
&plan,
aiCharacterCatalog,
injectCompanion,
); err != nil {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("plan companion P0 START_MAP actor: %w", err)
}
if len(scene.Monsters)+len(plan.Actors) > 0xff {
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: normal=%d extended=%d", errDungeonStartMapActorCount, len(scene.Monsters), len(plan.Actors))
}
return plan, nil
}
func newRuntimeDungeonExtendedActor(
kind runtimeDungeonExtendedActorKind,
objectKey uint32,
code int64,
packet currentDungeonStartMapActor,
) (runtimeDungeonExtendedActor, error) {
if objectKey == 0 || objectKey > uint32(^uint16(0)) {
return runtimeDungeonExtendedActor{}, fmt.Errorf("%w: object_key=%d", errDungeonExtendedActorObjectKeyRange, objectKey)
}
if code <= 0 || code > int64(^uint32(0)) {
return runtimeDungeonExtendedActor{}, fmt.Errorf("%w: code=%d", errDungeonExtendedActorCodeRange, code)
}
packet.ObjectKey = uint16(objectKey)
packet.Code = uint32(code)
return runtimeDungeonExtendedActor{
Kind: kind,
ObjectKey: objectKey,
Packet: packet,
State: runtimeDungeonMonsterPlanned,
}, nil
}
func currentDungeonSpecialMonsterLevel(spawn worldmap.SpecialObjectSpawn, basis worldmap.OptionalInt) (byte, error) {
level := spawn.Level
if level <= 0 {
if !basis.Set {
return 0, fmt.Errorf("%w: code=%d level=%d basis=unset", errDungeonSpecialSpawnLevel, spawn.Code, spawn.Level)
}
level = basis.Value
}
if level <= 0 || level > 0xff {
return 0, fmt.Errorf("%w: code=%d level=%d basis=%d basis_set=%t resolved=%d", errDungeonSpecialSpawnLevel, spawn.Code, spawn.Level, basis.Value, basis.Set, level)
}
return byte(level), nil
}
func currentDungeonAICharacterType(value string) (byte, error) {
switch normalizeDungeonPVFSymbol(value) {
case "normal":
return 5, nil
case "champion":
return 6, nil
case "super champion", "superchampion":
return 7, nil
case "boss":
return 8, nil
default:
return 0, fmt.Errorf("%w: type=%q", errDungeonAICharacterType, value)
}
}
func currentDungeonAICharacterBlocking(value string) (byte, error) {
if _, err := currentDungeonAICharacterHostile(value); err != nil {
return 0, err
}
// Current check_grid_clear and the old C# behavior both exclude every
// AICharacter/APC row (types 5..8) from the room-door blocking set. Faction
// still determines combat ownership, but never this wire flag.
return 0, nil
}
func currentDungeonAICharacterHostile(value string) (bool, error) {
switch normalizeDungeonPVFSymbol(value) {
case "monster":
return true, nil
case "character", "neutral":
return false, nil
default:
return false, fmt.Errorf("%w: faction=%q", errDungeonAICharacterFaction, value)
}
}
func normalizeDungeonPVFSymbol(value string) string {
return strings.ToLower(strings.Trim(value, "[] \t\r\n"))
}
func cloneRuntimeDungeonAICharacter(value worldmap.AICharacter) *worldmap.AICharacter {
value.Params = append([]int64(nil), value.Params...)
return &value
}