服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
416 lines
15 KiB
Go
416 lines
15 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
|
platformpvf "longheng.io/server/internal/platform/pvf"
|
|
)
|
|
|
|
func TestCurrentCeraPackageInventoryRouteSelectsEveryPVFGroupExactlyOnce(t *testing.T) {
|
|
source := dungeonDropCatalogTestSource{
|
|
"monster/monster.lst": "",
|
|
"equipment/equipment.lst": "",
|
|
"stackable/stackable.lst": "500 `cash/fate_package.stk`\n600 `material/fixed.stk`\n601 `material/title_a.stk`\n602 `material/title_b.stk`\n603 `material/bead_a.stk`\n604 `material/bead_b.stk`\n605 `cash/nested_box.stk`\n606 `material/nested_reward.stk`\n",
|
|
"stackable/cash/fate_package.stk": "[stackable type]\n`[cera package]`\n[stack limit]\n1\n[package data]\n600 3\n605 2\n[/package data]\n[package data selection]\n601 1\n602 1\n[/package data selection]\n[package data selection]\n603 1\n604 1\n[/package data selection]\n",
|
|
"stackable/cash/nested_box.stk": "[stackable type]\n`[cera package]`\n[stack limit]\n1\n[package data selection]\n606 4\n[/package data selection]\n",
|
|
"stackable/material/fixed.stk": "[stackable type]\n`[material]`\n[stack limit]\n999\n",
|
|
"stackable/material/title_a.stk": "[stackable type]\n`[material]`\n[stack limit]\n999\n",
|
|
"stackable/material/title_b.stk": "[stackable type]\n`[material]`\n[stack limit]\n999\n",
|
|
"stackable/material/bead_a.stk": "[stackable type]\n`[material]`\n[stack limit]\n999\n",
|
|
"stackable/material/bead_b.stk": "[stackable type]\n`[material]`\n[stack limit]\n999\n",
|
|
"stackable/material/nested_reward.stk": "[stackable type]\n`[material]`\n[stack limit]\n999\n",
|
|
}
|
|
catalog, err := newPVFDungeonDropCatalog(source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
definition, err := resolveCurrentCeraPackageDefinition(catalog, 500)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if definition.StackableType != "[cera package]" || len(definition.SelectionGroups) != 2 {
|
|
t.Fatalf("definition=%+v", definition)
|
|
}
|
|
rewards, err := resolveCurrentCeraPackageOpenRewards(definition, []currentCeraPackageChoice{{ItemID: 602}, {ItemID: 603}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := make([]uint32, 0, len(rewards))
|
|
for _, reward := range rewards {
|
|
got = append(got, reward.Definition.ItemID)
|
|
}
|
|
want := []uint32{600, 605, 602, 603}
|
|
if fmt.Sprint(got) != fmt.Sprint(want) {
|
|
t.Fatalf("selected rewards=%v want=%v", got, want)
|
|
}
|
|
expanded, err := expandCurrentDeterministicNestedCeraPackages(catalog, rewards)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got = got[:0]
|
|
counts := make(map[uint32]uint32)
|
|
for _, reward := range expanded {
|
|
got = append(got, reward.Definition.ItemID)
|
|
counts[reward.Definition.ItemID] += reward.Count
|
|
}
|
|
want = []uint32{600, 606, 602, 603}
|
|
if fmt.Sprint(got) != fmt.Sprint(want) || counts[606] != 8 {
|
|
t.Fatalf("expanded rewards=%v counts=%v want=%v nested_count=8", got, counts, want)
|
|
}
|
|
for _, choices := range [][]currentCeraPackageChoice{
|
|
nil,
|
|
{{ItemID: 601}},
|
|
{{ItemID: 601}, {ItemID: 602}, {ItemID: 603}},
|
|
{{ItemID: 601}, {ItemID: 603}, {ItemID: 604}},
|
|
{{ItemID: 601}, {ItemID: 603}, {ItemID: 600}},
|
|
} {
|
|
if _, err := resolveCurrentCeraPackageOpenRewards(definition, choices); err == nil {
|
|
t.Fatalf("invalid choices accepted: %+v", choices)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRealPVFFatePackageChainUsesNativePackageAndBoosterBoundaries(t *testing.T) {
|
|
catalog := currentCeraPackageRealPVFCatalog(t)
|
|
shopCatalog, err := newPVFCeraShopCatalog(catalog.source)
|
|
if err != nil {
|
|
t.Fatalf("load Fate Cera-shop catalog: %v", err)
|
|
}
|
|
|
|
for itemID := uint32(2682985); itemID <= 2683002; itemID++ {
|
|
definition, err := resolveCurrentCeraPackageDefinition(catalog, itemID)
|
|
if err != nil {
|
|
t.Fatalf("Fate main package item=%d: %v", itemID, err)
|
|
}
|
|
wantGroups := 1
|
|
if itemID >= 2682994 {
|
|
wantGroups = 2
|
|
}
|
|
if definition.StackableType != "[cera package]" || len(definition.Rewards) == 0 || len(definition.SelectionGroups) != wantGroups {
|
|
t.Fatalf("Fate main package item=%d type=%q fixed=%d groups=%d want_groups=%d", itemID, definition.StackableType, len(definition.Rewards), len(definition.SelectionGroups), wantGroups)
|
|
}
|
|
groupSizes := make([]int, 0, len(definition.SelectionGroups))
|
|
for _, group := range definition.SelectionGroups {
|
|
groupSizes = append(groupSizes, len(group))
|
|
}
|
|
t.Logf("Fate main package item=%d fixed=%d selection_group_sizes=%v", itemID, len(definition.Rewards), groupSizes)
|
|
|
|
selections := make([]currentCeraShopPackageSelection, 0, len(definition.SelectionGroups))
|
|
selectedRewards := append([]currentCeraPackageReward(nil), definition.Rewards...)
|
|
for groupIndex, group := range definition.SelectionGroups {
|
|
selections = append(selections, currentCeraShopPackageSelection{
|
|
PackageItemID: itemID,
|
|
GroupIndex: uint16(groupIndex),
|
|
ChoiceIndex: 0,
|
|
})
|
|
selectedRewards = append(selectedRewards, group[0])
|
|
}
|
|
avatarChoices := make([]currentCeraPackageChoice, 0)
|
|
seenAvatars := make(map[uint32]struct{})
|
|
for _, reward := range selectedRewards {
|
|
if !reward.Avatar {
|
|
continue
|
|
}
|
|
if _, duplicate := seenAvatars[reward.Definition.ItemID]; duplicate {
|
|
continue
|
|
}
|
|
seenAvatars[reward.Definition.ItemID] = struct{}{}
|
|
avatarChoices = append(avatarChoices, currentCeraPackageChoice{ItemID: reward.Definition.ItemID})
|
|
}
|
|
shopRewards, autoOpen, resolveErr := resolveCurrentCeraShopAutoOpenRewards(
|
|
shopCatalog,
|
|
definition.Source,
|
|
1,
|
|
avatarChoices,
|
|
selections,
|
|
)
|
|
if resolveErr != nil || !autoOpen || len(shopRewards) == 0 {
|
|
t.Fatalf("Fate checkout item=%d auto_open=%t rewards=%d err=%v", itemID, autoOpen, len(shopRewards), resolveErr)
|
|
}
|
|
for _, reward := range shopRewards {
|
|
if reward.Definition.ItemID == itemID || reward.Definition.ItemID == 2683004 || reward.Definition.ItemID == 2683005 || reward.Definition.ItemID == 2683006 {
|
|
t.Fatalf("Fate checkout item=%d leaked inert package reward=%+v", itemID, reward)
|
|
}
|
|
}
|
|
}
|
|
|
|
for itemID, wantGroups := range map[uint32]int{
|
|
2683004: 0, // title collection
|
|
2683005: 0, // card collection
|
|
2683006: 1, // full-body avatar box (singleton nested selection)
|
|
} {
|
|
definition, err := resolveCurrentCeraPackageDefinition(catalog, itemID)
|
|
if err != nil {
|
|
t.Fatalf("Fate nested cera package item=%d: %v", itemID, err)
|
|
}
|
|
if definition.StackableType != "[cera package]" || len(definition.SelectionGroups) != wantGroups {
|
|
t.Fatalf("Fate nested cera package item=%d type=%q groups=%d want=%d", itemID, definition.StackableType, len(definition.SelectionGroups), wantGroups)
|
|
}
|
|
opened, openErr := resolveCurrentCeraPackageOpenRewards(definition, nil)
|
|
if openErr != nil || len(opened) == 0 {
|
|
t.Fatalf("Fate nested inventory package item=%d rewards=%d err=%v", itemID, len(opened), openErr)
|
|
}
|
|
}
|
|
nestedRewards := make([]currentCeraPackageReward, 0, 3)
|
|
for _, itemID := range []uint32{2683004, 2683005, 2683006} {
|
|
item, err := catalog.ResolveItem(itemID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
nestedRewards = append(nestedRewards, currentCeraPackageReward{Definition: item, Count: 1})
|
|
}
|
|
expanded, err := expandCurrentDeterministicNestedCeraPackages(catalog, nestedRewards)
|
|
if err != nil {
|
|
t.Fatalf("expand deterministic Fate inner packages: %v", err)
|
|
}
|
|
containsFinalAvatarBox := false
|
|
for _, reward := range expanded {
|
|
switch reward.Definition.ItemID {
|
|
case 2683004, 2683005, 2683006:
|
|
t.Fatalf("inert Fate inner package leaked through deterministic expansion: %+v", reward)
|
|
case 2683028:
|
|
containsFinalAvatarBox = true
|
|
}
|
|
}
|
|
if !containsFinalAvatarBox {
|
|
t.Fatalf("expanded Fate inner rewards do not contain final op160 avatar selection box: %+v", expanded)
|
|
}
|
|
|
|
for _, itemID := range []uint32{2683003, 2683007, 2683008, 2683035} {
|
|
definition, err := resolveCurrentBoosterDefinition(catalog, itemID, currentBoosterRequestRandom)
|
|
if err != nil {
|
|
t.Fatalf("Fate random booster item=%d: %v", itemID, err)
|
|
}
|
|
if definition.Source.ItemID != itemID || len(definition.Random.Groups) == 0 {
|
|
t.Fatalf("Fate random booster item=%d definition=%+v", itemID, definition)
|
|
}
|
|
}
|
|
selection, err := resolveCurrentBoosterDefinition(catalog, 2683028, currentBoosterRequestSelection)
|
|
if err != nil {
|
|
t.Fatalf("Fate final avatar selection item=2683028: %v", err)
|
|
}
|
|
if selection.Source.ItemID != 2683028 || len(selection.Selection) == 0 || len(selection.SelectionCategory) == 0 {
|
|
t.Fatalf("Fate final avatar selection=%+v", selection)
|
|
}
|
|
}
|
|
|
|
func TestRealPVFAllCeraPackageDefinitionsUseTheSharedMapper(t *testing.T) {
|
|
catalog := currentCeraPackageRealPVFCatalog(t)
|
|
type counts struct {
|
|
total int
|
|
withFixed int
|
|
withGroup int
|
|
}
|
|
byType := make(map[string]counts)
|
|
invalidPVF := make(map[string][]string)
|
|
unexpected := make([]string, 0)
|
|
itemIDs := make([]int, 0, len(catalog.itemRefs))
|
|
for itemID := range catalog.itemRefs {
|
|
itemIDs = append(itemIDs, int(itemID))
|
|
}
|
|
sort.Ints(itemIDs)
|
|
for _, rawID := range itemIDs {
|
|
itemID := uint32(rawID)
|
|
definition, err := catalog.ResolveItem(itemID)
|
|
if err != nil || definition.Kind != dungeonDropItemStackable {
|
|
continue
|
|
}
|
|
stackableType := normalizeDungeonDropStackableType(definition.StackableType)
|
|
if stackableType != "[cera package]" && stackableType != "[usable cera package]" {
|
|
continue
|
|
}
|
|
parsed, recognized, parseErr := resolveCurrentCeraPackageDefinitionForSource(catalog, definition)
|
|
entry := byType[stackableType]
|
|
entry.total++
|
|
if parseErr != nil || !recognized {
|
|
detail := fmt.Sprintf("item=%d path=%s err=%v", itemID, definition.PVFPath, parseErr)
|
|
reason := "unexpected"
|
|
switch {
|
|
case strings.Contains(detail, " has no rewards"):
|
|
reason = "empty_package_data"
|
|
case strings.Contains(detail, "is not present in a supported PVF item list"):
|
|
reason = "missing_reward_item"
|
|
default:
|
|
unexpected = append(unexpected, detail)
|
|
}
|
|
invalidPVF[reason] = append(invalidPVF[reason], detail)
|
|
byType[stackableType] = entry
|
|
continue
|
|
}
|
|
if len(parsed.Rewards) > 0 {
|
|
entry.withFixed++
|
|
}
|
|
if len(parsed.SelectionGroups) > 0 {
|
|
entry.withGroup++
|
|
}
|
|
byType[stackableType] = entry
|
|
}
|
|
for stackableType, entry := range byType {
|
|
t.Logf("PVF package mapper type=%s total=%d fixed=%d selection=%d", stackableType, entry.total, entry.withFixed, entry.withGroup)
|
|
}
|
|
for reason, entries := range invalidPVF {
|
|
t.Logf("PVF package definitions rejected reason=%s count=%d", reason, len(entries))
|
|
}
|
|
if len(unexpected) > 0 {
|
|
limit := len(unexpected)
|
|
if limit > 50 {
|
|
limit = 50
|
|
}
|
|
t.Fatalf("unexpected package-mapper failures=%d first=%v", len(unexpected), unexpected[:limit])
|
|
}
|
|
}
|
|
|
|
func TestRealPVFAllNativeGiftBoxFamiliesAreClassified(t *testing.T) {
|
|
catalog := currentCeraPackageRealPVFCatalog(t)
|
|
type familyCount struct {
|
|
total int
|
|
mapped int
|
|
invalidShape int
|
|
missingReward int
|
|
}
|
|
families := make(map[string]familyCount)
|
|
samples := make(map[string][]string)
|
|
addSample := func(key string, itemID uint32, path string, detail ...string) {
|
|
if len(samples[key]) < 8 {
|
|
suffix := ""
|
|
if len(detail) > 0 && detail[0] != "" {
|
|
suffix = ":" + detail[0]
|
|
}
|
|
samples[key] = append(samples[key], fmt.Sprintf("%d:%s%s", itemID, path, suffix))
|
|
}
|
|
}
|
|
itemIDs := make([]int, 0, len(catalog.itemRefs))
|
|
for itemID := range catalog.itemRefs {
|
|
itemIDs = append(itemIDs, int(itemID))
|
|
}
|
|
sort.Ints(itemIDs)
|
|
for _, rawID := range itemIDs {
|
|
itemID := uint32(rawID)
|
|
item, err := catalog.ResolveItem(itemID)
|
|
if err != nil || item.Kind != dungeonDropItemStackable {
|
|
continue
|
|
}
|
|
family := normalizeMagicBoxPVFType(item.StackableType)
|
|
switch family {
|
|
case "booster selection":
|
|
entry := families[family]
|
|
entry.total++
|
|
definition, resolveErr := resolveCurrentBoosterDefinition(catalog, itemID, currentBoosterRequestSelection)
|
|
if resolveErr != nil || len(definition.Selection) == 0 {
|
|
entry.invalidShape++
|
|
detail := currentGiftBoxPVFShape(t, catalog.source, item.PVFPath)
|
|
if resolveErr != nil {
|
|
detail = "error=" + resolveErr.Error() + ";" + detail
|
|
}
|
|
addSample(family+":invalid_shape", itemID, item.PVFPath, detail)
|
|
families[family] = entry
|
|
continue
|
|
}
|
|
missing := false
|
|
for _, reward := range definition.Selection {
|
|
if _, rewardErr := catalog.ResolveItem(reward.ItemID); rewardErr != nil {
|
|
missing = true
|
|
break
|
|
}
|
|
}
|
|
if missing {
|
|
entry.missingReward++
|
|
addSample(family+":missing_reward", itemID, item.PVFPath)
|
|
} else {
|
|
entry.mapped++
|
|
}
|
|
families[family] = entry
|
|
case "booster", "cera booster", "booster random", "random upgradable legacy", "upgradable legacy":
|
|
entry := families[family]
|
|
entry.total++
|
|
resolution, resolveErr := resolveCurrentMagicBox(catalog, catalog.source, int64(itemID))
|
|
if resolveErr != nil || resolution.Kind != "random" || len(resolution.Groups) == 0 {
|
|
entry.invalidShape++
|
|
addSample(family+":invalid_shape", itemID, item.PVFPath, currentGiftBoxPVFShape(t, catalog.source, item.PVFPath))
|
|
families[family] = entry
|
|
continue
|
|
}
|
|
missing := false
|
|
for _, group := range resolution.Groups {
|
|
for _, reward := range group.Entries {
|
|
if reward.ItemID <= 0 {
|
|
continue
|
|
}
|
|
if _, rewardErr := catalog.ResolveItem(uint32(reward.ItemID)); rewardErr != nil {
|
|
missing = true
|
|
break
|
|
}
|
|
}
|
|
if missing {
|
|
break
|
|
}
|
|
}
|
|
if missing {
|
|
entry.missingReward++
|
|
addSample(family+":missing_reward", itemID, item.PVFPath)
|
|
} else {
|
|
entry.mapped++
|
|
}
|
|
families[family] = entry
|
|
}
|
|
}
|
|
for family, entry := range families {
|
|
t.Logf("PVF native gift-box family=%s total=%d mapped=%d invalid_shape=%d missing_reward=%d", family, entry.total, entry.mapped, entry.invalidShape, entry.missingReward)
|
|
}
|
|
for key, entries := range samples {
|
|
t.Logf("PVF native gift-box rejected key=%s samples=%v", key, entries)
|
|
}
|
|
for _, required := range []string{"booster", "booster selection", "upgradable legacy"} {
|
|
if entry := families[required]; entry.total == 0 || entry.mapped == 0 {
|
|
t.Fatalf("native gift-box family %q was not mapped: %+v", required, entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
func currentGiftBoxPVFShape(t *testing.T, source dnfpvf.Source, path string) string {
|
|
t.Helper()
|
|
document, err := parseDungeonCardPVFDocument(source, path)
|
|
if err != nil {
|
|
return "parse=" + err.Error()
|
|
}
|
|
parts := make([]string, 0, len(document.Sections))
|
|
for _, section := range document.Sections {
|
|
name := normalizeMagicBoxSectionName(section.Name)
|
|
if strings.HasPrefix(name, "/") || name == "name" || name == "stackable type" {
|
|
continue
|
|
}
|
|
ints := magicBoxSectionInts(document, section)
|
|
if len(ints) == 0 {
|
|
parts = append(parts, name)
|
|
continue
|
|
}
|
|
if len(ints) > 16 {
|
|
ints = ints[:16]
|
|
}
|
|
parts = append(parts, fmt.Sprintf("%s=%v", name, ints))
|
|
}
|
|
return strings.Join(parts, ";")
|
|
}
|
|
|
|
func currentCeraPackageRealPVFCatalog(t *testing.T) *pvfDungeonDropCatalog {
|
|
t.Helper()
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify package mapping against the active PVF")
|
|
}
|
|
archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: pvfPath})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := newPVFDungeonDropCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return catalog
|
|
}
|