服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
148 lines
4.3 KiB
Go
148 lines
4.3 KiB
Go
package mod
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type Extend110Writer struct{}
|
|
|
|
func (Extend110Writer) Files(pkg Package) []OutputFile {
|
|
files := []OutputFile{{Path: "pack.json"}}
|
|
for _, e := range pkg.Entries {
|
|
dir := "pvf"
|
|
if e.Operation != ReplaceFile {
|
|
dir = "merge"
|
|
}
|
|
files = append(files, OutputFile{Path: dir + "/" + e.Path, EntryPath: e.Path})
|
|
}
|
|
return files
|
|
}
|
|
|
|
func (Extend110Writer) Validate(pkg Package) error {
|
|
return validateTextPackage(pkg, "110USextend")
|
|
}
|
|
|
|
func validateTextPackage(pkg Package, format string) error {
|
|
if err := ValidateName(pkg.Metadata.Name); err != nil {
|
|
return err
|
|
}
|
|
if err := ValidateVersion(pkg.Metadata.Version); err != nil {
|
|
return err
|
|
}
|
|
return validateTextEntries(pkg.Entries, format)
|
|
}
|
|
|
|
func validateTextEntries(entries []Entry, format string) error {
|
|
if len(entries) == 0 {
|
|
return fmt.Errorf("没有可应用的 mod 内容")
|
|
}
|
|
seen := make(map[string]bool)
|
|
for _, e := range entries {
|
|
if err := ValidatePath(e.Path); err != nil {
|
|
return err
|
|
}
|
|
key := strings.ToLower(e.Path)
|
|
if seen[key] {
|
|
return fmt.Errorf("mod 路径冲突: %s", e.Path)
|
|
}
|
|
seen[key] = true
|
|
switch e.Operation {
|
|
case ReplaceFile:
|
|
if e.Encoding != Text || (e.DataType != 1 && e.DataType != 3) || !utf8.Valid(e.Data) || strings.HasPrefix(string(e.Data), "\ufeff") {
|
|
return fmt.Errorf("%s 不支持文件的内容类型或编码: %s", format, e.Path)
|
|
}
|
|
case MergeList, MergeStrings:
|
|
want := ".lst"
|
|
if e.Operation == MergeStrings {
|
|
want = ".str"
|
|
}
|
|
if !strings.EqualFold(filepath.Ext(e.Path), want) || len(e.Pairs) == 0 {
|
|
return fmt.Errorf("无效的合并文件: %s", e.Path)
|
|
}
|
|
keys := make(map[string]bool)
|
|
for _, pair := range e.Pairs {
|
|
if pair.Key == "" || strings.ContainsAny(pair.Key+pair.Value, "\r\n\x00") || !utf8.ValidString(pair.Key+pair.Value) || keys[pair.Key] {
|
|
return fmt.Errorf("无效或重复的合并条目: %s / %s", e.Path, pair.Key)
|
|
}
|
|
keys[pair.Key] = true
|
|
if e.Operation == MergeList {
|
|
n, err := strconv.ParseUint(pair.Key, 10, 32)
|
|
if err != nil || strconv.FormatUint(n, 10) != pair.Key || strings.Contains(pair.Value, "`") {
|
|
return fmt.Errorf("%s 列表编号或路径无法表达: %s / %s", format, e.Path, pair.Key)
|
|
}
|
|
if err := ValidatePath(pair.Value); err != nil {
|
|
return err
|
|
}
|
|
} else if strings.Contains(pair.Key, ">") || strings.HasPrefix(pair.Key, "//") || strings.HasPrefix(pair.Key, "\ufeff") {
|
|
return fmt.Errorf("%s 文字表键无法表达: %s / %s", format, e.Path, pair.Key)
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("%s 不支持操作 %s: %s", format, e.Operation, e.Path)
|
|
}
|
|
}
|
|
// Detect file/directory collisions too, including across pvf and merge.
|
|
for key := range seen {
|
|
for p := key; strings.Contains(p, "/"); {
|
|
p = p[:strings.LastIndex(p, "/")]
|
|
if seen[p] {
|
|
return fmt.Errorf("mod 文件与目录路径冲突: %s", key)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w Extend110Writer) Write(root string, pkg Package) error {
|
|
if err := w.Validate(pkg); err != nil {
|
|
return err
|
|
}
|
|
types := make(map[string]int32)
|
|
for _, e := range pkg.Entries {
|
|
dir, data := "pvf", e.Data
|
|
if e.Operation != ReplaceFile {
|
|
dir = "merge"
|
|
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())
|
|
} else {
|
|
types[e.Path] = e.DataType
|
|
}
|
|
dest := filepath.Join(root, dir, filepath.FromSlash(e.Path))
|
|
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(dest, data, 0o644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
version := pkg.Metadata.Version
|
|
if version == "" {
|
|
version = "1.0"
|
|
}
|
|
manifest := struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Priority int `json:"priority"`
|
|
Enabled bool `json:"enabled"`
|
|
Types map[string]int32 `json:"types"`
|
|
}{pkg.Metadata.Name, version, 100, true, types}
|
|
data, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(filepath.Join(root, "pack.json"), append(data, '\n'), 0o644)
|
|
}
|