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

293 lines
9.1 KiB
Go

// pvfsurvey 是一次性 PVF 数据勘察程序(不进入生产路径)。
// 加载真实 Script.pvf,回答关于"事件怪物位类遭遇"的五个问题:
//
// Q1 含 EventMonsterPositions 的 dungeon 地图总量与分布
// Q2 SpecialPassiveObject 生成 monster 的 (ObjectID, monsterCode) 组合
// Q3 被动对象定义文件是否声明行为类型/事件脚本
// Q4 [destroy object] clear 条件与特殊/普通被动对象的关联
// Q5 电梯型结构(≥1 EventMonsterPositions + special 生成 monster)候选清单
//
// 用法:
//
// go run ./cmd/scratch/pvfsurvey -phase worldmap # Q1/Q2/Q4/Q5
// go run ./cmd/scratch/pvfsurvey -phase objects # Q3(读取全部被动对象定义)
// go run ./cmd/scratch/pvfsurvey -phase all # 全部
//
// 结果写入 cmd/scratch/pvfsurvey/out/。
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"path"
"regexp"
"sort"
"strings"
"time"
"longheng.io/server/internal/modules/dnf/worldmap"
platformpvf "longheng.io/server/internal/platform/pvf"
)
const (
defaultPVFPath = `D:/DNF/runtime/data/dnf/Script.pvf`
outDir = `D:/DNF/go-server/cmd/scratch/pvfsurvey/out`
)
var lstEntryPattern = regexp.MustCompile("([0-9]+)\\s+`([^`]+)`")
func normSymbol(value string) string {
return strings.ToLower(strings.Trim(value, "[] \t\r\n"))
}
func normPath(value string) string {
return strings.ToLower(strings.ReplaceAll(strings.TrimSpace(value), "\\", "/"))
}
// ------------------------- 数据结构 -------------------------
type mapUsage struct {
DungeonID int64 `json:"dungeon_id"`
MazeIndex int `json:"maze_index"`
X int64 `json:"x,omitempty"`
Y int64 `json:"y,omitempty"`
Source string `json:"source"`
}
func (u mapUsage) String() string {
if u.Source == "ownership" {
return fmt.Sprintf("dungeon=%d(ownership)", u.DungeonID)
}
return fmt.Sprintf("dungeon=%d maze=%d (%d,%d) %s", u.DungeonID, u.MazeIndex, u.X, u.Y, u.Source)
}
type q1Map struct {
MapID int64 `json:"map_id"`
Path string `json:"path"`
Name string `json:"name,omitempty"`
Positions int `json:"positions"`
Dungeon []string `json:"dungeons,omitempty"`
}
type comboKey struct {
ObjectID int64
Code int64
}
type comboInfo struct {
ObjectID int64 `json:"object_id"`
Code int64 `json:"monster_code"`
Count int `json:"count"`
Maps map[int64]bool `json:"-"`
Dungeons map[int64]bool `json:"-"`
ObjRef string `json:"obj_ref,omitempty"`
MapList []int64 `json:"maps"`
DgList []int64 `json:"dungeons"`
Levels map[int64]bool `json:"-"`
LevelVal []int64 `json:"levels,omitempty"`
RawKinds map[string]bool `json:"-"`
}
type q4Condition struct {
DungeonID int64 `json:"dungeon_id"`
DungeonName string `json:"dungeon_name,omitempty"`
MazeIndex int `json:"maze_index"`
TargetID int64 `json:"target_id"`
Count int64 `json:"count"`
ObjRef string `json:"obj_ref,omitempty"`
Placements []string `json:"placements"`
Status string `json:"status"`
}
type q5Candidate struct {
MapID int64 `json:"map_id"`
Path string `json:"path"`
Name string `json:"name,omitempty"`
Positions int `json:"positions"`
PassiveIDs []int64 `json:"passive_object_ids"`
SpecialObjects []string `json:"special_objects"`
ObjectClasses string `json:"object_classes"`
Dungeons []string `json:"dungeons,omitempty"`
IsDungeon53 bool `json:"is_known_dungeon_53"`
}
// ------------------------- 主流程 -------------------------
func main() {
pvfPath := flag.String("pvf", defaultPVFPath, "Script.pvf 路径")
phase := flag.String("phase", "all", "worldmap|objects|all")
objLimit := flag.Int("obj-limit", 0, "Q3 最多读取多少个 obj 定义(0=不限)")
flag.Parse()
started := time.Now()
archive, err := platformpvf.Open(*pvfPath)
if err != nil {
panic(err)
}
snap := archive.Snapshot()
fmt.Printf("[survey] pvf opened: format=%s files=%d size=%d checksum=%s (%.1fs)\n",
snap.Format, snap.FileCount, snap.Size, snap.Checksum[:16], time.Since(started).Seconds())
fmt.Printf("[survey] loading worldmap table ...\n")
loadStart := time.Now()
table, err := worldmap.LoadSource(context.Background(), archive, worldmap.Options{})
if err != nil {
panic(err)
}
tableSnap := table.Snapshot()
fmt.Printf("[survey] worldmap loaded: %+v (%.1fs)\n", tableSnap, time.Since(loadStart).Seconds())
// 基础索引
dungeonNames := map[int64]string{}
for _, d := range table.Dungeons() {
dungeonNames[d.ID] = d.Metadata.Name
}
mapUsages := buildMapUsages(table)
// passiveobject.lst 索引(Q2/Q3/Q4/Q5 共用)
objRefs := loadPassiveObjectList(archive)
fmt.Printf("[survey] passiveobject.lst entries=%d\n", len(objRefs))
// 地图实际摆放对象集合与行为画像(Q3/Q5 共用)
usedOnMaps, usedOnDungeonMaps := collectUsedObjects(table, mapUsages)
profileStart := time.Now()
profiles := buildObjProfiles(archive, objRefs, usedOnMaps)
fmt.Printf("[survey] obj profiles built: used_on_maps=%d used_on_dungeon_maps=%d (%.1fs)\n",
len(usedOnMaps), len(usedOnDungeonMaps), time.Since(profileStart).Seconds())
report := map[string]any{
"pvf": map[string]any{
"path": *pvfPath, "format": string(snap.Format), "file_count": snap.FileCount,
"size": snap.Size, "checksum": snap.Checksum,
},
"worldmap_snapshot": tableSnap,
"generated_at": time.Now().Format(time.RFC3339),
}
if *phase == "worldmap" || *phase == "all" {
report["q1"] = runQ1(table, mapUsages, dungeonNames)
report["q2"] = runQ2(table, mapUsages, dungeonNames, objRefs)
report["q4"] = runQ4(table, mapUsages, dungeonNames, objRefs)
report["q5"] = runQ5(table, mapUsages, dungeonNames, objRefs, profiles)
}
if *phase == "objects" || *phase == "all" {
report["q3"] = runQ3(archive, table, mapUsages, objRefs, profiles, usedOnMaps, usedOnDungeonMaps, *objLimit)
}
if err := os.MkdirAll(outDir, 0o755); err != nil {
panic(err)
}
data, err := json.MarshalIndent(report, "", " ")
if err != nil {
panic(err)
}
reportPath := path.Join(outDir, "survey_report.json")
if *phase != "all" {
reportPath = path.Join(outDir, fmt.Sprintf("survey_report_%s.json", *phase))
}
if err := os.WriteFile(reportPath, data, 0o644); err != nil {
panic(err)
}
fmt.Printf("[survey] report written: %s (%.1fs total)\n", reportPath, time.Since(started).Seconds())
}
func collectUsedObjects(table *worldmap.Table, usages map[int64][]mapUsage) (map[int64]bool, map[int64]bool) {
usedAll := map[int64]bool{}
usedDungeon := map[int64]bool{}
for _, m := range table.Maps() {
placed := make([]int64, 0, len(m.PassiveObjects)+len(m.SpecialPassiveObjects))
for _, o := range m.PassiveObjects {
placed = append(placed, o.ObjectID)
}
for _, o := range m.SpecialPassiveObjects {
placed = append(placed, o.ObjectID)
}
for _, id := range placed {
usedAll[id] = true
if len(usages[m.ID]) > 0 {
usedDungeon[id] = true
}
}
}
return usedAll, usedDungeon
}
func buildMapUsages(table *worldmap.Table) map[int64][]mapUsage {
usages := map[int64][]mapUsage{}
add := func(mapID, dungeonID int64, mazeIndex int, x, y int64, source string) {
key := mapUsage{DungeonID: dungeonID, MazeIndex: mazeIndex, X: x, Y: y, Source: source}
for _, existing := range usages[mapID] {
if existing == key {
return
}
}
usages[mapID] = append(usages[mapID], key)
}
for _, d := range table.Dungeons() {
for _, maze := range d.Mazes {
for _, spec := range maze.MapSpecifications {
for _, id := range spec.MapIDs {
add(id, d.ID, maze.Index, spec.Coordinate.X, spec.Coordinate.Y, "spec:"+spec.Type)
}
}
for _, spec := range maze.BossSpecifications {
for _, id := range spec.MapIDs {
add(id, d.ID, maze.Index, spec.Coordinate.X, spec.Coordinate.Y, "boss-spec:"+spec.Type)
}
}
for _, spec := range maze.LayeredSpecifications {
for _, id := range spec.MapIDs {
add(id, d.ID, maze.Index, spec.Coordinate.X, spec.Coordinate.Y, "layered-spec:"+spec.Type)
}
}
}
}
for _, m := range table.Maps() {
for _, id := range m.DungeonIDs {
add(m.ID, id, -1, 0, 0, "ownership")
}
}
return usages
}
func loadPassiveObjectList(archive *platformpvf.Archive) map[int64]string {
text, err := archive.ReadText("passiveobject/passiveobject.lst")
if err != nil {
fmt.Printf("[survey] WARN: passiveobject.lst unreadable: %v\n", err)
return map[int64]string{}
}
out := map[int64]string{}
for _, m := range lstEntryPattern.FindAllStringSubmatch(text, -1) {
var id int64
fmt.Sscan(m[1], &id)
if _, exists := out[id]; !exists {
out[id] = strings.ReplaceAll(m[2], "\\", "/")
}
}
return out
}
func usageStrings(usages []mapUsage) []string {
if len(usages) == 0 {
return nil
}
out := make([]string, 0, len(usages))
for _, u := range usages {
out = append(out, u.String())
}
sort.Strings(out)
return out
}
func sortedIDs(set map[int64]bool) []int64 {
out := make([]int64, 0, len(set))
for id := range set {
out = append(out, id)
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out
}