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

454 lines
13 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 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()
}