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

+453
View File
@@ -0,0 +1,453 @@
package mod
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"unicode/utf8"
)
const CN115Format = "115CN-mod"
var modIDPattern = regexp.MustCompile(`^[a-z0-9_-]{1,64}$`)
var resourceHashPattern = regexp.MustCompile(`^[a-f0-9]{64}$`)
func ValidateID(id string) error {
if !modIDPattern.MatchString(id) {
return fmt.Errorf("MOD 和组件 ID 仅支持小写字母、数字、-、_,长度为 1–64")
}
return nil
}
type cn115Manifest struct {
Schema int `json:"schema"`
Client string `json:"client"`
Kind string `json:"kind"`
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Author string `json:"author"`
Description string `json:"description"`
Components []Component `json:"components"`
Requires []string `json:"requires,omitempty"`
Conflicts []string `json:"conflicts,omitempty"`
Files []cn115Resource `json:"files"`
PVF []cn115Operation `json:"pvf"`
}
type cn115Resource struct {
Component string `json:"component"`
Source string `json:"source"`
Target string `json:"target"`
Size *int64 `json:"size"`
SHA256 string `json:"sha256"`
}
type cn115Operation struct {
Component string `json:"component"`
Source string `json:"source"`
Path string `json:"path"`
Mode string `json:"mode"`
Type int32 `json:"type,omitempty"`
}
type CN115Reader struct{}
type CN115Writer struct{}
func cn115Root(root fs.FS) (fs.FS, error) {
entries, err := fs.ReadDir(root, ".")
if err != nil {
return nil, err
}
for _, e := range entries {
if e.Name() == "mod.json" {
if !e.Type().IsRegular() {
return nil, fmt.Errorf("mod.json 必须是普通文件")
}
return root, nil
}
}
if len(entries) == 1 && entries[0].IsDir() {
sub, err := fs.Sub(root, entries[0].Name())
if err == nil {
nested, readErr := fs.ReadDir(sub, ".")
if readErr != nil {
return nil, readErr
}
for _, e := range nested {
if e.Name() == "mod.json" && e.Type().IsRegular() {
return sub, nil
}
}
}
}
return nil, fmt.Errorf("根目录或唯一的一层包目录内缺少 mod.json")
}
func (CN115Reader) Detect(root fs.FS) (bool, error) {
_, err := cn115Root(root)
if err != nil {
return false, err
}
return true, nil
}
func (CN115Reader) Read(root fs.FS) (Package, error) {
var pkg Package
root, err := cn115Root(root)
if err != nil {
return pkg, err
}
data, err := readPackageFile(root, "mod.json", 4<<20)
if err != nil {
return pkg, err
}
var manifest cn115Manifest
if !utf8.Valid(data) {
return pkg, fmt.Errorf("mod.json 必须使用 UTF-8")
}
if err := json.Unmarshal(data, &manifest); err != nil {
return pkg, fmt.Errorf("解析 mod.json: %w", err)
}
if manifest.Schema != 1 || manifest.Client != "115CN" || manifest.Kind != "resource" {
return pkg, fmt.Errorf("只支持 schema=1、client=115CN、kind=resource 的 MOD")
}
if err := ValidateID(manifest.ID); err != nil {
return pkg, err
}
if strings.TrimSpace(manifest.Name) == "" {
return pkg, fmt.Errorf("MOD 名称不能为空")
}
if err := ValidateVersion(manifest.Version); err != nil {
return pkg, err
}
pkg.Metadata = Metadata{ID: manifest.ID, Name: manifest.Name, Version: manifest.Version}
pkg.Components = manifest.Components
if err := validateComponents(pkg.Components); err != nil {
return pkg, err
}
components := map[string]bool{}
for _, c := range pkg.Components {
components[c.ID] = true
}
for _, id := range append(append([]string{}, manifest.Requires...), manifest.Conflicts...) {
if err := ValidateID(id); err != nil {
return pkg, err
}
if id == manifest.ID {
return pkg, fmt.Errorf("MOD 不能依赖自身或与自身冲突")
}
}
if len(manifest.Requires)+len(manifest.Conflicts) > 0 {
pkg.Warnings = append(pkg.Warnings, "包级依赖/冲突需在登录器中核对;本工具仅将所选 PVF 内容导入当前归档,不管理 MOD 安装状态")
}
for _, r := range manifest.Files {
if !components[r.Component] || ValidatePath(r.Source) != nil || protectedPackagePath(r.Source) || !validResourceTarget(r.Target) || r.Size == nil || *r.Size < 0 || !resourceHashPattern.MatchString(r.SHA256) {
return pkg, fmt.Errorf("无效的资源声明: %s", r.Target)
}
}
if len(manifest.Files) > 0 {
pkg.Warnings = append(pkg.Warnings, fmt.Sprintf("包内 %d 个 NPK/BK2 客户端资源不导入 PVF,请由登录器安装、关闭或恢复", len(manifest.Files)))
}
var total int
for _, op := range manifest.PVF {
if !components[op.Component] || ValidatePath(op.Path) != nil || op.Path != strings.ToLower(op.Path) || protectedPackagePath(op.Path) {
return pkg, fmt.Errorf("无效的 PVF 路径或组件: %s", op.Path)
}
if op.Mode != "add" && op.Mode != "replace" && op.Mode != "merge" {
return pkg, fmt.Errorf("不支持的 115CN PVF 操作 %q: %s;patch 标签补丁需完整协议,当前不执行", op.Mode, op.Path)
}
content, err := readPackageFile(root, op.Source, maxPayloadSize)
if err != nil {
return pkg, err
}
total += len(content)
if total > maxPackageSize {
return pkg, fmt.Errorf("MOD PVF 内容大小超出限制")
}
if !utf8.Valid(content) || strings.HasPrefix(string(content), "\ufeff") {
return pkg, fmt.Errorf("PVF 内容必须使用无 BOM 的 UTF-8: %s", op.Source)
}
e := Entry{Component: op.Component, Path: op.Path, Operation: ReplaceFile, Encoding: Text, DataType: op.Type, Data: content}
if e.DataType == 0 {
e.DataType = 1
if ext := path.Ext(op.Path); ext == ".str" || ext == ".nut" {
e.DataType = 3
}
}
if e.DataType != 1 && e.DataType != 3 {
return pkg, fmt.Errorf("不支持的 PVF 类型: %s", op.Path)
}
switch op.Mode {
case "add":
e.Operation = AddFile
case "merge":
switch path.Ext(op.Path) {
case ".lst":
e.Operation = MergeList
if e.DataType != 1 {
return pkg, fmt.Errorf("lst 合并类型必须为 1: %s", op.Path)
}
case ".str":
e.Operation = MergeStrings
if e.DataType != 3 {
return pkg, fmt.Errorf("str 合并类型必须为 3: %s", op.Path)
}
default:
return pkg, fmt.Errorf("merge 只支持 lst/str: %s", op.Path)
}
e.Pairs, err = ParseMergeText(string(content), e.Operation)
if err != nil {
return pkg, fmt.Errorf("%s: %w", op.Source, err)
}
e.Data = nil
}
checked := e
if checked.Operation == AddFile {
checked.Operation = ReplaceFile
}
if err := validateTextEntries([]Entry{checked}, CN115Format); err != nil {
return pkg, err
}
pkg.Entries = append(pkg.Entries, e)
}
if len(manifest.PVF)+len(manifest.Files) == 0 {
return pkg, fmt.Errorf("MOD 没有 PVF 操作或资源")
}
return pkg, nil
}
func validResourceTarget(target string) bool {
if ValidatePath(target) != nil || protectedPackagePath(target) {
return false
}
p := strings.ToLower(target)
if strings.HasPrefix(p, "imagepacks2/") || strings.HasPrefix(p, "soundpacks/") {
return strings.Count(p, "/") == 1 && path.Ext(p) == ".npk"
}
return (strings.HasPrefix(p, "video/character/") || strings.HasPrefix(p, "stream/video/")) && path.Ext(p) == ".bk2"
}
func validateComponents(components []Component) error {
if len(components) == 0 {
return fmt.Errorf("MOD 必须声明组件")
}
byID := map[string]Component{}
for _, c := range components {
if ValidateID(c.ID) != nil || c.Title == "" {
return fmt.Errorf("无效的组件 ID 或标题: %s", c.ID)
}
if _, exists := byID[c.ID]; exists {
return fmt.Errorf("重复的组件 ID: %s", c.ID)
}
byID[c.ID] = c
}
state := map[string]int{}
var visit func(string) error
visit = func(id string) error {
if state[id] == 1 {
return fmt.Errorf("组件存在循环依赖: %s", id)
}
if state[id] == 2 {
return nil
}
state[id] = 1
for _, dep := range byID[id].Requires {
if _, exists := byID[dep]; !exists {
return fmt.Errorf("组件 %s 依赖不存在的组件 %s", id, dep)
}
if err := visit(dep); err != nil {
return err
}
}
state[id] = 2
return nil
}
for id := range byID {
if err := visit(id); err != nil {
return err
}
}
return nil
}
// SelectComponents requires an explicit, valid selection. nil uses defaults;
// an empty non-nil list means no components, not "select everything".
func SelectComponents(pkg Package, ids []string) (Package, []string, error) {
if err := validateComponents(pkg.Components); err != nil {
return Package{}, nil, err
}
known := map[string]Component{}
for _, c := range pkg.Components {
known[c.ID] = c
}
if ids == nil {
ids = []string{}
for _, c := range pkg.Components {
if c.DefaultSelected {
ids = append(ids, c.ID)
}
}
}
selected := map[string]bool{}
groups := map[string]string{}
for _, id := range ids {
c, exists := known[id]
if !exists || selected[id] {
return Package{}, nil, fmt.Errorf("未知或重复的组件: %s", id)
}
selected[id] = true
if c.ExclusiveGroup != "" {
if previous := groups[c.ExclusiveGroup]; previous != "" {
return Package{}, nil, fmt.Errorf("互斥组件不能同时选择: %s / %s", previous, id)
}
groups[c.ExclusiveGroup] = id
}
}
for _, id := range ids {
for _, dep := range known[id].Requires {
if !selected[dep] {
return Package{}, nil, fmt.Errorf("组件 %s 需要同时选择 %s", id, dep)
}
}
}
result := pkg
result.Entries = nil
seen := map[string]bool{}
for _, e := range pkg.Entries {
if !selected[e.Component] {
continue
}
if seen[e.Path] {
return Package{}, nil, fmt.Errorf("所选组件存在重复 PVF 目标: %s", e.Path)
}
seen[e.Path] = true
result.Entries = append(result.Entries, e)
}
for p := range seen {
for parent := path.Dir(p); parent != "."; parent = path.Dir(parent) {
if seen[parent] {
return Package{}, nil, fmt.Errorf("PVF 文件与目录路径冲突: %s", p)
}
}
}
return result, append([]string{}, ids...), nil
}
func (CN115Writer) Validate(pkg Package) error {
if err := ValidateID(pkg.Metadata.ID); err != nil {
return err
}
plain := pkg
plain.Entries = append([]Entry(nil), pkg.Entries...)
for i := range plain.Entries {
e := &plain.Entries[i]
if e.Path != strings.ToLower(e.Path) || protectedPackagePath(e.Path) {
return fmt.Errorf("115CN PVF 路径必须为小写且不能是敏感路径: %s", e.Path)
}
if e.Operation == AddFile {
e.Operation = ReplaceFile
}
}
return validateTextPackage(plain, CN115Format)
}
func (CN115Writer) Files(pkg Package) []OutputFile {
files := []OutputFile{{Path: "mod.json"}}
for _, e := range pkg.Entries {
files = append(files, OutputFile{Path: "pvf/" + e.Path, EntryPath: e.Path})
}
return files
}
func cn115Output(pkg Package) (cn115Manifest, map[string][]byte) {
version := pkg.Metadata.Version
if version == "" {
version = "1.0.0"
}
m := cn115Manifest{Schema: 1, Client: "115CN", Kind: "resource", ID: pkg.Metadata.ID, Name: pkg.Metadata.Name, Version: version,
Components: []Component{{ID: "base", Title: "基础内容", DefaultSelected: true}}, Files: []cn115Resource{}, PVF: []cn115Operation{}}
payloads := map[string][]byte{}
for _, e := range pkg.Entries {
mode := "replace"
data := e.Data
dataType := e.DataType
switch e.Operation {
case AddFile:
mode = "add"
case MergeList, MergeStrings:
mode = "merge"
dataType = 1
if e.Operation == MergeStrings {
dataType = 3
}
var text strings.Builder
for _, pair := range e.Pairs {
if e.Operation == MergeList {
fmt.Fprintf(&text, "%s\t`%s`\n", pair.Key, pair.Value)
} else {
fmt.Fprintf(&text, "%s>%s\n", pair.Key, pair.Value)
}
}
data = []byte(text.String())
}
source := "pvf/" + e.Path
m.PVF = append(m.PVF, cn115Operation{Component: "base", Source: source, Path: e.Path, Mode: mode, Type: dataType})
payloads[source] = data
}
return m, payloads
}
func (w CN115Writer) Write(root string, pkg Package) error {
if err := w.Validate(pkg); err != nil {
return err
}
m, payloads := cn115Output(pkg)
manifest, err := json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
payloads["mod.json"] = append(manifest, '\n')
for _, file := range w.Files(pkg) {
dest := filepath.Join(root, filepath.FromSlash(file.Path))
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return err
}
if err := os.WriteFile(dest, payloads[file.Path], 0644); err != nil {
return err
}
}
return nil
}
func (w CN115Writer) WriteArchive(output io.Writer, pkg Package) error {
if err := w.Validate(pkg); err != nil {
return err
}
m, payloads := cn115Output(pkg)
manifest, err := json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
payloads["mod.json"] = append(manifest, '\n')
z := zip.NewWriter(output)
for _, file := range w.Files(pkg) {
f, err := z.Create(file.Path)
if err != nil {
z.Close()
return err
}
if _, err = f.Write(payloads[file.Path]); err != nil {
z.Close()
return err
}
}
return z.Close()
}