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,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
|
||||
}
|
||||
Reference in new issue
Block a user