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,209 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/purchaselimit"
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func TestParseCurrentPurchaseLimitSectionReadsNewField(t *testing.T) {
|
||||
document := mustCurrentPurchaseLimitDocument(t, "[purchase limit]\n`account` `weekly` 10\n")
|
||||
policy, found, err := parseCurrentPurchaseLimitSection(490004806, document)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("found=%t err=%v", found, err)
|
||||
}
|
||||
if policy.ItemID != 490004806 || policy.Scope != purchaselimit.ScopeAccount ||
|
||||
policy.Period != purchaselimit.PeriodWeekly || policy.Limit != 10 ||
|
||||
policy.Section != currentPurchaseLimitSection {
|
||||
t.Fatalf("policy=%+v", policy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCurrentPurchaseLimitSectionReadsLegacyDailyField(t *testing.T) {
|
||||
document := mustCurrentPurchaseLimitDocument(t, "[daily purchase limit]\n`character` 1\n")
|
||||
policy, found, err := parseCurrentPurchaseLimitSection(10154547, document)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("found=%t err=%v", found, err)
|
||||
}
|
||||
if policy.Scope != purchaselimit.ScopeCharacter || policy.Period != purchaselimit.PeriodDaily ||
|
||||
policy.Limit != 1 || policy.Section != currentDailyPurchaseLimitSection {
|
||||
t.Fatalf("policy=%+v", policy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCurrentPurchaseLimitSectionRejectsUnknownShape(t *testing.T) {
|
||||
document := mustCurrentPurchaseLimitDocument(t, "[purchase limit]\n`account` 10\n")
|
||||
if _, found, err := parseCurrentPurchaseLimitSection(1, document); err == nil || found {
|
||||
t.Fatalf("found=%t err=%v want malformed error", found, err)
|
||||
}
|
||||
document = mustCurrentPurchaseLimitDocument(t, "[purchase limit]\n`clan` `weekly` 10\n")
|
||||
if _, _, err := parseCurrentPurchaseLimitSection(1, document); err == nil {
|
||||
t.Fatal("unknown scope accepted")
|
||||
}
|
||||
document = mustCurrentPurchaseLimitDocument(t, "[purchase limit]\n`account` `fortnightly` 10\n")
|
||||
if _, _, err := parseCurrentPurchaseLimitSection(1, document); err == nil {
|
||||
t.Fatal("unknown period accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCurrentPurchaseLimitSectionAbsent(t *testing.T) {
|
||||
document := mustCurrentPurchaseLimitDocument(t, "[stack limit]\n999\n")
|
||||
if _, found, err := parseCurrentPurchaseLimitSection(1, document); found || err != nil {
|
||||
t.Fatalf("found=%t err=%v", found, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 真机 PVF:确认三件真实道具的限购字段解析结果(新字段 / 旧字段 / 装备)。
|
||||
func TestRealPVFPurchaseLimitSections(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to the runtime Script.pvf")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
items, err := newPVFDungeonDropCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cases := []struct {
|
||||
itemID uint32
|
||||
scope purchaselimit.Scope
|
||||
period purchaselimit.Period
|
||||
limit uint32
|
||||
section string
|
||||
}{
|
||||
{490004806, purchaselimit.ScopeAccount, purchaselimit.PeriodWeekly, 10, currentPurchaseLimitSection},
|
||||
{10154547, purchaselimit.ScopeCharacter, purchaselimit.PeriodDaily, 1, currentDailyPurchaseLimitSection},
|
||||
{400350073, purchaselimit.ScopeCharacter, purchaselimit.PeriodAccumulate, 1, currentPurchaseLimitSection},
|
||||
}
|
||||
for _, want := range cases {
|
||||
definition, err := items.ResolveItem(want.itemID)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve item=%d: %v", want.itemID, err)
|
||||
}
|
||||
if definition.PurchaseLimitError != "" {
|
||||
t.Fatalf("item=%d purchase limit parse error: %s", want.itemID, definition.PurchaseLimitError)
|
||||
}
|
||||
if !definition.PurchaseLimitFound {
|
||||
t.Fatalf("item=%d has no purchase limit (path=%s)", want.itemID, definition.PVFPath)
|
||||
}
|
||||
got := definition.PurchaseLimit
|
||||
if got.Scope != want.scope || got.Period != want.period || got.Limit != want.limit || got.Section != want.section {
|
||||
t.Fatalf("item=%d policy=%+v want scope=%s period=%s limit=%d section=%s",
|
||||
want.itemID, got, want.scope, want.period, want.limit, want.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustCurrentPurchaseLimitDocument(t *testing.T, text string) *dnfpvf.Document {
|
||||
t.Helper()
|
||||
document, err := dnfpvf.Parse("test.stk", text)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return document
|
||||
}
|
||||
|
||||
// 全库回归:Script.pvf 里每一个带限购字段的脚本都必须能被解析器读出来。
|
||||
// 这是“畸形字段按不限购处理”这条兜底的守门人——真出现读不懂的写法时要在这里
|
||||
// 暴露出来,而不是悄悄放行。
|
||||
func TestRealPVFPurchaseLimitCorpusParses(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to the runtime Script.pvf")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files := 0
|
||||
bothSections := 0
|
||||
shapes := make(map[string]int)
|
||||
buckets := make(map[string]int)
|
||||
failures := make([]string, 0, 8)
|
||||
for _, file := range archive.Files() {
|
||||
// 只有道具脚本参与限购:stackable/equipment 才是道具目录解析的对象。
|
||||
// etc/** 里的同名段落是别的系统(如 pvpcolosseum.etc 的两个数字)。
|
||||
if !strings.HasPrefix(file.ArchivePath, "stackable/") && !strings.HasPrefix(file.ArchivePath, "equipment/") {
|
||||
continue
|
||||
}
|
||||
text, readErr := archive.ReadText(file.ArchivePath)
|
||||
if readErr != nil {
|
||||
continue
|
||||
}
|
||||
lower := strings.ToLower(text)
|
||||
hasNew := strings.Contains(lower, "["+currentPurchaseLimitSection+"]")
|
||||
hasLegacy := strings.Contains(lower, "["+currentDailyPurchaseLimitSection+"]")
|
||||
if !hasNew && !hasLegacy {
|
||||
continue
|
||||
}
|
||||
files++
|
||||
// 两套字段同时出现的道具:解析器只取新字段(旧字段被忽略)。这里统计规模,
|
||||
// 一旦不是 0 就要确认"同一件道具是否需要同时受两条限购约束"。
|
||||
if hasNew && hasLegacy {
|
||||
bothSections++
|
||||
}
|
||||
document, parseErr := dnfpvf.Parse(file.ArchivePath, text)
|
||||
if parseErr != nil {
|
||||
t.Fatalf("parse %s: %v", file.ArchivePath, parseErr)
|
||||
}
|
||||
policy, found, err := parseCurrentPurchaseLimitSection(1, document)
|
||||
if err != nil {
|
||||
// 空的段落与语义不明的取值(如 `trade`)是“没有可执行限购”,
|
||||
// 解析器已经把它记成 PurchaseLimitError 并放行;这里只要求它
|
||||
// 不把可识别的写法判错,畸形写法单独归档。
|
||||
raw := currentPurchaseLimitRawSection(text)
|
||||
switch {
|
||||
case strings.TrimSpace(raw) == "["+currentDailyPurchaseLimitSection+"]" ||
|
||||
strings.TrimSpace(raw) == "["+currentPurchaseLimitSection+"]":
|
||||
buckets["empty_section"]++
|
||||
case strings.Contains(raw, "`"+currentPurchaseLimitUnknownTradeScope+"`"):
|
||||
buckets["unknown_scope_trade"]++
|
||||
default:
|
||||
failures = append(failures, file.ArchivePath+" :: "+raw)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("limit section of %s was not detected", file.ArchivePath)
|
||||
}
|
||||
shapes[fmt.Sprintf("%s %s %s %d", policy.Section, policy.Scope, policy.Period, policy.Limit)]++
|
||||
}
|
||||
if files < 400 {
|
||||
t.Fatalf("only %d files declared a purchase limit; the corpus scan looks wrong", files)
|
||||
}
|
||||
t.Logf("purchase-limit corpus: files=%d shapes=%d both_sections=%d buckets=%v unparsed=%d", files, len(shapes), bothSections, buckets, len(failures))
|
||||
for shape, count := range shapes {
|
||||
t.Logf(" %4d %s", count, shape)
|
||||
}
|
||||
for _, failure := range failures {
|
||||
t.Logf(" UNPARSED %s", failure)
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
t.Fatalf("%d purchase-limit sections could not be parsed", len(failures))
|
||||
}
|
||||
}
|
||||
|
||||
// currentPurchaseLimitRawSection 取限购字段的原文,便于诊断畸形写法。
|
||||
func currentPurchaseLimitRawSection(text string) string {
|
||||
lower := strings.ToLower(text)
|
||||
index := strings.Index(lower, "["+currentPurchaseLimitSection+"]")
|
||||
if index < 0 {
|
||||
index = strings.Index(lower, "["+currentDailyPurchaseLimitSection+"]")
|
||||
}
|
||||
if index < 0 {
|
||||
return ""
|
||||
}
|
||||
rest := text[index:]
|
||||
if end := strings.Index(rest, "\n["); end > 0 {
|
||||
rest = rest[:end]
|
||||
}
|
||||
return strings.Join(strings.Fields(rest), " ")
|
||||
}
|
||||
Reference in new issue
Block a user