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