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

209 lines
6.2 KiB
Go

package rendering
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
appconfig "pvfine/config"
)
const sourceRelativePath = "config/rendering.json"
// LoadDefault loads the embedded rendering rules used at application start.
func LoadDefault() (*Engine, error) {
return Parse(appconfig.RenderingJSON)
}
// LoadFile loads and validates a rendering rule document from disk.
func LoadFile(filePath string) (*Engine, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("读取渲染规则失败: %w", err)
}
engine, err := Parse(data)
if err != nil {
return nil, err
}
return engine, nil
}
// Parse parses and validates one complete JSON rendering document.
func Parse(data []byte) (*Engine, error) {
document, err := parseDocument(data)
if err != nil {
return nil, err
}
return Compile(document)
}
// ParseDocument parses one rendering document without compiling its rules.
func parseDocument(data []byte) (Document, error) {
var document Document
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&document); err != nil {
return Document{}, fmt.Errorf("解析渲染规则失败: %w", err)
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
if err == nil {
return Document{}, fmt.Errorf("渲染规则只能包含一个 JSON 文档")
}
return Document{}, fmt.Errorf("解析渲染规则失败: %w", err)
}
return document, nil
}
// Marshal validates and formats a rendering document.
func Marshal(document Document) ([]byte, error) {
if err := Validate(document); err != nil {
return nil, err
}
data, err := json.MarshalIndent(document, "", " ")
if err != nil {
return nil, err
}
return append(data, '\n'), nil
}
// Validate checks the V1 rendering schema and all matching constraints.
func Validate(document Document) error {
problems := make([]string, 0)
if document.Version != 1 {
problems = append(problems, fmt.Sprintf("version 必须为 1,当前为 %d", document.Version))
}
seenIDs := make(map[string]bool, len(document.Rules))
for index, rule := range document.Rules {
prefix := fmt.Sprintf("rules[%d]", index)
id := strings.TrimSpace(rule.ID)
if id == "" {
problems = append(problems, prefix+".id 不能为空")
} else if seenIDs[id] {
problems = append(problems, prefix+".id 重复: "+rule.ID)
}
seenIDs[id] = true
for _, rawExtension := range rule.Match.Extensions {
extension := strings.ToLower(strings.TrimSpace(rawExtension))
if extension == "" || !strings.HasPrefix(extension, ".") || strings.ContainsAny(extension, "/\\") {
problems = append(problems, prefix+".match.extensions 必须是以点开头的文件后缀")
break
}
}
if strings.TrimSpace(rule.Match.Glob) != "" {
if err := validateGlob(rule.Match.Glob); err != nil {
problems = append(problems, prefix+".match.glob 无效: "+err.Error())
}
}
switch rule.Target.Kind {
case "file":
if len(rule.Format.NestedSections) > 0 {
problems = append(problems, prefix+".format.nestedSections 只允许用于 section 规则")
}
if strings.TrimSpace(rule.Target.Section) != "" {
problems = append(problems, prefix+".target.section 仅允许用于 section 规则")
}
if rule.Format.TokensPerLineIndex != nil {
problems = append(problems, prefix+".format.tokensPerLineIndex 只允许用于 section 规则")
}
case "section":
if strings.TrimSpace(rule.Target.Section) == "" {
problems = append(problems, prefix+".target.section 不能为空")
}
default:
problems = append(problems, prefix+".target.kind 必须是 file 或 section")
}
for _, child := range rule.Format.NestedSections {
if strings.TrimSpace(child) == "" || strings.ContainsAny(child, "[]/") {
problems = append(problems, prefix+".format.nestedSections 必须包含有效的子 Section 名称")
break
}
}
if rule.Format.Offset < 0 {
problems = append(problems, prefix+".format.offset 不能为负数")
}
if rule.Format.TokensPerLine <= 0 {
problems = append(problems, prefix+".format.tokensPerLine 必须是正数")
}
for version, width := range rule.Format.TokensPerLineByVersion {
switch version {
case "90US", "90CN", "TW", "100CN", "110US", "115US", "115CN":
default:
problems = append(problems, prefix+".format.tokensPerLineByVersion 包含无效版本 "+version)
}
if width <= 0 {
problems = append(problems, prefix+".format.tokensPerLineByVersion 的行宽必须是正数")
}
}
if rule.Format.TokensPerLineIndex != nil && *rule.Format.TokensPerLineIndex < 0 {
problems = append(problems, prefix+".format.tokensPerLineIndex 不能为负数")
}
}
if len(problems) > 0 {
return fmt.Errorf("渲染规则校验失败:\n- %s", strings.Join(problems, "\n- "))
}
return nil
}
func validateGlob(pattern string) error {
for _, segment := range strings.Split(normalizePath(pattern), "/") {
if segment == "**" {
continue
}
if _, err := path.Match(segment, segment); err != nil {
return err
}
}
return nil
}
// RuntimePath is the writable rule location used by packaged applications.
func RuntimePath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("获取用户配置目录失败: %w", err)
}
return filepath.Join(configDir, "pvfine", "rendering.json"), nil
}
// FindSourcePath locates the repository rendering file during development.
func FindSourcePath() (string, bool) {
starts := make([]string, 0, 2)
if cwd, err := os.Getwd(); err == nil {
starts = append(starts, cwd)
}
if executable, err := os.Executable(); err == nil {
starts = append(starts, filepath.Dir(executable))
}
seen := make(map[string]struct{})
for _, start := range starts {
for dir := filepath.Clean(start); ; dir = filepath.Dir(dir) {
if _, ok := seen[dir]; !ok {
seen[dir] = struct{}{}
candidate := filepath.Join(dir, filepath.FromSlash(sourceRelativePath))
if fileExists(filepath.Join(dir, "go.mod")) && fileExists(candidate) {
return candidate, true
}
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
}
}
return "", false
}
func fileExists(filePath string) bool {
info, err := os.Stat(filePath)
return err == nil && !info.IsDir()
}