DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+236
View File
@@ -0,0 +1,236 @@
// Loads the auxiliary-profession catalog out of Script.pvf and reports what the
// disjointer loader actually parsed, then simulates PlanDisjointer across the
// machine-grade / rarity / state matrix so the produced materials can be checked
// against the archive definition field by field.
package main
import (
"context"
"fmt"
"os"
"sort"
"strings"
dnfexpertjob "longheng.io/server/internal/modules/dnf/expertjob"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func main() {
archivePath := `runtime/data/dnf/Script.pvf`
if len(os.Args) > 1 {
archivePath = os.Args[1]
}
archive, err := platformpvf.Open(archivePath)
if err != nil {
panic(err)
}
if needle := os.Getenv("FINDSTACK"); needle != "" {
// Resolve stackable items by name substring.
lst, lstErr := archive.ReadText("stackable/stackable.lst")
if lstErr != nil {
panic(lstErr)
}
fields := strings.Fields(lst)
hits := 0
for i := 0; i+1 < len(fields) && hits < 40; i += 2 {
rel := strings.Trim(fields[i+1], "`")
text, readErr := archive.ReadText("stackable/" + rel)
if readErr != nil || !strings.Contains(text, needle) {
continue
}
doc, parseErr := dnfpvf.Parse(rel, text)
if parseErr != nil {
continue
}
name, _ := doc.Text("name")
if !strings.Contains(name, needle) {
continue
}
fmt.Printf("id=%-10s %s\n", fields[i], name)
hits++
}
fmt.Printf("hits=%d\n", hits)
return
}
if needle := os.Getenv("FINDEQUIP"); needle != "" {
// Resolve a known equipment by name substring and report its [rarity],
// which is the only way to name a numeric tier from evidence.
lst, lstErr := archive.ReadText("equipment/equipment.lst")
if lstErr != nil {
panic(lstErr)
}
fields := strings.Fields(lst)
hits := 0
for i := 0; i+1 < len(fields) && hits < 12; i += 2 {
rel := strings.Trim(fields[i+1], "`")
text, readErr := archive.ReadText("equipment/" + rel)
if readErr != nil || !strings.Contains(text, needle) {
continue
}
doc, parseErr := dnfpvf.Parse(rel, text)
if parseErr != nil {
continue
}
name, _ := doc.Text("name")
rarity, _ := doc.Int("rarity")
level, _ := doc.Int("minimum level")
fmt.Printf("id=%-9s rarity=%d level=%-3d %s\n", fields[i], rarity, level, name)
hits++
}
fmt.Printf("hits=%d\n", hits)
return
}
if os.Getenv("RARITY") != "" {
// Sample equipment names per [rarity] value so the numeric tier can be
// named without guessing.
lst, lstErr := archive.ReadText("equipment/equipment.lst")
if lstErr != nil {
panic(lstErr)
}
fields := strings.Fields(lst)
samples := map[int64][]string{}
for i := 0; i+1 < len(fields); i += 2 {
if len(samples) >= 7 {
full := true
for _, v := range samples {
if len(v) < 3 {
full = false
}
}
if full {
break
}
}
rel := strings.Trim(fields[i+1], "`")
text, readErr := archive.ReadText("equipment/" + rel)
if readErr != nil {
continue
}
doc, parseErr := dnfpvf.Parse(rel, text)
if parseErr != nil {
continue
}
rarity, ok := doc.Int("rarity")
if !ok || len(samples[rarity]) >= 4 {
continue
}
// Endgame gear only: low-level rows are dominated by avatars and
// costume pieces whose names say nothing about the tier.
if level, hasLevel := doc.Int("minimum level"); !hasLevel || level < 85 {
continue
}
name, _ := doc.Text("name")
samples[rarity] = append(samples[rarity], name)
}
keys := make([]int, 0, len(samples))
for k := range samples {
keys = append(keys, int(k))
}
sort.Ints(keys)
for _, k := range keys {
fmt.Printf("rarity %d -> %v\n", k, samples[int64(k)])
}
return
}
if needle := os.Getenv("GREPALL"); needle != "" {
// Locate a UI/message string anywhere in the archive. PVF entries are
// stored encoded, so a raw byte search over Script.pvf finds nothing;
// every file has to be decoded before matching.
hits := 0
for _, file := range archive.Files() {
text, readErr := archive.FileText(file.Index)
if readErr != nil || !strings.Contains(text, needle) {
continue
}
for _, line := range strings.Split(text, "\n") {
if !strings.Contains(line, needle) {
continue
}
fmt.Printf("%s\t%s\n", file.Path, strings.TrimSpace(line))
hits++
if hits >= 60 {
break
}
}
if hits >= 60 {
break
}
}
fmt.Printf("hits=%d\n", hits)
return
}
if raw := os.Getenv("RAW"); raw != "" {
// RAW=1 keeps the original disjointer dump; any other value is read as
// an archive-relative path so other definitions can be inspected too.
path := "character/expertjob/disjointer.exj"
if raw != "1" {
path = raw
}
text, readErr := archive.ReadText(path)
if readErr != nil {
panic(readErr)
}
fmt.Print(text)
return
}
catalog, err := dnfexpertjob.Load(context.Background(), archive)
if err != nil {
panic(err)
}
config, ok := catalog.Disjointer()
if !ok {
panic("disjointer config missing")
}
fmt.Printf("initialEndurance=%d baseConst=%v enduranceReduce=[%d,%d] gainExp=[%d,%d]\n",
config.InitialEndurance, config.BaseConst,
config.EnduranceReduceMin, config.EnduranceReduceMax,
config.ExperienceGainMin, config.ExperienceGainMax)
fmt.Printf("selfService chance=%d item=%d count=%d\n",
config.SelfServiceChance, config.SelfServiceItemID, config.SelfServiceItemCount)
fmt.Printf("results=%d additionalTables=%d bigWinTables=%d\n\n",
len(config.Results), len(config.AdditionalResults), len(config.BigWinResults))
dumpTables := func(label string, tables map[int][]dnfexpertjob.DisjointerSelectionRule) {
keys := make([]int, 0, len(tables))
for k := range tables {
keys = append(keys, k)
}
sort.Ints(keys)
fmt.Printf("-- %s --\n", label)
for _, k := range keys {
fmt.Printf(" table %d:\n", k)
for _, r := range tables[k] {
fmt.Printf(" grade %d..%-2d item=%-9d weight=%-5d countDivisor=%v\n",
r.MinimumGrade, r.MaximumGrade, r.ItemID, r.Weight, r.CountDivisor)
}
}
fmt.Println()
}
dumpTables("additional result", config.AdditionalResults)
dumpTables("big win result", config.BigWinResults)
// Deterministic rng so the matrix is reproducible: always take the first
// eligible weighted row and never roll a big win.
rng := func(n int) int { return 0 }
fmt.Println("-- PlanDisjointer matrix (equipmentGrade=90, sellGold=10000, deterministic rng) --")
for grade := 1; grade <= 11; grade++ {
for rarity := 0; rarity <= 6; rarity++ {
for state := 0; state <= 2; state++ {
meta := dnfexpertjob.EquipmentMetadata{
Rarity: rarity, Level: 90, Grade: 90, State: state, SellGold: 10000,
}
plan, planErr := catalog.PlanDisjointer(100, grade, meta, false, rng)
if planErr != nil {
continue
}
fmt.Printf(" grade=%-2d rarity=%d state=%d ->", grade, rarity, state)
for _, m := range plan.Materials {
fmt.Printf(" %d x%d", m.ItemID, m.Count)
}
fmt.Printf(" (endurance-%d exp+%d)\n", plan.EnduranceReduction, plan.ExperienceGain)
}
}
}
}