Files
90CNServer/server/internal/services/dnfbridge/dungeon_extended_actor_runtime.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

332 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}