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

313 lines
9.8 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"math"
"os"
"path"
"sort"
"strconv"
"strings"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
platformpvf "longheng.io/server/internal/platform/pvf"
)
type skillRoleInput struct {
Schema int `json:"schema"`
RefreshMS uint32 `json:"refresh_ms"`
Common map[string]json.RawMessage `json:"common"`
Jobs map[string]map[string]json.RawMessage `json:"jobs"`
PassiveGrow []struct {
Job uint8 `json:"job"`
File string `json:"file"`
Grow uint8 `json:"grow"`
} `json:"passive_grow"`
}
type skillPolicyRow struct {
Job uint8 `json:"job"`
ID uint16 `json:"id"`
Purpose uint8 `json:"purpose"`
Flags uint8 `json:"flags"`
Group uint8 `json:"exclusive_group"`
PassiveGrow uint16 `json:"passive_grow"`
Cooldown uint32 `json:"cooldown_ms"`
Refresh uint32 `json:"refresh_ms"`
Path string `json:"path"`
Name string `json:"name"`
}
var policyPurposes = map[string]uint8{
"party_buff": 2, "once_party_buff": 2, "heal": 3, "support_awakening": 5,
"self_buff": 6, "once_self_buff": 6, "defensive_support": 7, "unsupported": 8, "summon": 9, "self_heal": 10,
}
// Only the main dungeon cooldown is authoritative. Apocalypse has a 145000
// reference-skill value before its actual 170000 dungeon cooldown. Never use
// first-number lookup or the PvP table for native action timing.
func currentDungeonCooldown(a *platformpvf.Archive, raw []byte) (uint32, error) {
depth, reference, alternate := 0, 0, 0
var global uint32
items := toks(a, raw)
for i, t := range items {
if t.typ != 3 {
continue
}
switch strings.ToLower(t.txt) {
case "[reference skill]":
reference++
case "[/reference skill]":
reference--
case "[dungeon]":
depth++
case "[/dungeon]":
depth--
case "[pvp]", "[death tower]", "[warroom]":
alternate++
case "[/pvp]", "[/death tower]", "[/warroom]":
alternate--
case "[cool time]":
if alternate != 0 || reference != 0 {
continue
}
maximum := float64(0)
count := 0
for j := i + 1; j < len(items) && items[j].typ != 3; j++ {
v := float64(items[j].val)
if items[j].typ == 2 {
v = float64(math.Float32frombits(uint32(items[j].val)))
} else if items[j].typ != 0 {
continue
}
if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 || v > 3600000 {
return 0, fmt.Errorf("invalid main dungeon cooldown %g", v)
}
maximum = math.Max(maximum, v)
count++
}
if count != 2 {
return 0, fmt.Errorf("dungeon cooldown has %d values, expected rank endpoints", count)
}
value := uint32(math.Ceil(maximum)) // Conservative rank maximum, no guessed interpolation.
if depth > 0 {
return value, nil
}
global = value // Files without [dungeon] store the main value at top level.
}
}
return global, nil
}
func compileSkillPolicy(a *platformpvf.Archive, input skillRoleInput) ([]skillPolicyRow, error) {
if input.Schema != 1 || input.RefreshMS < 1000 || input.RefreshMS > 3600000 {
return nil, fmt.Errorf("invalid skill role schema/refresh policy")
}
master, err := a.ReadText("skill/skilllist.lst")
if err != nil {
return nil, err
}
doc, err := dnfpvf.Parse("skill/skilllist.lst", master)
if err != nil {
return nil, err
}
var rows []skillPolicyRow
seenJobs := map[uint8]bool{}
for _, entry := range dnfpvf.ParseList(doc) {
if entry.ID < 0 || entry.ID >= 16 {
return nil, fmt.Errorf("unsupported job list %d", entry.ID)
}
job := uint8(entry.ID)
seenJobs[job] = true
jobRules, ok := input.Jobs[strconv.Itoa(int(job))]
if !ok {
return nil, fmt.Errorf("missing job rules %d", job)
}
listPath := path.Join("skill", entry.Path)
text, err := a.ReadText(listPath)
if err != nil {
return nil, err
}
list, err := dnfpvf.Parse(listPath, text)
if err != nil {
return nil, err
}
seenFiles := map[string]bool{}
roleFiles := map[string]string{}
nativeFiles := map[string]bool{}
groups := map[string]uint8{}
for _, set := range []map[string]json.RawMessage{input.Common, jobRules} {
for role, encoded := range set {
if role == "exclusive_groups" {
var named [][]string
if err := json.Unmarshal(encoded, &named); err != nil {
return nil, err
}
for group, files := range named {
for _, file := range files {
groups[strings.ToLower(file)] = uint8(group + 1)
}
}
continue
}
var files []string
if err := json.Unmarshal(encoded, &files); err != nil {
return nil, err
}
for _, file := range files {
key := strings.ToLower(file)
if role == "native_buff" {
nativeFiles[key] = true
continue
}
if _, ok := policyPurposes[role]; !ok {
return nil, fmt.Errorf("unknown purpose %s", role)
}
if old, exists := roleFiles[key]; exists && old != role {
return nil, fmt.Errorf("conflicting roles job=%d %s", job, file)
}
roleFiles[key] = role
}
}
}
seenIDs := map[uint16]bool{}
for _, skill := range dnfpvf.ParseList(list) {
if skill.ID < 0 || skill.ID >= 512 {
return nil, fmt.Errorf("job %d skill %d exceeds native projection contract", job, skill.ID)
}
id := uint16(skill.ID)
if seenIDs[id] {
return nil, fmt.Errorf("duplicate job %d skill %d", job, id)
}
seenIDs[id] = true
skillPath := path.Join(path.Dir(listPath), skill.Path)
text, err := a.ReadText(skillPath)
if err != nil {
return nil, err
}
d, err := dnfpvf.Parse(skillPath, text)
if err != nil {
return nil, err
}
name, _ := d.Text("name")
kind, _ := d.Text("type")
row := skillPolicyRow{Job: job, ID: id, Path: skillPath, Name: name, Purpose: 4}
file := strings.ToLower(path.Base(skillPath))
seenFiles[file] = true
if strings.EqualFold(kind, "[active]") {
row.Purpose = 1
role, exists := roleFiles[file]
if exists {
row.Purpose = policyPurposes[role]
if strings.HasPrefix(role, "once_") || role == "summon" {
row.Flags |= 1
}
row.Group = groups[file]
if nativeFiles[file] {
if job != 4 || row.Purpose != 2 {
return nil, fmt.Errorf("unverified native buff rule %d/%s", job, file)
}
row.Flags |= 2
}
if row.Purpose == 2 || row.Purpose == 6 {
row.Refresh = input.RefreshMS
}
}
raw, err := a.ReadRaw(skillPath)
if err != nil {
return nil, err
}
row.Cooldown, err = currentDungeonCooldown(a, raw)
if err != nil {
return nil, fmt.Errorf("%s: %w", skillPath, err)
}
if row.Purpose == 1 {
explain, _ := d.Text("explain")
for _, phrase := range []string{"周围敌人", "周围的敌人", "范围内的敌人", "范围内所有敌人", "结界内的敌人", "所有敌人", "大范围", "广范围", "多名敌人"} {
if strings.Contains(explain, phrase) {
row.Flags |= 4
break
}
}
}
}
for _, exception := range input.PassiveGrow {
if exception.Job == job && strings.EqualFold(exception.File, file) {
if exception.Grow >= 16 {
return nil, fmt.Errorf("invalid passive grow")
}
row.PassiveGrow |= 1 << exception.Grow
}
}
rows = append(rows, row)
}
for file := range roleFiles {
if !seenFiles[file] {
return nil, fmt.Errorf("job %d role references missing skill file %s", job, file)
}
}
for file := range nativeFiles {
if !seenFiles[file] {
return nil, fmt.Errorf("missing native buff %d/%s", job, file)
}
}
}
if len(seenJobs) != 16 {
return nil, fmt.Errorf("expected 16 current job lists, got %d", len(seenJobs))
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].Job != rows[j].Job {
return rows[i].Job < rows[j].Job
}
return rows[i].ID < rows[j].ID
})
return rows, nil
}
func writeSkillPolicy(base, rulesPath, headerPath, catalogPath string) error {
a, err := platformpvf.Open(base)
if err != nil {
return err
}
raw, err := os.ReadFile(rulesPath)
if err != nil {
return err
}
var input skillRoleInput
if err = json.Unmarshal(raw, &input); err != nil {
return err
}
rows, err := compileSkillPolicy(a, input)
if err != nil {
return err
}
baseHash, _, err := sha256File(base)
if err != nil {
return err
}
var b bytes.Buffer
fmt.Fprintf(&b, "// Generated by companionpvf --skill-policy from current PVF %s.\n// Edit data/companion_skill_roles.json and regenerate; do not edit this table.\n#pragma once\n#include <cstdint>\n#include <cstddef>\nnamespace dnf90_companion_skill_data {\nstruct Rule { std::uint8_t job; std::uint16_t id; std::uint8_t purpose,flags,group; std::uint16_t passiveGrow; std::uint32_t cooldown,refresh; };\nconstexpr Rule rules[] = {\n", baseHash)
for _, r := range rows {
fmt.Fprintf(&b, "{%d,%d,%d,%d,%d,%d,%d,%d}, // %s\n", r.Job, r.ID, r.Purpose, r.Flags, r.Group, r.PassiveGrow, r.Cooldown, r.Refresh, r.Path)
}
fmt.Fprintln(&b, "};\ninline const Rule* Find(std::uint8_t job, std::uint32_t id) {\n if (job>=16 || id>=512) return nullptr;\n std::size_t low=0, high=sizeof(rules)/sizeof(rules[0]); const auto key=(std::uint32_t(job)<<16)|id;\n while (low<high) {const auto mid=low+(high-low)/2; const auto candidate=(std::uint32_t(rules[mid].job)<<16)|rules[mid].id; if (candidate<key) low=mid+1; else high=mid;}\n return low<sizeof(rules)/sizeof(rules[0]) && rules[low].job==job && rules[low].id==id ? &rules[low] : nullptr;\n}\n}")
if err = os.WriteFile(headerPath, b.Bytes(), 0644); err != nil {
return err
}
catalog := struct {
PVFSHA256 string `json:"pvf_sha256"`
Rows []skillPolicyRow `json:"skills"`
}{baseHash, rows}
data, err := json.MarshalIndent(catalog, "", " ")
if err != nil {
return err
}
if err = os.WriteFile(catalogPath, data, 0644); err != nil {
return err
}
fmt.Printf("generated jobs=16 skills=%d header=%s catalog=%s\n", len(rows), headerPath, catalogPath)
return nil
}