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

240 lines
8.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}