Files
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

347 lines
12 KiB
Go

package raid
import (
"fmt"
"strings"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
type FiendWarArea struct {
Index, WorldMap, Dungeon, LimitSeconds, MemberMax int
Symbol uint32
Kind string
Next []int
}
type FiendWarInstruction struct {
Op string
Symbol uint32
Args []dnfpvf.Token
}
type FiendWarRule struct {
Conditions, Actions []FiendWarInstruction
}
type FiendWarMerchantRate struct {
Kind, Weight, DurationMS int
}
type FiendWarMerchantPosition struct {
Area, X, Y int
}
type FiendWarBattleConfig struct {
Raid *AntonConfig
WaitingTown, WaitingArea int
Areas map[int]FiendWarArea
Symbols map[string]uint32
Rules []FiendWarRule
EnemyRates []int
MerchantPositions []int
MerchantRates []FiendWarMerchantRate
MerchantPositionRows []FiendWarMerchantPosition
DieImprisonSeconds []int
DailyAccountEnterLimit int
WaitingNPCs []int
}
// Native sub_382A910. These are timer kinds, not packet opcodes.
var FiendWarTimerKinds = map[string]byte{
"move_enemy": 0, "create_monster": 1, "activate_radar": 2,
"activate_battery": 3, "top_create_enemy": 4, "mid_create_enemy": 5,
"bot_create_enemy": 6, "top_restore_center": 7, "mid_restore_center": 8,
"bot_restore_center": 9, "warning_enemy": 10, "delete_monster": 11,
}
func parseFiendWarBattle(recruitment *FiendWarConfig, doc, symbolDoc *dnfpvf.Document) (*FiendWarBattleConfig, error) {
c := &FiendWarBattleConfig{Areas: make(map[int]FiendWarArea), Symbols: make(map[string]uint32)}
// The .symbol file pairs an integer with the following section token.
for i, token := range symbolDoc.Tokens {
if token.Kind != dnfpvf.TokenSection {
continue
}
if i == 0 || symbolDoc.Tokens[i-1].Kind != dnfpvf.TokenInt || symbolDoc.Tokens[i-1].Int <= 0 {
return nil, fmt.Errorf("fiendwar symbol %q has no numeric index", token.Value)
}
name := antonSectionName(token.Value)
if _, exists := c.Symbols[name]; exists {
return nil, fmt.Errorf("duplicate fiendwar symbol %s", name)
}
c.Symbols[name] = uint32(symbolDoc.Tokens[i-1].Int)
}
phases, err := antonFindBlocks(doc, "phase", 0, len(doc.Sections))
if err != nil || len(phases) != 1 {
return nil, fmt.Errorf("fiendwar phase: %v", err)
}
end := phases[0].open
c.DailyAccountEnterLimit, err = antonReadPositiveIntSection(doc, "daily account enter limit", 0, end)
if err != nil || c.DailyAccountEnterLimit > 255 {
return nil, fmt.Errorf("invalid fiendwar daily account limit: %v", err)
}
base := &AntonConfig{PhaseMax: 1, MemberMax: recruitment.MemberMax, StartMinimumMembers: recruitment.StartMinimumMembers, EntryCost: recruitment.EntryCost}
if base.StartDelaySeconds, err = antonReadPositiveIntSection(doc, "start delay time", 0, end); err != nil {
return nil, err
}
if base.PhaseTimeoutSeconds, err = antonReadPositiveIntListSection(doc, "phase time over", 0, end, 1); err != nil {
return nil, err
}
if base.MovieEndSeconds, err = antonReadPositiveIntListSection(doc, "movie end time", 0, end, 2); err != nil {
return nil, err
}
values, err := fiendWarInts(doc, "waiting room", 0, end)
if err != nil || len(values) != 2 {
return nil, fmt.Errorf("fiendwar waiting room: %v", err)
}
c.WaitingTown, c.WaitingArea = values[0], values[1]
if c.WaitingNPCs, err = fiendWarInts(doc, "waiting room npc", 0, end); err != nil {
return nil, err
}
if c.DieImprisonSeconds, err = fiendWarInts(doc, "die imprison time", 0, end); err != nil {
return nil, err
}
if len(c.DieImprisonSeconds) == 0 {
return nil, fmt.Errorf("missing fiendwar imprisonment durations")
}
for _, seconds := range c.DieImprisonSeconds {
if seconds <= 0 {
return nil, fmt.Errorf("invalid fiendwar imprisonment duration")
}
}
if c.EnemyRates, err = fiendWarInts(doc, "enemy create rate", 0, end); err != nil {
return nil, err
}
if len(c.EnemyRates) == 0 || len(c.EnemyRates)%2 != 0 {
return nil, fmt.Errorf("invalid fiendwar enemy rates")
}
for i := 0; i < len(c.EnemyRates); i += 2 {
if c.EnemyRates[i] < 1 || c.EnemyRates[i] > 4 || c.EnemyRates[i+1] <= 0 {
return nil, fmt.Errorf("invalid fiendwar enemy weight")
}
}
if values, err = fiendWarInts(doc, "merchant create pos", 0, end); err != nil {
return nil, err
}
if len(values)%3 != 0 {
return nil, fmt.Errorf("invalid fiendwar merchant positions")
}
for i := 0; i < len(values); i += 3 {
row := FiendWarMerchantPosition{Area: values[i], X: values[i+1], Y: values[i+2]}
c.MerchantPositionRows = append(c.MerchantPositionRows, row)
c.MerchantPositions = append(c.MerchantPositions, row.Area)
}
if values, err = fiendWarInts(doc, "merchant create rate", 0, end); err != nil {
return nil, err
}
if len(values) == 0 || len(values)%3 != 0 {
return nil, fmt.Errorf("invalid fiendwar merchant rates")
}
for i := 0; i < len(values); i += 3 {
if values[i] < 0 || values[i+1] <= 0 || values[i+2] <= 0 {
return nil, fmt.Errorf("invalid fiendwar merchant rate")
}
c.MerchantRates = append(c.MerchantRates, FiendWarMerchantRate{Kind: values[i], Weight: values[i+1], DurationMS: values[i+2]})
}
p := phases[0]
name, err := antonParseSingleModeName(doc, p)
if err != nil {
return nil, err
}
phase := AntonPhase{Name: name}
phase.MinimalDungeonClearCount, err = antonReadPositiveIntSection(doc, "minimal dungeon clear count", p.open+1, p.close)
if err != nil {
return nil, err
}
rewardBlocks, err := antonFindBlocks(doc, "clear reward item", p.open+1, p.close)
if err != nil || len(rewardBlocks) != 1 {
return nil, fmt.Errorf("fiendwar rewards: %v", err)
}
if phase.Rewards, err = antonParseRewards(doc, rewardBlocks[0], 0); err != nil {
return nil, err
}
areaBlocks, err := antonFindBlocks(doc, "area info", p.open+1, p.close)
if err != nil {
return nil, err
}
dungeons := make(map[int]bool)
for _, b := range areaBlocks {
a := FiendWarArea{}
for field, dst := range map[string]*int{"area index": &a.Index, "worldmap index": &a.WorldMap, "dungeon index": &a.Dungeon, "member max": &a.MemberMax} {
v, e := fiendWarInts(doc, field, b.open+1, b.close)
if e != nil || len(v) != 1 {
return nil, fmt.Errorf("fiendwar area %s: %v", field, e)
}
*dst = v[0]
}
if a.Index < 1 || a.Index > 127 || a.MemberMax < 1 || a.MemberMax > recruitment.MemberMax {
return nil, fmt.Errorf("invalid fiendwar area %+v", a)
}
if _, exists := c.Areas[a.Index]; exists {
return nil, fmt.Errorf("duplicate fiendwar area %d", a.Index)
}
a.Symbol = c.Symbols[fmt.Sprintf("%d area state", a.Index)]
if a.Symbol == 0 {
return nil, fmt.Errorf("missing fiendwar area symbol %d", a.Index)
}
if a.Next, err = fiendWarInts(doc, "move available map", b.open+1, b.close); err != nil {
return nil, err
}
for i := b.open + 1; i < b.close; i++ {
field := antonSectionName(doc.Sections[i].Name)
if strings.HasSuffix(field, " area") && field != "on giveup area" {
a.Kind = strings.TrimSuffix(field, " area")
}
}
if a.Dungeon > 0 {
if dungeons[a.Dungeon] {
return nil, fmt.Errorf("duplicate fiendwar dungeon %d", a.Dungeon)
}
dungeons[a.Dungeon] = true
if a.LimitSeconds, err = antonReadPositiveIntSection(doc, "dungeon limit time", b.open+1, b.close); err != nil {
return nil, err
}
// Each area clear is one completed dungeon, not Anton's HP counter.
phase.Dungeons = append(phase.Dungeons, AntonDungeonInfo{Index: uint32(a.Dungeon), InitState: AntonDungeonHide, HP: 1, PartyMax: 1})
}
c.Areas[a.Index] = a
}
if _, ok := c.Areas[c.WaitingArea]; !ok {
return nil, fmt.Errorf("missing fiendwar waiting area")
}
for _, a := range c.Areas {
for _, n := range a.Next {
if _, ok := c.Areas[n]; !ok {
return nil, fmt.Errorf("unknown fiendwar adjacent area %d", n)
}
}
}
triggers, err := antonFindBlocks(doc, "trigger", p.open+1, p.close)
if err != nil {
return nil, err
}
behaviors, err := antonFindBlocks(doc, "behavior", p.open+1, p.close)
if err != nil || len(triggers) != len(behaviors) || len(triggers) == 0 {
return nil, fmt.Errorf("fiendwar rule pairs: %v", err)
}
for i, t := range triggers {
b := behaviors[i]
if t.close+1 != b.open {
return nil, fmt.Errorf("fiendwar unpaired trigger %d", i)
}
conditions, e := c.parseInstructions(doc, t, true)
if e != nil {
return nil, e
}
actions, e := c.parseInstructions(doc, b, false)
if e != nil {
return nil, e
}
c.Rules = append(c.Rules, FiendWarRule{conditions, actions})
}
base.Phases = []AntonPhase{phase}
c.Raid = base
return c, nil
}
func fiendWarInts(doc *dnfpvf.Document, name string, start, end int) ([]int, error) {
i, err := antonUniqueSection(doc, name, start, end)
if err != nil {
return nil, err
}
tokens, err := antonSectionTokens(doc, i)
if err != nil {
return nil, err
}
values := make([]int, len(tokens))
for j, t := range tokens {
values[j], err = antonIntToken(t, name)
if err != nil {
return nil, err
}
}
return values, nil
}
func (c *FiendWarBattleConfig) parseInstructions(doc *dnfpvf.Document, b antonSectionBlock, condition bool) ([]FiendWarInstruction, error) {
var out []FiendWarInstruction
for i := b.open + 1; i < b.close; i++ {
name := antonSectionName(doc.Sections[i].Name)
args, err := antonSectionTokens(doc, i)
if err != nil {
return nil, err
}
inst := FiendWarInstruction{Op: name, Args: args}
if name == "if" || name == "set" || name == "on change symbol" {
i++
if i >= b.close {
return nil, fmt.Errorf("fiendwar missing symbol after %s", name)
}
inst.Symbol = c.Symbols[antonSectionName(doc.Sections[i].Name)]
if inst.Symbol == 0 {
return nil, fmt.Errorf("unknown fiendwar symbol %s", doc.Sections[i].Name)
}
inst.Args, err = antonSectionTokens(doc, i)
if err != nil {
return nil, err
}
if name == "if" && len(inst.Args) == 0 {
i++
if i >= b.close || antonSectionName(doc.Sections[i].Name) != "<" {
return nil, fmt.Errorf("unsupported fiendwar comparison")
}
inst.Op = "less"
inst.Args, err = antonSectionTokens(doc, i)
if err != nil {
return nil, err
}
}
}
shape := ""
if condition {
switch inst.Op {
case "if", "less", "on change area state", "on giveup area", "on enter dungeon", "on enter map", "on giveup dungeon", "on change dungeon state":
shape = "i"
case "on change symbol":
shape = ""
case "check area state", "check dungeon state":
shape = "is"
case "check timer end":
shape = "si"
default:
return nil, fmt.Errorf("unsupported fiendwar condition %s", inst.Op)
}
} else {
switch inst.Op {
case "set", "create enemy", "delete monster", "warning enemy":
shape = "i"
case "move enemy":
shape = "ii"
case "set timer":
shape = "sii"
case "reset timer":
shape = "si"
case "create merchant", "create monsters", "delete all enemy", "boss pattern init", "clear phase", "fail fiendwar":
shape = ""
default:
return nil, fmt.Errorf("unsupported fiendwar action %s", inst.Op)
}
}
if len(inst.Args) != len(shape) {
return nil, fmt.Errorf("fiendwar %s argument count", inst.Op)
}
for j, k := range shape {
if k == 'i' && inst.Args[j].Kind != dnfpvf.TokenInt || k == 's' && inst.Args[j].Kind != dnfpvf.TokenString {
return nil, fmt.Errorf("fiendwar %s argument type", inst.Op)
}
}
if strings.Contains(inst.Op, "timer") {
if _, ok := FiendWarTimerKinds[inst.Args[0].Value]; !ok {
return nil, fmt.Errorf("unknown fiendwar timer")
}
}
out = append(out, inst)
}
return out, nil
}