Files
90CNServer/server/cmd/scratch/emblemrowcensus/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

248 lines
6.7 KiB
Go
Raw 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.
// emblemrowcensus 只读统计:每个角色在徽章页 289..344 里有多少行、空格落在哪几段;
// 以及 321..344 这"最后一整行之后"的格子到底有没有被用过。
//
// 现场 2026-10-03(服主报):服主说徽章栏总共 63 格,而服务端把落点夹到 289..320
// (main_slot_count=0 时),于是 321..344 永远不放东西。要判断这些格子到底能不能用,
// 就看真实库里**已经**有没有行落在 321..344 —— 有,就说明服务端白留着这 24 格。
//
// 只读:只 SELECT。
//
// 跑法:
//
// go run ./cmd/scratch/emblemrowcensus
package main
import (
"context"
"database/sql"
"flag"
"fmt"
"os"
"sort"
"strconv"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
pageStart := flag.Int("page-start", 289, "emblem page first slot")
pageEnd := flag.Int("page-end", 344, "emblem page last slot")
probeStart := flag.Int("probe-start", 321, "the range the server never places into")
dsn := flag.String("dsn", "", "mysql dsn (default: the deployed runtime logic.toml)")
flag.Parse()
resolvedDSN := *dsn
var err error
if resolvedDSN == "" {
resolvedDSN, err = dsnFromRuntimeConfig()
if err != nil {
fmt.Fprintln(os.Stderr, "FAILED:", err)
os.Exit(2)
}
}
db, err := sql.Open("mysql", resolvedDSN)
if err != nil {
fmt.Fprintln(os.Stderr, "FAILED:", err)
os.Exit(1)
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
fmt.Fprintln(os.Stderr, "FAILED: mysql unreachable:", err)
os.Exit(1)
}
schema, table, err := locateInventoryTable(ctx, db)
if err != nil {
fmt.Fprintln(os.Stderr, "FAILED:", err)
os.Exit(1)
}
// 逐角色统计这一页。entry_key 形如 "0:<slot>"。
rows, err := db.QueryContext(ctx,
"SELECT character_id, entry_key, item_id, item_count FROM `"+schema+"`.`"+table+"` ORDER BY character_id")
if err != nil {
fmt.Fprintln(os.Stderr, "FAILED: query:", err)
os.Exit(1)
}
defer rows.Close()
type pageState struct {
slots map[int]int64
}
byCharacter := map[string]*pageState{}
probeHits := map[string][]int{}
probeTotals := 0
for rows.Next() {
var characterID, key string
var itemID, count sql.NullInt64
if err := rows.Scan(&characterID, &key, &itemID, &count); err != nil {
fmt.Fprintln(os.Stderr, "FAILED: scan:", err)
os.Exit(1)
}
listType, slot, ok := parseEntryKey(key)
if !ok || listType != 0 {
continue
}
if slot >= *probeStart && slot <= *pageEnd {
probeHits[characterID] = append(probeHits[characterID], slot)
probeTotals++
}
if slot < *pageStart || slot > *pageEnd {
continue
}
state := byCharacter[characterID]
if state == nil {
state = &pageState{slots: map[int]int64{}}
byCharacter[characterID] = state
}
state.slots[slot] = count.Int64
}
if err := rows.Err(); err != nil {
fmt.Fprintln(os.Stderr, "FAILED: rows:", err)
os.Exit(1)
}
ids := make([]string, 0, len(byCharacter))
for id := range byCharacter {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool {
left, _ := strconv.Atoi(ids[i])
right, _ := strconv.Atoi(ids[j])
return left < right
})
fmt.Printf("徽章页 %d..%d 逐角色统计(只列有行的角色):\n", *pageStart, *pageEnd)
for _, id := range ids {
state := byCharacter[id]
used := make([]int, 0, len(state.slots))
for slot := range state.slots {
used = append(used, slot)
}
sort.Ints(used)
free := make([]int, 0, *pageEnd-*pageStart+1)
for slot := *pageStart; slot <= *pageEnd; slot++ {
if _, occupied := state.slots[slot]; !occupied {
free = append(free, slot)
}
}
lastUsed := 0
if len(used) > 0 {
lastUsed = used[len(used)-1]
}
fmt.Printf(" char %-6s 行=%-3d 空格=%-3d 最大已用格=%d 空格段=%s\n",
id, len(used), len(free), lastUsed, compact(free))
}
fmt.Printf("\n== %d..%d 段(服务端在 main_slot_count=0 时从不放置的区间)==\n", *probeStart, *pageEnd)
if probeTotals == 0 {
fmt.Printf("整库没有任何一行落在这里。\n")
} else {
fmt.Printf("整库共 %d 行落在这里,涉及 %d 个角色:\n", probeTotals, len(probeHits))
probeIDs := make([]string, 0, len(probeHits))
for id := range probeHits {
probeIDs = append(probeIDs, id)
}
sort.Slice(probeIDs, func(i, j int) bool {
left, _ := strconv.Atoi(probeIDs[i])
right, _ := strconv.Atoi(probeIDs[j])
return left < right
})
for _, id := range probeIDs {
slots := probeHits[id]
sort.Ints(slots)
fmt.Printf(" char %-6s %d 行:%s\n", id, len(slots), compact(slots))
}
}
}
func parseEntryKey(key string) (int, int, bool) {
listPart, slotPart, found := strings.Cut(strings.TrimSpace(key), ":")
if !found {
return 0, 0, false
}
list, err := strconv.Atoi(listPart)
if err != nil {
return 0, 0, false
}
slot, err := strconv.Atoi(slotPart)
if err != nil {
return 0, 0, false
}
return list, slot, true
}
func dsnFromRuntimeConfig() (string, error) {
candidates := []string{
`runtime\configs\dnf\logic.toml`,
`..\runtime\configs\dnf\logic.toml`,
}
for _, path := range candidates {
raw, err := os.ReadFile(path)
if err != nil {
continue
}
for _, line := range strings.Split(string(raw), "\n") {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "mysql_dsn") {
continue
}
_, value, found := strings.Cut(trimmed, "=")
if !found {
continue
}
return strings.Trim(strings.TrimSpace(value), `"`), nil
}
}
return "", fmt.Errorf("mysql_dsn not found; pass -dsn explicitly")
}
func locateInventoryTable(ctx context.Context, db *sql.DB) (string, string, error) {
rows, err := db.QueryContext(ctx,
"SELECT table_schema, table_name FROM information_schema.tables WHERE table_name LIKE '%\\_inventory\\_items' ORDER BY table_schema, table_name")
if err != nil {
return "", "", err
}
defer rows.Close()
for rows.Next() {
var schema, table string
if err := rows.Scan(&schema, &table); err != nil {
return "", "", err
}
if strings.Contains(table, "account") {
continue
}
return schema, table, nil
}
return "", "", fmt.Errorf("no character inventory_items table found")
}
func compact(slots []int) string {
if len(slots) == 0 {
return "(none)"
}
var builder strings.Builder
runStart, previous := slots[0], slots[0]
flush := func(from, to int) {
if builder.Len() > 0 {
builder.WriteString(", ")
}
if from == to {
fmt.Fprintf(&builder, "%d", from)
return
}
fmt.Fprintf(&builder, "%d-%d", from, to)
}
for _, slot := range slots[1:] {
if slot == previous+1 {
previous = slot
continue
}
flush(runStart, previous)
runStart, previous = slot, slot
}
flush(runStart, previous)
return builder.String()
}