Files
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

161 lines
3.7 KiB
Go

// Scratch tool: inspect the social (friend / guild / party) UI definitions and
// any UI text inside the shipped Script.pvf.
//
// usage:
//
// go run ./cmd/scratch/pvfsocial <pvf> list <pathSubstring> [max]
// go run ./cmd/scratch/pvfsocial <pvf> dump <pathSubstring> [maxFiles]
// go run ./cmd/scratch/pvfsocial <pvf> find <text> [pathFilter] [maxHits]
package main
import (
"fmt"
"os"
"strconv"
"strings"
"longheng.io/server/internal/platform/pvf"
)
func main() {
if len(os.Args) < 4 {
fmt.Println("usage: pvfsocial <pvf> list|dump|find <arg> [extra] [max]")
return
}
path, mode, needle := os.Args[1], os.Args[2], os.Args[3]
extra := ""
if len(os.Args) > 4 {
extra = os.Args[4]
}
limit := 0
if len(os.Args) > 5 {
if n, err := strconv.Atoi(os.Args[5]); err == nil && n > 0 {
limit = n
}
}
bundle, err := pvf.Load(pvf.Options{Path: path, MaxBytes: pvf.NoMaxBytes})
if err != nil {
fmt.Println("load:", err)
return
}
archive, err := pvf.OpenArchive(bundle)
if err != nil {
fmt.Println("open:", err)
return
}
fmt.Printf("archive format=%s files=%d readable=%v\n", archive.Format(), archive.FileCount(), archive.CanReadFileData())
switch mode {
case "list":
printList(archive, needle, limit)
case "dump":
dump(archive, strings.ToLower(needle), limit)
case "find":
search(archive, needle, strings.ToLower(extra), limit)
default:
fmt.Println("unknown mode", mode)
}
}
func printList(archive *pvf.Archive, needle string, limit int) {
if limit == 0 {
limit = 400
}
low := strings.ToLower(strings.ReplaceAll(needle, "\\", "/"))
count := 0
archive.RangePaths(func(p string) bool {
if strings.Contains(strings.ToLower(strings.ReplaceAll(p, "\\", "/")), low) {
count++
if count <= limit {
fmt.Println(" " + p)
}
}
return true
})
fmt.Printf("paths containing %q: %d (shown %d)\n", needle, count, min(count, limit))
}
func dump(archive *pvf.Archive, low string, maxFiles int) {
if maxFiles == 0 {
maxFiles = 4
}
shown := 0
var matches []string
archive.RangePaths(func(p string) bool {
if strings.Contains(strings.ToLower(strings.ReplaceAll(p, "\\", "/")), low) {
matches = append(matches, p)
}
return true
})
for _, m := range matches {
if shown >= maxFiles {
break
}
text, err := archive.ReadText(m)
if err != nil {
fmt.Printf("=== %s: read error: %v\n", m, err)
continue
}
shown++
fmt.Printf("\n================ %s (%d chars) ================\n", m, len(text))
fmt.Println(text)
}
if shown == 0 {
fmt.Println("no path matched")
}
}
// search reads the text of every file whose path contains pathFilter and
// reports the ones whose body contains needle.
func search(archive *pvf.Archive, needle, pathFilter string, maxHits int) {
if pathFilter == "" {
pathFilter = "ui/"
}
if maxHits == 0 {
maxHits = 40
}
scanned, hits := 0, 0
var paths []string
archive.RangePaths(func(p string) bool {
if strings.Contains(strings.ToLower(strings.ReplaceAll(p, "\\", "/")), pathFilter) {
paths = append(paths, p)
}
return true
})
fmt.Printf("scanning %d files under %q for %q\n", len(paths), pathFilter, needle)
for _, p := range paths {
text, err := archive.ReadText(p)
if err != nil {
continue
}
scanned++
if !strings.Contains(text, needle) {
continue
}
hits++
fmt.Printf("HIT %s\n", p)
for _, line := range strings.Split(text, "\n") {
if strings.Contains(line, needle) {
trimmed := strings.TrimSpace(line)
if len(trimmed) > 260 {
trimmed = trimmed[:260] + "..."
}
fmt.Printf(" %s\n", trimmed)
}
}
if hits >= maxHits {
fmt.Println(" (hit limit reached)")
break
}
}
fmt.Printf("scanned=%d hits=%d\n", scanned, hits)
}
func min(a, b int) int {
if a < b {
return a
}
return b
}