DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+247
View File
@@ -0,0 +1,247 @@
// 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()
}