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