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,264 @@
|
||||
package companion
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AICArtifact is the deterministic, text representation of the part of an
|
||||
// AI profile that is already expressible by the checked-in AIC grammar. It
|
||||
// is deliberately separate from a packed PVF: callers may inspect and
|
||||
// validate this artifact without opening or modifying a runtime archive.
|
||||
//
|
||||
// The Lua source remains the source of truth for the full profile. AICText
|
||||
// contains only whitelisted scalar AIC tags, so unsupported Lua capabilities
|
||||
// cannot accidentally turn into arbitrary PVF tokens.
|
||||
type AICArtifact struct {
|
||||
Path string `json:"path"`
|
||||
TemplateKey string `json:"template_key"`
|
||||
Job uint8 `json:"job"`
|
||||
ClassID uint16 `json:"class_id"`
|
||||
Behavior string `json:"behavior"`
|
||||
ComboPreset string `json:"combo_preset"`
|
||||
APCCode int64 `json:"apc_code"`
|
||||
AICPath string `json:"aic_path"`
|
||||
AICText string `json:"aic_text"`
|
||||
SHA256 string `json:"sha256"`
|
||||
LuaSHA256 string `json:"lua_sha256"`
|
||||
StrategyStatus string `json:"strategy_status"`
|
||||
MissingCapabilities []StrategyCapability `json:"missing_capabilities,omitempty"`
|
||||
}
|
||||
|
||||
// PVFOverlayManifest is a small offline hand-off artifact. The
|
||||
// companionpvf command is expected to consume Path/AICText and encode it
|
||||
// with the PVF archive's string pool in a copied archive; this manifest
|
||||
// itself never writes runtime/data/dnf/Script.pvf.
|
||||
type PVFOverlayManifest struct {
|
||||
Format string `json:"format"`
|
||||
BasePVF string `json:"base_pvf"`
|
||||
BaseSHA256 string `json:"base_sha256,omitempty"`
|
||||
Artifacts []AICArtifact `json:"artifacts"`
|
||||
ManifestHash string `json:"manifest_sha256"`
|
||||
}
|
||||
|
||||
var aicScalarTags = []string{
|
||||
"[think term]",
|
||||
"[destination change term]",
|
||||
"[keep distance with target]",
|
||||
"[warlike]",
|
||||
"[vision]",
|
||||
}
|
||||
|
||||
var aicScalarLine = regexp.MustCompile(`^\[([^\]]+)\]\s*(-?[0-9]+)\s*$`)
|
||||
|
||||
// CompileAICArtifact lowers a validated profile into an inspectable AIC
|
||||
// artifact. It intentionally does not claim that the artifact has been
|
||||
// consumed by a native client; that claim belongs to the action-receipt gate.
|
||||
func CompileAICArtifact(profile AIProfile, caps StrategyCapabilities) (AICArtifact, error) {
|
||||
if !caps.ThinkTerm || !caps.Distance {
|
||||
return AICArtifact{}, fmt.Errorf("AIC scalar compiler requires think_term and target_distance capabilities")
|
||||
}
|
||||
job := uint8(profile.ClassID)
|
||||
return CompileAICArtifactForJob(profile, caps, job)
|
||||
}
|
||||
|
||||
// CompileAICArtifactForJob is the job-aware form used by dungeon projection.
|
||||
// ClassID selects the learned combo branch; job selects the native profession
|
||||
// family and therefore the verified APC/AIC pair.
|
||||
func CompileAICArtifactForJob(profile AIProfile, caps StrategyCapabilities, job uint8) (AICArtifact, error) {
|
||||
compiled, err := CompileAIProfile(profile, caps)
|
||||
if err != nil {
|
||||
return AICArtifact{}, err
|
||||
}
|
||||
profile = NormalizeAIProfile(profile)
|
||||
selector := SelectAITemplate(profile, uint16(job))
|
||||
tuning := EffectiveAITuning(profile.Tuning)
|
||||
lines := []string{
|
||||
"[think term]", strconv.Itoa(tuning.ThinkIntervalMS),
|
||||
"[destination change term]", strconv.Itoa(tuning.RetryMS),
|
||||
"[keep distance with target]", strconv.Itoa(tuning.FollowDistance),
|
||||
"[warlike]", strconv.Itoa(aicWarlike(profile)),
|
||||
"[vision]", strconv.Itoa(tuning.VisionRange),
|
||||
}
|
||||
text := strings.Join(lines, "\n") + "\n"
|
||||
if _, err := ReadAICArtifact(text); err != nil {
|
||||
return AICArtifact{}, fmt.Errorf("generated AIC failed readback: %w", err)
|
||||
}
|
||||
return AICArtifact{
|
||||
// Path is the real AICharacter.lst path that the PVF overlay replaces.
|
||||
// TemplateKey/LuaSHA256 keep the profile identity auditable even when
|
||||
// two profiles share one native profession template.
|
||||
Path: selector.AICPath, TemplateKey: selector.Key,
|
||||
Job: job, ClassID: selector.ClassID, Behavior: selector.Behavior,
|
||||
ComboPreset: selector.ComboPreset,
|
||||
APCCode: selector.APCCode, AICPath: selector.AICPath,
|
||||
AICText: text,
|
||||
SHA256: sha256Hex([]byte(text)),
|
||||
LuaSHA256: compiled.LuaSHA256,
|
||||
StrategyStatus: compiled.Status,
|
||||
MissingCapabilities: append([]StrategyCapability(nil), compiled.MissingCapabilities...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func aicWarlike(profile AIProfile) int {
|
||||
if profile.Behavior == "aggressive" {
|
||||
return 80
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func artifactSlug(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
var b strings.Builder
|
||||
lastDash := false
|
||||
for _, r := range value {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' {
|
||||
b.WriteRune(r)
|
||||
lastDash = false
|
||||
continue
|
||||
}
|
||||
if !lastDash {
|
||||
b.WriteByte('-')
|
||||
lastDash = true
|
||||
}
|
||||
}
|
||||
out := strings.Trim(b.String(), "-")
|
||||
if out == "" {
|
||||
return "auto"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ReadAICArtifact validates and indexes a generated AIC text artifact. The
|
||||
// parser accepts exactly one scalar value for each generated tag and rejects
|
||||
// unknown tags, duplicate tags, malformed numbers, or non-whitespace data.
|
||||
func ReadAICArtifact(text string) (map[string]int, error) {
|
||||
allowed := make(map[string]struct{}, len(aicScalarTags))
|
||||
for _, tag := range aicScalarTags {
|
||||
allowed[tag] = struct{}{}
|
||||
}
|
||||
values := make(map[string]int, len(aicScalarTags))
|
||||
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
|
||||
for i := 0; i < len(lines); {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
match := aicScalarLine.FindStringSubmatch(line)
|
||||
var tag string
|
||||
var number string
|
||||
if len(match) == 3 {
|
||||
tag = "[" + strings.ToLower(match[1]) + "]"
|
||||
number = match[2]
|
||||
} else if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
tag = "[" + strings.ToLower(strings.TrimSuffix(strings.TrimPrefix(line, "["), "]")) + "]"
|
||||
if i+1 >= len(lines) || strings.TrimSpace(lines[i+1]) == "" {
|
||||
return nil, fmt.Errorf("line %d: scalar tag %q has no value", i+1, tag)
|
||||
}
|
||||
number = strings.TrimSpace(lines[i+1])
|
||||
} else {
|
||||
return nil, fmt.Errorf("line %d: expected [tag] followed by an integer", i+1)
|
||||
}
|
||||
if _, ok := allowed[tag]; !ok {
|
||||
return nil, fmt.Errorf("line %d: unsupported AIC tag %q", i+1, tag)
|
||||
}
|
||||
if _, ok := values[tag]; ok {
|
||||
return nil, fmt.Errorf("line %d: duplicate AIC tag %q", i+1, tag)
|
||||
}
|
||||
value, err := strconv.Atoi(number)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("line %d: invalid value: %w", i+1, err)
|
||||
}
|
||||
values[tag] = value
|
||||
if len(match) == 3 {
|
||||
i++
|
||||
} else {
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
if len(values) != len(aicScalarTags) {
|
||||
return nil, fmt.Errorf("AIC scalar set incomplete: got %d of %d tags", len(values), len(aicScalarTags))
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// NewPVFOverlayManifest builds a stable JSON manifest for an offline PVF
|
||||
// overlay. The hash is computed over canonical JSON with ManifestHash
|
||||
// cleared, making repeat compilation byte-for-byte deterministic.
|
||||
func NewPVFOverlayManifest(basePVF, baseSHA256 string, artifacts []AICArtifact) (PVFOverlayManifest, error) {
|
||||
if strings.TrimSpace(basePVF) == "" {
|
||||
return PVFOverlayManifest{}, fmt.Errorf("base PVF path is required")
|
||||
}
|
||||
copyArtifacts := append([]AICArtifact(nil), artifacts...)
|
||||
sort.Slice(copyArtifacts, func(i, j int) bool { return copyArtifacts[i].Path < copyArtifacts[j].Path })
|
||||
manifest := PVFOverlayManifest{
|
||||
Format: "dnf90-companion-pvf-overlay-v1",
|
||||
BasePVF: basePVF,
|
||||
BaseSHA256: baseSHA256,
|
||||
Artifacts: copyArtifacts,
|
||||
}
|
||||
manifest.ManifestHash = manifestHash(manifest)
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func manifestHash(manifest PVFOverlayManifest) string {
|
||||
manifest.ManifestHash = ""
|
||||
data, _ := json.Marshal(manifest)
|
||||
return sha256Hex(data)
|
||||
}
|
||||
|
||||
// ValidatePVFOverlayManifest checks both each AIC readback and the manifest
|
||||
// hash. It is suitable for a CI/readback step after an offline artifact was
|
||||
// copied to a review directory.
|
||||
func ValidatePVFOverlayManifest(manifest PVFOverlayManifest) error {
|
||||
if manifest.Format != "dnf90-companion-pvf-overlay-v1" {
|
||||
return fmt.Errorf("unsupported overlay format %q", manifest.Format)
|
||||
}
|
||||
if strings.TrimSpace(manifest.BasePVF) == "" || len(manifest.Artifacts) == 0 {
|
||||
return fmt.Errorf("overlay must contain base PVF and at least one AIC artifact")
|
||||
}
|
||||
seen := make(map[string]struct{}, len(manifest.Artifacts))
|
||||
for _, artifact := range manifest.Artifacts {
|
||||
if artifact.Path == "" {
|
||||
return fmt.Errorf("artifact path is empty")
|
||||
}
|
||||
selector := AITemplateSelector{
|
||||
Key: artifact.TemplateKey, Job: artifact.Job, ClassID: artifact.ClassID,
|
||||
Behavior: artifact.Behavior, ComboPreset: artifact.ComboPreset,
|
||||
APCCode: artifact.APCCode, AICPath: artifact.AICPath,
|
||||
BaseAICPath: artifact.AICPath,
|
||||
}
|
||||
if err := ValidateAITemplateSelector(selector); err != nil {
|
||||
return fmt.Errorf("%s: invalid template selector: %w", artifact.Path, err)
|
||||
}
|
||||
if !strings.EqualFold(artifact.Path, artifact.AICPath) {
|
||||
return fmt.Errorf("%s: artifact path must equal AIC path %s", artifact.Path, artifact.AICPath)
|
||||
}
|
||||
if _, ok := seen[artifact.Path]; ok {
|
||||
return fmt.Errorf("duplicate artifact path %q", artifact.Path)
|
||||
}
|
||||
seen[artifact.Path] = struct{}{}
|
||||
if _, err := ReadAICArtifact(artifact.AICText); err != nil {
|
||||
return fmt.Errorf("%s: %w", artifact.Path, err)
|
||||
}
|
||||
if want := sha256Hex([]byte(artifact.AICText)); artifact.SHA256 != want {
|
||||
return fmt.Errorf("%s: artifact hash mismatch", artifact.Path)
|
||||
}
|
||||
}
|
||||
if manifest.ManifestHash == "" || manifest.ManifestHash != manifestHash(manifest) {
|
||||
return fmt.Errorf("manifest hash mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sha256Hex(data []byte) string {
|
||||
sum := sha256.Sum256(data)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
Reference in new issue
Block a user