服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
237 lines
6.9 KiB
Go
237 lines
6.9 KiB
Go
// 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)
|
|
}
|
|
}
|
|
}
|
|
}
|