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

+239
View File
@@ -0,0 +1,239 @@
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
dnfcompanion "longheng.io/server/internal/modules/dnf/companion"
platformpvf "longheng.io/server/internal/platform/pvf"
)
// ProfileOverlayRequest describes an offline build. OutputPVF must point at a
// review/temp directory; this API never writes the runtime PVF in place.
type ProfileOverlayRequest struct {
BasePVF string
OutputPVF string
Profiles []dnfcompanion.AIProfile
Jobs []uint8
Capabilities dnfcompanion.StrategyCapabilities
}
type ProfileOverlayResult struct {
OutputPVF string
OutputSHA256 string
Manifest dnfcompanion.PVFOverlayManifest
}
func sha256File(path string) (string, []byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", nil, err
}
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:]), data, nil
}
// BuildProfileOverlay compiles a profile into the scalar AIC values and the
// native AI/key programs that the checked-in PVF grammar can execute. It
// reopens the generated archive and validates every replacement. The output
// path is caller-owned and should be outside runtime/current.
func BuildProfileOverlay(req ProfileOverlayRequest) (result ProfileOverlayResult, err error) {
if strings.TrimSpace(req.BasePVF) == "" || strings.TrimSpace(req.OutputPVF) == "" {
return result, fmt.Errorf("base and output PVF paths are required")
}
if len(req.Profiles) == 0 || len(req.Profiles) != len(req.Jobs) {
return result, fmt.Errorf("profiles/jobs must be non-empty and have equal length")
}
baseHash, _, err := sha256File(req.BasePVF)
if err != nil {
return result, fmt.Errorf("read base PVF: %w", err)
}
archive, err := platformpvf.Open(req.BasePVF)
if err != nil {
return result, err
}
caps := req.Capabilities
if caps == (dnfcompanion.StrategyCapabilities{}) {
caps = dnfcompanion.DefaultStrategyCapabilities()
}
artifacts := make([]dnfcompanion.AICArtifact, 0, len(req.Profiles))
replacements := make([]platformpvf.RawReplacement, 0, len(req.Profiles)*4)
seen := make(map[string]string, len(req.Profiles))
replacementSeen := make(map[string]string, len(req.Profiles)*4)
appendReplacement := func(replacement platformpvf.RawReplacement) error {
key := strings.ToLower(strings.ReplaceAll(replacement.Path, "\\", "/"))
hash := sha256.Sum256(replacement.Data)
hashText := hex.EncodeToString(hash[:])
if previous, ok := replacementSeen[key]; ok {
if previous != hashText {
return fmt.Errorf("two profiles generate different data for %s", replacement.Path)
}
return nil
}
replacementSeen[key] = hashText
replacements = append(replacements, replacement)
return nil
}
for index, profile := range req.Profiles {
profile = dnfcompanion.NormalizeAIProfile(profile)
artifact, compileErr := dnfcompanion.CompileAICArtifactForJob(profile, caps, req.Jobs[index])
if compileErr != nil {
return result, fmt.Errorf("compile profile %d: %w", index, compileErr)
}
path := artifact.AICPath
key := strings.ToLower(strings.ReplaceAll(path, "\\", "/"))
profileKey := fmt.Sprintf("%s|%s|%s|%t|%d|%t|%#v", profile.Behavior, profile.BuffPolicy, profile.ComboPreset, profile.ComboEnabled, profile.AOEMinTargets, profile.ProtectLeader, profile.Tuning)
if previous, ok := seen[key]; ok {
if previous != profileKey {
return result, fmt.Errorf("two profiles target %s with different artifacts", path)
}
continue
}
raw, readErr := archive.ReadRaw(path)
if readErr != nil {
return result, fmt.Errorf("read base AIC %s: %w", path, readErr)
}
profileReplacements, patchErr := patchCompanionAICProfile(archive, raw, artifact, profile)
if patchErr != nil {
return result, fmt.Errorf("patch AIC %s: %w", path, patchErr)
}
for _, replacement := range profileReplacements {
if err := appendReplacement(replacement); err != nil {
return result, err
}
}
artifacts = append(artifacts, artifact)
seen[key] = profileKey
}
// 重写 24 个按键程序文件本体 —— 这是 legacy 路径(main.go 里 keyFiles /
// directPoolFiles 那两个循环)一直在做、而 --profile-overlay 一直漏掉的一步。
//
// 漏掉的后果(实测取证):AIC 的 [key stream] 引用的还是 DB4 那批 25 字节旧
// key(magician/biske2、magician/flea_inflame_hands2),零售客户端把它编译成
// **0x80 字节**的运行时按键程序,而默认绑定表的
// kRuntimeDirectKeyBytecodeSize 要求 **0x180**,于是客户端直接拒绝:
// runtime-key projection blocked reason=direct-bytecode-shape ... size=128
// -> runtime_keys_applied=0 -> 快捷栏 12 格技能没有绑定 -> 支援兵不放技能。
//
// 这里按 legacy 的同一套生成器重写:
// 前段 12 格 -> [input] <a/s/d/f/g/h/q/w/e/r/t/y> 的按键程序
// 直接池 12 格 -> 含 skill:511 的占位程序(对应 kPlaceholderSkillId=511,
// 客户端会比对 kSkillCodeBias(0x100)+511 = 0x2FF)
for index, path := range companionFrontKeyFiles {
if index >= len(companionQuickSlotKeys) {
break
}
if err := appendReplacement(platformpvf.RawReplacement{
Path: path,
Data: buildQuickSlotKey(archive, companionQuickSlotKeys[index]),
}); err != nil {
return result, err
}
}
for _, path := range companionDirectKeyFiles {
if err := appendReplacement(platformpvf.RawReplacement{
Path: path,
Data: buildDirectPlaceholderKey(archive),
}); err != nil {
return result, err
}
}
sort.Slice(replacements, func(i, j int) bool {
return strings.ToLower(replacements[i].Path) < strings.ToLower(replacements[j].Path)
})
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Path < artifacts[j].Path })
patched, err := archive.RepackRawAndStringPools(replacements)
if err != nil {
return result, fmt.Errorf("repack isolated PVF: %w", err)
}
if err := os.MkdirAll(filepath.Dir(req.OutputPVF), 0o755); err != nil {
return result, err
}
if err := os.WriteFile(req.OutputPVF, patched, 0o644); err != nil {
return result, err
}
verify, err := platformpvf.Open(req.OutputPVF)
if err != nil {
return result, fmt.Errorf("open generated PVF: %w", err)
}
for _, replacement := range replacements {
got, err := verify.ReadRaw(replacement.Path)
if err != nil {
return result, fmt.Errorf("read generated program %s: %w", replacement.Path, err)
}
if !bytes.Equal(got, replacement.Data) {
return result, fmt.Errorf("generated program %s differs from compiled data", replacement.Path)
}
}
for _, artifact := range artifacts {
raw, readErr := verify.ReadRaw(artifact.AICPath)
if readErr != nil {
return result, fmt.Errorf("read generated AIC %s: %w", artifact.AICPath, readErr)
}
if err := validateAICReadback(verify, raw, artifact); err != nil {
return result, fmt.Errorf("validate generated AIC %s: %w", artifact.AICPath, err)
}
}
manifest, err := dnfcompanion.NewPVFOverlayManifest(req.BasePVF, baseHash, artifacts)
if err != nil {
return result, err
}
outputHash, _, err := sha256File(req.OutputPVF)
if err != nil {
return result, err
}
return ProfileOverlayResult{OutputPVF: req.OutputPVF, OutputSHA256: outputHash, Manifest: manifest}, nil
}
func patchAICArtifactScalars(a *platformpvf.Archive, raw []byte, artifact dnfcompanion.AICArtifact) (patched []byte, err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("scalar patch failed: %v", recovered)
}
}()
values, err := dnfcompanion.ReadAICArtifact(artifact.AICText)
if err != nil {
return nil, err
}
patched = append([]byte(nil), raw...)
for _, tag := range []string{"[think term]", "[destination change term]", "[keep distance with target]", "[warlike]", "[vision]"} {
patched = replaceIntAfterTag(patched, toks(a, patched), tag, values[tag])
}
return patched, nil
}
func validateAICReadback(a *platformpvf.Archive, raw []byte, artifact dnfcompanion.AICArtifact) error {
if err := validateCompanionKeyStream(a, raw); err != nil {
return err
}
want, err := dnfcompanion.ReadAICArtifact(artifact.AICText)
if err != nil {
return err
}
for key, value := range want {
got, ok := readIntAfterTag(a, raw, key)
if !ok || got != value {
return fmt.Errorf("%s=%v want=%d", key, got, value)
}
}
return nil
}
func readIntAfterTag(a *platformpvf.Archive, raw []byte, tag string) (int, bool) {
items := toks(a, raw)
for index, item := range items {
if index+1 < len(items) && item.typ == 3 && strings.EqualFold(item.txt, tag) {
next := items[index+1]
if next.typ == 0 {
return int(next.val), true
}
}
}
return 0, false
}