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,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()
|
||||
}
|
||||
Reference in new issue
Block a user