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,515 @@
|
||||
package datatable
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/csv"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errXLSXPartNotFound = errors.New("xlsx part not found")
|
||||
|
||||
type GenerateOptions struct {
|
||||
Input string `json:"input"`
|
||||
Output string `json:"output"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
ManifestOut string `json:"manifest_out,omitempty"`
|
||||
Pretty bool `json:"pretty,omitempty"`
|
||||
}
|
||||
|
||||
type GeneratedTable struct {
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
Output string `json:"output"`
|
||||
Rows int `json:"rows"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
type GenerateReport struct {
|
||||
Input string `json:"input"`
|
||||
Output string `json:"output"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Tables []GeneratedTable `json:"tables"`
|
||||
Manifest Manifest `json:"manifest"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
}
|
||||
|
||||
func GenerateTables(ctx context.Context, options GenerateOptions) (GenerateReport, error) {
|
||||
if err := contextErr(ctx); err != nil {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
options.Input = strings.TrimSpace(options.Input)
|
||||
options.Output = strings.TrimSpace(options.Output)
|
||||
options.Format = strings.ToLower(strings.TrimSpace(options.Format))
|
||||
options.Version = strings.TrimSpace(options.Version)
|
||||
options.ManifestOut = strings.TrimSpace(options.ManifestOut)
|
||||
if options.Input == "" {
|
||||
return GenerateReport{}, ErrDirectoryRequired
|
||||
}
|
||||
if options.Output == "" {
|
||||
return GenerateReport{}, fmt.Errorf("data table output is required")
|
||||
}
|
||||
info, err := os.Stat(options.Input)
|
||||
if err != nil {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
sources, err := generatorSources(options.Input, info)
|
||||
if err != nil {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
if len(sources) == 0 {
|
||||
return GenerateReport{}, fmt.Errorf("no data table source files found in %s", options.Input)
|
||||
}
|
||||
if err := os.MkdirAll(options.Output, 0o750); err != nil {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
report := GenerateReport{
|
||||
Input: options.Input,
|
||||
Output: options.Output,
|
||||
Version: options.Version,
|
||||
GeneratedAt: time.Now().UTC(),
|
||||
}
|
||||
tableSources := make(map[string]string, len(sources))
|
||||
for _, source := range sources {
|
||||
if err := contextErr(ctx); err != nil {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
tableName := tableNameFromFile(source)
|
||||
tableKey := strings.ToLower(tableName)
|
||||
if previous, ok := tableSources[tableKey]; ok {
|
||||
return GenerateReport{}, fmt.Errorf("duplicate generated data table name %q from %s and %s", tableName, previous, source)
|
||||
}
|
||||
tableSources[tableKey] = source
|
||||
rows, err := loadGeneratedRows(source, options.Format)
|
||||
if err != nil {
|
||||
return GenerateReport{}, fmt.Errorf("generate table %s: %w", source, err)
|
||||
}
|
||||
data, err := marshalGeneratedRows(rows, options.Pretty)
|
||||
if err != nil {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
outPath := filepath.Join(options.Output, tableName+".json")
|
||||
if err := os.WriteFile(outPath, data, 0o600); err != nil {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
checksum := sha256.Sum256(data)
|
||||
report.Tables = append(report.Tables, GeneratedTable{
|
||||
Name: tableName,
|
||||
Source: source,
|
||||
Output: outPath,
|
||||
Rows: len(rows),
|
||||
Bytes: int64(len(data)),
|
||||
Checksum: hex.EncodeToString(checksum[:]),
|
||||
})
|
||||
}
|
||||
sort.Slice(report.Tables, func(i, j int) bool { return report.Tables[i].Name < report.Tables[j].Name })
|
||||
registry := NewRegistry(options.Output, options.Version)
|
||||
if err := registry.Load(ctx); err != nil {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
report.Manifest = registry.Manifest()
|
||||
if options.ManifestOut != "" {
|
||||
manifestData, err := json.MarshalIndent(report.Manifest, "", " ")
|
||||
if err != nil {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(options.ManifestOut), 0o750); err != nil && filepath.Dir(options.ManifestOut) != "." {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
if err := os.WriteFile(options.ManifestOut, append(manifestData, '\n'), 0o600); err != nil {
|
||||
return GenerateReport{}, err
|
||||
}
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func generatorSources(input string, info os.FileInfo) ([]string, error) {
|
||||
if !info.IsDir() {
|
||||
if genExtSupported(input) {
|
||||
return []string{input}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported data table source %s", input)
|
||||
}
|
||||
var sources []string
|
||||
if err := filepath.WalkDir(input, func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(filepath.Base(path), ".") {
|
||||
return nil
|
||||
}
|
||||
if genExtSupported(path) {
|
||||
sources = append(sources, path)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(sources)
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func genExtSupported(path string) bool {
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".json", ".csv", ".xlsx":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func loadGeneratedRows(path, format string) ([]map[string]any, error) {
|
||||
if format == "" || format == "auto" {
|
||||
format = strings.TrimPrefix(strings.ToLower(filepath.Ext(path)), ".")
|
||||
}
|
||||
switch format {
|
||||
case "json":
|
||||
return loadGeneratedJSON(path)
|
||||
case "csv":
|
||||
return loadGeneratedCSV(path)
|
||||
case "xlsx":
|
||||
return loadGeneratedXLSX(path)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported data table format %q", format)
|
||||
}
|
||||
}
|
||||
|
||||
func loadGeneratedJSON(path string) ([]map[string]any, error) {
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304:路径来自框架配置、仓库扫描或测试临时目录,调用点负责限定输入范围。
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []map[string]any
|
||||
if err := json.Unmarshal(data, &rows); err == nil {
|
||||
return rows, nil
|
||||
}
|
||||
var wrapped struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &wrapped); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wrapped.Items == nil {
|
||||
return nil, fmt.Errorf("json table must be an array or object with items")
|
||||
}
|
||||
return wrapped.Items, nil
|
||||
}
|
||||
|
||||
func loadGeneratedCSV(path string) ([]map[string]any, error) {
|
||||
file, err := os.Open(path) //nolint:gosec // G304:路径来自框架配置、仓库扫描或测试临时目录,调用点负责限定输入范围。
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
reader := csv.NewReader(file)
|
||||
reader.TrimLeadingSpace = true
|
||||
reader.FieldsPerRecord = -1
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return recordsToRows(records)
|
||||
}
|
||||
|
||||
func loadGeneratedXLSX(path string) ([]map[string]any, error) {
|
||||
reader, err := zip.OpenReader(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
shared, err := readXLSXStrings(&reader.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sheet, err := readZipFile(&reader.Reader, "xl/worksheets/sheet1.xml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records, err := parseXLSXSheet(sheet, shared)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return recordsToRows(records)
|
||||
}
|
||||
|
||||
func recordsToRows(records [][]string) ([]map[string]any, error) {
|
||||
if len(records) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
headers := normalizeHeaders(records[0])
|
||||
if len(headers) == 0 {
|
||||
return nil, fmt.Errorf("data table header row is empty")
|
||||
}
|
||||
rows := make([]map[string]any, 0, len(records)-1)
|
||||
for _, record := range records[1:] {
|
||||
if emptyRecord(record) {
|
||||
continue
|
||||
}
|
||||
row := make(map[string]any, len(headers))
|
||||
for idx, header := range headers {
|
||||
value := ""
|
||||
if idx < len(record) {
|
||||
value = strings.TrimSpace(record[idx])
|
||||
}
|
||||
row[header] = inferGeneratedValue(value)
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func normalizeHeaders(headers []string) []string {
|
||||
out := make([]string, 0, len(headers))
|
||||
seen := make(map[string]struct{}, len(headers))
|
||||
for idx, header := range headers {
|
||||
header = strings.TrimSpace(header)
|
||||
if header == "" {
|
||||
header = fmt.Sprintf("col_%d", idx+1)
|
||||
}
|
||||
base := header
|
||||
for suffix := 2; ; suffix++ {
|
||||
if _, ok := seen[header]; !ok {
|
||||
break
|
||||
}
|
||||
header = fmt.Sprintf("%s_%d", base, suffix)
|
||||
}
|
||||
seen[header] = struct{}{}
|
||||
out = append(out, header)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func emptyRecord(record []string) bool {
|
||||
for _, value := range record {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func inferGeneratedValue(value string) any {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
switch strings.ToLower(value) {
|
||||
case "true", "false":
|
||||
parsed, _ := strconv.ParseBool(strings.ToLower(value))
|
||||
return parsed
|
||||
}
|
||||
if parsed, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
return parsed
|
||||
}
|
||||
if parsed, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return parsed
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func marshalGeneratedRows(rows []map[string]any, pretty bool) ([]byte, error) {
|
||||
var data []byte
|
||||
var err error
|
||||
if pretty {
|
||||
data, err = json.MarshalIndent(rows, "", " ")
|
||||
} else {
|
||||
data, err = json.Marshal(rows)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(data, '\n'), nil
|
||||
}
|
||||
|
||||
func tableNameFromFile(path string) string {
|
||||
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return "table"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func readZipFile(reader *zip.Reader, name string) ([]byte, error) {
|
||||
for _, file := range reader.File {
|
||||
if filepath.ToSlash(file.Name) != name {
|
||||
continue
|
||||
}
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = rc.Close() }()
|
||||
return io.ReadAll(rc)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", errXLSXPartNotFound, name)
|
||||
}
|
||||
|
||||
func readXLSXStrings(reader *zip.Reader) ([]string, error) {
|
||||
data, err := readZipFile(reader, "xl/sharedStrings.xml")
|
||||
if err != nil {
|
||||
if errors.Is(err, errXLSXPartNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
decoder := xml.NewDecoder(bytes.NewReader(data))
|
||||
var values []string
|
||||
var current strings.Builder
|
||||
inText := false
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch t := token.(type) {
|
||||
case xml.StartElement:
|
||||
if t.Name.Local == "si" {
|
||||
current.Reset()
|
||||
}
|
||||
if t.Name.Local == "t" {
|
||||
inText = true
|
||||
}
|
||||
case xml.EndElement:
|
||||
if t.Name.Local == "t" {
|
||||
inText = false
|
||||
}
|
||||
if t.Name.Local == "si" {
|
||||
values = append(values, current.String())
|
||||
}
|
||||
case xml.CharData:
|
||||
if inText {
|
||||
current.Write([]byte(t))
|
||||
}
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func parseXLSXSheet(data []byte, shared []string) ([][]string, error) {
|
||||
decoder := xml.NewDecoder(bytes.NewReader(data))
|
||||
var records [][]string
|
||||
var row []string
|
||||
var cellRef string
|
||||
var cellType string
|
||||
var value strings.Builder
|
||||
inValue := false
|
||||
inInlineText := false
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch t := token.(type) {
|
||||
case xml.StartElement:
|
||||
switch t.Name.Local {
|
||||
case "row":
|
||||
row = nil
|
||||
case "c":
|
||||
cellRef = ""
|
||||
cellType = ""
|
||||
value.Reset()
|
||||
for _, attr := range t.Attr {
|
||||
switch attr.Name.Local {
|
||||
case "r":
|
||||
cellRef = attr.Value
|
||||
case "t":
|
||||
cellType = attr.Value
|
||||
}
|
||||
}
|
||||
case "v":
|
||||
inValue = true
|
||||
case "t":
|
||||
if cellType == "inlineStr" {
|
||||
inInlineText = true
|
||||
}
|
||||
}
|
||||
case xml.EndElement:
|
||||
switch t.Name.Local {
|
||||
case "v":
|
||||
inValue = false
|
||||
case "t":
|
||||
inInlineText = false
|
||||
case "c":
|
||||
text := decodeXLSXCell(value.String(), cellType, shared)
|
||||
idx := xlsxCellIndex(cellRef)
|
||||
if idx < 0 {
|
||||
idx = len(row)
|
||||
}
|
||||
for len(row) <= idx {
|
||||
row = append(row, "")
|
||||
}
|
||||
row[idx] = text
|
||||
case "row":
|
||||
records = append(records, append([]string(nil), row...))
|
||||
}
|
||||
case xml.CharData:
|
||||
if inValue || inInlineText {
|
||||
value.Write([]byte(t))
|
||||
}
|
||||
}
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func decodeXLSXCell(raw, cellType string, shared []string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if cellType != "s" {
|
||||
return raw
|
||||
}
|
||||
idx, err := strconv.Atoi(raw)
|
||||
if err != nil || idx < 0 || idx >= len(shared) {
|
||||
return raw
|
||||
}
|
||||
return shared[idx]
|
||||
}
|
||||
|
||||
func xlsxCellIndex(ref string) int {
|
||||
ref = strings.TrimSpace(ref)
|
||||
if ref == "" {
|
||||
return -1
|
||||
}
|
||||
idx := 0
|
||||
found := false
|
||||
for _, r := range ref {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
idx = idx*26 + int(r-'A'+1)
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
if r >= 'a' && r <= 'z' {
|
||||
idx = idx*26 + int(r-'a'+1)
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
return -1
|
||||
}
|
||||
return idx - 1
|
||||
}
|
||||
Reference in new issue
Block a user