Files
90CNServer/server/cmd/scratch/itemexpiryaudit/main.go
T
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

180 lines
5.4 KiB
Go

package main
import (
"context"
"database/sql"
"encoding/binary"
"encoding/json"
"flag"
"fmt"
"net"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/go-sql-driver/mysql"
)
const (
itemWireSize = 0x77
itemExpireOffset = 0x38
legacyExpireOffset = 0x2B
defaultTargetItemID = int64(10004024)
)
var safeIdentifier = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
type instanceConfig struct {
Database struct {
Host string `json:"host"`
Port int `json:"port"`
Name string `json:"name"`
User string `json:"user"`
Password string `json:"password"`
} `json:"database"`
}
type tableAudit struct {
Table string `json:"table"`
Rows int64 `json:"rows"`
RowsWithExpireAt int64 `json:"rows_with_expire_at"`
TargetRows int64 `json:"target_rows"`
TargetRowsWithExpire int64 `json:"target_rows_with_expire_at"`
TargetRowsWithRawTime int64 `json:"target_rows_with_raw_expire"`
}
type auditReport struct {
Database string `json:"database"`
TargetItemID int64 `json:"target_item_id"`
Tables []tableAudit `json:"tables"`
}
func main() {
instancePath := flag.String("instance", `D:/DNF/runtime/config/instance.json`, "runtime instance config")
targetItemID := flag.Int64("item", defaultTargetItemID, "item ID to inspect without mutation")
flag.Parse()
result, err := audit(*instancePath, *targetItemID)
if err != nil {
fmt.Fprintln(os.Stderr, "itemexpiryaudit:", err)
os.Exit(1)
}
encoded, err := json.MarshalIndent(result, "", " ")
if err != nil {
fmt.Fprintln(os.Stderr, "itemexpiryaudit: encode report:", err)
os.Exit(1)
}
fmt.Println(string(encoded))
}
func audit(instancePath string, targetItemID int64) (auditReport, error) {
var result auditReport
data, err := os.ReadFile(instancePath)
if err != nil {
return result, fmt.Errorf("read instance config: %w", err)
}
var instance instanceConfig
if err := json.Unmarshal(data, &instance); err != nil {
return result, fmt.Errorf("decode instance config: %w", err)
}
if !safeIdentifier.MatchString(instance.Database.Name) {
return result, fmt.Errorf("unsafe database name %q", instance.Database.Name)
}
configuration := mysql.NewConfig()
configuration.User = instance.Database.User
configuration.Passwd = instance.Database.Password
configuration.Net = "tcp"
configuration.Addr = net.JoinHostPort(instance.Database.Host, strconv.Itoa(instance.Database.Port))
configuration.DBName = instance.Database.Name
configuration.ParseTime = true
configuration.Loc = time.UTC
database, err := sql.Open("mysql", configuration.FormatDSN())
if err != nil {
return result, fmt.Errorf("open database: %w", err)
}
defer database.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := database.PingContext(ctx); err != nil {
return result, fmt.Errorf("ping database: %w", err)
}
result.Database = instance.Database.Name
result.TargetItemID = targetItemID
tables, err := expirationTables(ctx, database, instance.Database.Name)
if err != nil {
return result, err
}
for _, table := range tables {
row, err := auditTable(ctx, database, table, targetItemID)
if err != nil {
return result, err
}
result.Tables = append(result.Tables, row)
}
return result, nil
}
func expirationTables(ctx context.Context, database *sql.DB, databaseName string) ([]string, error) {
rows, err := database.QueryContext(ctx, `SELECT table_name FROM information_schema.tables WHERE table_schema = ? ORDER BY table_name`, databaseName)
if err != nil {
return nil, fmt.Errorf("list database tables: %w", err)
}
defer rows.Close()
wantedSuffixes := []string{"account_inventory_items", "inventory_items", "equipment_entries", "mail_attachments"}
var tables []string
for rows.Next() {
var table string
if err := rows.Scan(&table); err != nil {
return nil, err
}
if !safeIdentifier.MatchString(table) {
return nil, fmt.Errorf("unsafe table name %q", table)
}
for _, suffix := range wantedSuffixes {
if table == suffix || strings.HasSuffix(table, "_"+suffix) {
tables = append(tables, table)
break
}
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return tables, nil
}
func auditTable(ctx context.Context, database *sql.DB, table string, targetItemID int64) (tableAudit, error) {
result := tableAudit{Table: table}
query := "SELECT COUNT(*), COALESCE(SUM(expire_at IS NOT NULL), 0), COALESCE(SUM(item_id = ?), 0), COALESCE(SUM(item_id = ? AND expire_at IS NOT NULL), 0) FROM `" + table + "`"
if err := database.QueryRowContext(ctx, query, targetItemID, targetItemID).Scan(
&result.Rows,
&result.RowsWithExpireAt,
&result.TargetRows,
&result.TargetRowsWithExpire,
); err != nil {
return result, fmt.Errorf("audit table %s: %w", table, err)
}
rows, err := database.QueryContext(ctx, "SELECT raw_entry FROM `"+table+"` WHERE item_id = ?", targetItemID)
if err != nil {
return result, fmt.Errorf("audit target raw rows in %s: %w", table, err)
}
defer rows.Close()
for rows.Next() {
var raw []byte
if err := rows.Scan(&raw); err != nil {
return result, err
}
if len(raw) == itemWireSize &&
(binary.LittleEndian.Uint32(raw[itemExpireOffset:itemExpireOffset+4]) != 0 ||
binary.LittleEndian.Uint32(raw[legacyExpireOffset:legacyExpireOffset+4]) != 0) {
result.TargetRowsWithRawTime++
}
}
if err := rows.Err(); err != nil {
return result, err
}
return result, nil
}