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,700 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
dnfraid "longheng.io/server/internal/modules/dnf/raid"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func TestRealScriptPVFLukeSingleRewardPoolsResolveFinalItems(t *testing.T) {
|
||||
pvfPath := realLukeRewardPVFPath()
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Skipf("runtime Script.pvf unavailable: %v", err)
|
||||
}
|
||||
config, err := dnfraid.LoadLukeConfig(archive)
|
||||
if err != nil {
|
||||
t.Fatalf("load Luke raid config: %v", err)
|
||||
}
|
||||
items, err := newPVFDungeonDropCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatalf("load real PVF item catalog: %v", err)
|
||||
}
|
||||
catalog, err := newRaidRewardPVFCatalog(items, config.Raid, nil, false, currentLukeRewardPhaseCount)
|
||||
if err != nil {
|
||||
t.Fatalf("load Luke reward catalog: %v", err)
|
||||
}
|
||||
singlePhaseIndex := len(config.Raid.Phases)
|
||||
for _, kind := range []string{"party_card", "squad_item"} {
|
||||
pool, found := catalog.Pool(singlePhaseIndex, kind)
|
||||
if !found || len(pool.Entries) == 0 || pool.TotalWeight == 0 {
|
||||
t.Fatalf("Luke single %s pool=%+v found=%t", kind, pool, found)
|
||||
}
|
||||
resolved, resolveErr := catalog.ResolvePool(singlePhaseIndex, kind, &antonRewardPVFSequenceRNG{})
|
||||
if resolveErr != nil {
|
||||
t.Fatalf("resolve Luke single %s: %v", kind, resolveErr)
|
||||
}
|
||||
definition, definitionErr := catalog.ResolveFinalItem(resolved.ItemID)
|
||||
if definitionErr != nil {
|
||||
t.Fatalf("resolve Luke single %s final item %d: %v", kind, resolved.ItemID, definitionErr)
|
||||
}
|
||||
if definition.ItemID != resolved.ItemID || resolved.Count == 0 || resolved.ItemPVFPath == "" {
|
||||
t.Fatalf("Luke single %s resolved=%+v definition=%+v", kind, resolved, definition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealScriptPVFLukeSingleRewardDefinitionsIncludeDarkRootRows(t *testing.T) {
|
||||
pvfPath := strings.TrimSpace(os.Getenv("DNF_LUKE_REWARD_REAL_PVF_SMOKE"))
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNF_LUKE_REWARD_REAL_PVF_SMOKE to verify transplanted Luke reward rows")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Skipf("runtime Script.pvf unavailable: %v", err)
|
||||
}
|
||||
for itemID, want := range map[uint32][]int64{
|
||||
10302724: {10159143, 100000, 4, 10159143, 100000, 5, 10159143, 100000, 6},
|
||||
10302725: {10159143, 100000, 2, 10159143, 100000, 3},
|
||||
} {
|
||||
documentPath := fmt.Sprintf("stackable/10302001/%d.stk", itemID)
|
||||
text, readErr := archive.ReadText(documentPath)
|
||||
if readErr != nil {
|
||||
t.Fatalf("read Luke single reward %d: %v", itemID, readErr)
|
||||
}
|
||||
document, parseErr := dnfpvf.Parse(documentPath, text)
|
||||
if parseErr != nil {
|
||||
t.Fatalf("parse Luke single reward %d: %v", itemID, parseErr)
|
||||
}
|
||||
if got := document.Ints("int data"); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Luke single reward %d int data=%v want=%v", itemID, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
const darkRootBoosterRow = "1 10159143 36500 5 10159143 33255 6 10159143 30250 7"
|
||||
for itemID := uint32(10302726); itemID <= 10302733; itemID++ {
|
||||
documentPath := fmt.Sprintf("stackable/10302001/%d.stk", itemID)
|
||||
text, readErr := archive.ReadText(documentPath)
|
||||
if readErr != nil {
|
||||
t.Fatalf("read Luke single reward %d: %v", itemID, readErr)
|
||||
}
|
||||
normalized := strings.Join(strings.Fields(text), " ")
|
||||
if !strings.Contains(normalized, "[booster info]") || !strings.Contains(normalized, darkRootBoosterRow) {
|
||||
t.Fatalf("Luke single reward %d is missing the 5-7 dark-root booster row", itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func realLukeRewardPVFPath() string {
|
||||
if configured := strings.TrimSpace(os.Getenv("DNF_LUKE_REWARD_REAL_PVF_SMOKE")); configured != "" {
|
||||
return configured
|
||||
}
|
||||
return filepath.Clean(filepath.Join("..", "..", "..", "..", "runtime", "data", "dnf", "Script.pvf"))
|
||||
}
|
||||
|
||||
func TestAntonRewardPVFCatalogReadsOnlyCurrentDefinitionsAndBuildsPhasePools(t *testing.T) {
|
||||
items, source, finalByDefinition := newAntonRewardPVFTestItems(t)
|
||||
catalog, err := newAntonRewardPVFCatalog(items, antonRewardPVFTestConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expectedPaths := antonRewardPVFTestDefinitionPaths(t, items.itemRefs)
|
||||
if !reflect.DeepEqual(source.paths, expectedPaths) {
|
||||
t.Fatalf("read paths=%v\nwant=%v", source.paths, expectedPaths)
|
||||
}
|
||||
if got := catalog.DefinitionIDs(); !reflect.DeepEqual(got, currentAntonRewardDefinitionOrder[:]) {
|
||||
t.Fatalf("definition ids=%v", got)
|
||||
}
|
||||
if snapshot := catalog.Snapshot(); snapshot != (antonRewardPVFCatalogSnapshot{
|
||||
Definitions: 12,
|
||||
DefinitionEntries: 13,
|
||||
PhasePools: 6,
|
||||
PhasePoolEntries: 13,
|
||||
}) {
|
||||
t.Fatalf("snapshot=%+v", snapshot)
|
||||
}
|
||||
|
||||
for index, definitionItemID := range currentAntonRewardDefinitionOrder {
|
||||
definition, found := catalog.Definition(definitionItemID)
|
||||
if !found || definition.DefinitionItemID != definitionItemID || definition.Kind != dungeonDropItemStackable || definition.PVFPath != expectedPaths[index] || definition.TotalWeight == 0 || len(definition.Entries) == 0 {
|
||||
t.Fatalf("definition=%d got=%+v found=%t", definitionItemID, definition, found)
|
||||
}
|
||||
for _, entry := range definition.Entries {
|
||||
if entry.ItemID == definitionItemID {
|
||||
t.Fatalf("definition item leaked into its final table: %+v", definition)
|
||||
}
|
||||
}
|
||||
resolved, resolveErr := catalog.ResolveDefinition(definitionItemID, &antonRewardPVFSequenceRNG{})
|
||||
if resolveErr != nil {
|
||||
t.Fatalf("resolve definition=%d: %v", definitionItemID, resolveErr)
|
||||
}
|
||||
if resolved.ItemID != finalByDefinition[definitionItemID] || resolved.Count == 0 || resolved.DefinitionItemID != definitionItemID || resolved.ItemPVFPath == "" {
|
||||
t.Fatalf("definition=%d resolved=%+v", definitionItemID, resolved)
|
||||
}
|
||||
}
|
||||
|
||||
phaseOneSquad, found := catalog.Pool(0, " SQUAD_ITEM ")
|
||||
if !found || phaseOneSquad.TotalWeight != 100 || !reflect.DeepEqual(phaseOneSquad.Entries, []antonRewardPVFPoolEntry{
|
||||
{DefinitionItemID: 10157773, Rank: -1, Weight: 50, Flag: 0},
|
||||
{DefinitionItemID: 10157775, Rank: -1, Weight: 1, Flag: 1},
|
||||
{DefinitionItemID: 10157776, Rank: -1, Weight: 1, Flag: 1},
|
||||
{DefinitionItemID: 10157777, Rank: -1, Weight: 48, Flag: 1},
|
||||
}) {
|
||||
t.Fatalf("phase one squad=%+v found=%t", phaseOneSquad, found)
|
||||
}
|
||||
phaseTwoSquad, found := catalog.Pool(1, "squad_item")
|
||||
if !found || phaseTwoSquad.TotalWeight != 1000 || !reflect.DeepEqual(phaseTwoSquad.Entries, []antonRewardPVFPoolEntry{
|
||||
{DefinitionItemID: 10161936, Rank: -1, Weight: 775, Flag: 0},
|
||||
{DefinitionItemID: 10157783, Rank: -1, Weight: 145, Flag: 1},
|
||||
{DefinitionItemID: 10157784, Rank: -1, Weight: 50, Flag: 1},
|
||||
{DefinitionItemID: 10157785, Rank: -1, Weight: 25, Flag: 1},
|
||||
{DefinitionItemID: 10157786, Rank: -1, Weight: 5, Flag: 1},
|
||||
}) {
|
||||
t.Fatalf("phase two squad=%+v found=%t", phaseTwoSquad, found)
|
||||
}
|
||||
if party, ok := catalog.Pool(0, "party_card"); !ok || len(party.Entries) != 1 || party.Entries[0].DefinitionItemID != 10094735 {
|
||||
t.Fatalf("phase one party=%+v found=%t", party, ok)
|
||||
}
|
||||
if pcroom, ok := catalog.Pool(1, "pcroom_card"); !ok || len(pcroom.Entries) != 1 || pcroom.Entries[0].DefinitionItemID != 10094733 {
|
||||
t.Fatalf("phase two pcroom=%+v found=%t", pcroom, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAntonRewardPVFPoolsIncludeSingleModeAfterTeamPhases(t *testing.T) {
|
||||
config := antonRewardPVFTestConfig()
|
||||
config.SingleMode = &dnfraid.AntonSingleModeConfig{Phase: dnfraid.AntonPhase{Rewards: []dnfraid.AntonStateReward{
|
||||
{Kind: "party_card", Rank: -1, Weight: 100, DefinitionItemID: 10164076},
|
||||
{Kind: "squad_item", Rank: -1, Weight: 900, DefinitionItemID: 10164077},
|
||||
}}}
|
||||
pools, _, err := buildAntonRewardPVFPools(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pool, found := pools[antonRewardPVFPoolKey{phaseIndex: len(config.Phases), kind: "party_card"}]; !found || len(pool.Entries) != 1 || pool.Entries[0].DefinitionItemID != 10164076 {
|
||||
t.Fatalf("single party pool=%+v found=%t", pool, found)
|
||||
}
|
||||
if pool, found := pools[antonRewardPVFPoolKey{phaseIndex: len(config.Phases), kind: "squad_item"}]; !found || len(pool.Entries) != 1 || pool.Entries[0].DefinitionItemID != 10164077 {
|
||||
t.Fatalf("single squad pool=%+v found=%t", pool, found)
|
||||
}
|
||||
if got := antonRewardDefinitionOrder(config); len(got) != len(currentAntonRewardDefinitionOrder)+2 || got[len(got)-2] != 10164076 || got[len(got)-1] != 10164077 {
|
||||
t.Fatalf("single definition order=%v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAntonRewardPVFWeightedResolveUsesInjectedRNGAndOneNestedDefinition(t *testing.T) {
|
||||
items, source, finalByDefinition := newAntonRewardPVFTestItems(t)
|
||||
outerPath := mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)
|
||||
nestedPath := mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094733)
|
||||
firstFinal := finalByDefinition[10094733]
|
||||
secondFinal := uint32(29000001)
|
||||
items.itemRefs[secondFinal] = dungeonDropItemReference{kind: dungeonDropItemEquipment, path: "anton_reward/second_nested.equ"}
|
||||
source.documents[outerPath] = "[int data]\n10094733 1 2\n[/int data]\n"
|
||||
source.documents[nestedPath] = fmt.Sprintf("[int data]\n%d 2 3 %d 3 4\n[/int data]\n", firstFinal, secondFinal)
|
||||
|
||||
catalog, err := newAntonRewardPVFCatalog(items, antonRewardPVFTestConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if snapshot := catalog.Snapshot(); snapshot.NestedReferences != 1 {
|
||||
t.Fatalf("snapshot=%+v", snapshot)
|
||||
}
|
||||
first, err := catalog.ResolveDefinition(10094735, &antonRewardPVFSequenceRNG{values: []uint64{0, 0}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first.DefinitionItemID != 10094735 || first.NestedDefinitionItemID != 10094733 || first.ItemID != firstFinal || first.Count != 6 || first.ItemKind != dungeonDropItemEquipment {
|
||||
t.Fatalf("first nested resolve=%+v", first)
|
||||
}
|
||||
second, err := catalog.ResolveDefinition(10094735, &antonRewardPVFSequenceRNG{values: []uint64{0, 2}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if second.NestedDefinitionItemID != 10094733 || second.ItemID != secondFinal || second.Count != 8 || second.ItemKind != dungeonDropItemEquipment || second.ItemPVFPath != "equipment/anton_reward/second_nested.equ" {
|
||||
t.Fatalf("second nested resolve=%+v", second)
|
||||
}
|
||||
|
||||
selected, err := catalog.ResolvePool(0, "squad_item", &antonRewardPVFSequenceRNG{values: []uint64{50, 0}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if selected.DefinitionItemID != 10157775 || selected.PoolWeight != 1 || selected.Flag != 1 || selected.PhaseIndex != 0 || selected.PoolKind != "squad_item" {
|
||||
t.Fatalf("phase pool roll 50=%+v", selected)
|
||||
}
|
||||
selected, err = catalog.ResolvePool(0, "squad_item", &antonRewardPVFSequenceRNG{values: []uint64{52, 0}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if selected.DefinitionItemID != 10157777 || selected.PoolWeight != 48 {
|
||||
t.Fatalf("phase pool roll 52=%+v", selected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaidRewardPVFDirectBoosterDefinitionGrantsTheBoxItself(t *testing.T) {
|
||||
items, source, _ := newAntonRewardPVFTestItems(t)
|
||||
definitionItemID := currentAntonRewardDefinitionOrder[0]
|
||||
definitionPath := mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, definitionItemID)
|
||||
// Luke's current reward definitions are booster boxes with one or more
|
||||
// [etc] groups. Those rows describe what opening the box may contain; the
|
||||
// Raid reward itself is the box and must not pre-open that second RNG layer.
|
||||
source.documents[definitionPath] = "[booster]\n[booster info]\n[etc]\n1 20000001 1000 3\n[etc]\n1 20000002 1000 1\n"
|
||||
|
||||
catalog, err := newAntonRewardPVFCatalog(items, antonRewardPVFTestConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
definition, found := catalog.Definition(definitionItemID)
|
||||
if !found || !definition.DirectFinal || len(definition.Entries) != 1 ||
|
||||
definition.Entries[0] != (antonRewardPVFEntry{ItemID: definitionItemID, Weight: 1, Count: 1}) {
|
||||
t.Fatalf("direct booster definition=%+v found=%t", definition, found)
|
||||
}
|
||||
resolved, err := catalog.ResolveDefinition(definitionItemID, &antonRewardPVFSequenceRNG{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolved.ItemID != definitionItemID || resolved.Count != 1 || resolved.ItemPVFPath != definition.PVFPath {
|
||||
t.Fatalf("direct booster resolved=%+v definition=%+v", resolved, definition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAntonRewardPVFResolveFinalItemReadsOnlySelectedDocumentAndCachesMetadata(t *testing.T) {
|
||||
items, source, finalByDefinition := newAntonRewardPVFTestItems(t)
|
||||
catalog, err := newAntonRewardPVFCatalog(items, antonRewardPVFTestConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
finalItemID := finalByDefinition[10094735]
|
||||
reference := items.itemRefs[finalItemID]
|
||||
finalPath, err := antonRewardPVFReferencePath(reference)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source.documents[finalPath] = "[stackable type]\n`[material]`\n[stack limit]\n999\n"
|
||||
source.paths = nil
|
||||
|
||||
first, err := catalog.ResolveFinalItem(finalItemID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := catalog.ResolveFinalItem(finalItemID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(first, second) || first.ItemID != finalItemID || first.Kind != dungeonDropItemStackable || first.PVFPath != finalPath || first.StackableType != "[material]" || first.StackLimit != 999 || first.SlotStart != 121 || first.SlotEnd != 176 {
|
||||
t.Fatalf("final metadata first=%+v second=%+v", first, second)
|
||||
}
|
||||
if !reflect.DeepEqual(source.paths, []string{finalPath}) {
|
||||
t.Fatalf("final item reads=%v want exact cached path %q", source.paths, finalPath)
|
||||
}
|
||||
if _, err := catalog.ResolveFinalItem(10094735); !errors.Is(err, errAntonRewardPVFNestingInvalid) {
|
||||
t.Fatalf("definition-as-final error=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAntonRewardPVFCatalogRejectsMalformedDefinitionTablesAndUnresolvedFinals(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*testing.T, *pvfDungeonDropCatalog, *antonRewardPVFRecordingSource)
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "missing int data",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = "[name]\n`missing`\n"
|
||||
},
|
||||
want: errAntonRewardPVFSectionShape,
|
||||
},
|
||||
{
|
||||
name: "duplicate int data",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
final := uint32(20000001)
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = fmt.Sprintf("[int data]\n%d 1 1\n[/int data]\n[int data]\n%d 1 1\n[/int data]\n", final, final)
|
||||
},
|
||||
want: errAntonRewardPVFSectionShape,
|
||||
},
|
||||
{
|
||||
name: "unclosed int data",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = "[int data]\n20000001 1 1\n"
|
||||
},
|
||||
want: errAntonRewardPVFSectionShape,
|
||||
},
|
||||
{
|
||||
name: "partial triple",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = "[int data]\n20000001 1\n[/int data]\n"
|
||||
},
|
||||
want: errAntonRewardPVFSectionShape,
|
||||
},
|
||||
{
|
||||
name: "non integer token",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = "[int data]\n20000001 1 `one`\n[/int data]\n"
|
||||
},
|
||||
want: errAntonRewardPVFSectionShape,
|
||||
},
|
||||
{
|
||||
name: "zero item",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = "[int data]\n0 1 1\n[/int data]\n"
|
||||
},
|
||||
want: errAntonRewardPVFValueInvalid,
|
||||
},
|
||||
{
|
||||
name: "zero weight",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = "[int data]\n20000001 0 1\n[/int data]\n"
|
||||
},
|
||||
want: errAntonRewardPVFValueInvalid,
|
||||
},
|
||||
{
|
||||
name: "zero count",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = "[int data]\n20000001 1 0\n[/int data]\n"
|
||||
},
|
||||
want: errAntonRewardPVFValueInvalid,
|
||||
},
|
||||
{
|
||||
name: "unresolved final item",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = "[int data]\n29999999 1 1\n[/int data]\n"
|
||||
},
|
||||
want: errAntonRewardPVFFinalItemUnresolved,
|
||||
},
|
||||
{
|
||||
name: "self reference",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = "[int data]\n10094735 1 1\n[/int data]\n"
|
||||
},
|
||||
want: errAntonRewardPVFNestingInvalid,
|
||||
},
|
||||
{
|
||||
name: "more than one nested definition",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = "[int data]\n10094733 1 1\n[/int data]\n"
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094733)] = "[int data]\n10157773 1 1\n[/int data]\n"
|
||||
},
|
||||
want: errAntonRewardPVFNestingInvalid,
|
||||
},
|
||||
{
|
||||
name: "nested count overflow",
|
||||
mutate: func(t *testing.T, items *pvfDungeonDropCatalog, source *antonRewardPVFRecordingSource) {
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094735)] = fmt.Sprintf("[int data]\n10094733 1 %d\n[/int data]\n", uint64(math.MaxUint32))
|
||||
final := uint32(20000002)
|
||||
source.documents[mustAntonRewardPVFTestDefinitionPath(t, items.itemRefs, 10094733)] = fmt.Sprintf("[int data]\n%d 1 2\n[/int data]\n", final)
|
||||
},
|
||||
want: errAntonRewardPVFValueInvalid,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
items, source, _ := newAntonRewardPVFTestItems(t)
|
||||
test.mutate(t, items, source)
|
||||
_, err := newAntonRewardPVFCatalog(items, antonRewardPVFTestConfig())
|
||||
if !errors.Is(err, test.want) {
|
||||
t.Fatalf("error=%v want errors.Is(%v)", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAntonRewardPVFCatalogRejectsIncompleteOrUnexpectedConfigAndInvalidResolve(t *testing.T) {
|
||||
if _, err := newAntonRewardPVFCatalog(nil, antonRewardPVFTestConfig()); !errors.Is(err, errAntonRewardPVFCatalogRequired) {
|
||||
t.Fatalf("nil catalog error=%v", err)
|
||||
}
|
||||
items, _, _ := newAntonRewardPVFTestItems(t)
|
||||
if _, err := newAntonRewardPVFCatalog(items, nil); !errors.Is(err, errAntonRewardPVFConfigRequired) {
|
||||
t.Fatalf("nil config error=%v", err)
|
||||
}
|
||||
|
||||
t.Run("missing current definition", func(t *testing.T) {
|
||||
config := antonRewardPVFTestConfig()
|
||||
config.Phases[1].Rewards = config.Phases[1].Rewards[:len(config.Phases[1].Rewards)-1]
|
||||
if _, err := newAntonRewardPVFCatalog(items, config); !errors.Is(err, errAntonRewardPVFDefinitionSet) {
|
||||
t.Fatalf("error=%v", err)
|
||||
}
|
||||
})
|
||||
t.Run("unexpected definition", func(t *testing.T) {
|
||||
config := antonRewardPVFTestConfig()
|
||||
config.Phases[0].Rewards[0].DefinitionItemID = 42
|
||||
if _, err := newAntonRewardPVFCatalog(items, config); !errors.Is(err, errAntonRewardPVFDefinitionSet) {
|
||||
t.Fatalf("error=%v", err)
|
||||
}
|
||||
})
|
||||
t.Run("nonpositive outer weight", func(t *testing.T) {
|
||||
config := antonRewardPVFTestConfig()
|
||||
config.Phases[0].Rewards[0].Weight = 0
|
||||
if _, err := newAntonRewardPVFCatalog(items, config); !errors.Is(err, errAntonRewardPVFValueInvalid) {
|
||||
t.Fatalf("error=%v", err)
|
||||
}
|
||||
})
|
||||
|
||||
catalog, err := newAntonRewardPVFCatalog(items, antonRewardPVFTestConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := catalog.ResolveDefinition(10094735, nil); !errors.Is(err, errAntonRewardPVFRNGRequired) {
|
||||
t.Fatalf("nil definition RNG error=%v", err)
|
||||
}
|
||||
if _, err := catalog.ResolveDefinition(42, &antonRewardPVFSequenceRNG{}); !errors.Is(err, errAntonRewardPVFDefinitionMissing) {
|
||||
t.Fatalf("unknown definition error=%v", err)
|
||||
}
|
||||
if _, err := catalog.ResolvePool(3, "squad_item", &antonRewardPVFSequenceRNG{}); !errors.Is(err, errAntonRewardPVFPoolUnresolved) {
|
||||
t.Fatalf("unknown pool error=%v", err)
|
||||
}
|
||||
if _, err := catalog.ResolvePool(0, "squad_item", nil); !errors.Is(err, errAntonRewardPVFRNGRequired) {
|
||||
t.Fatalf("nil pool RNG error=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealScriptPVFAntonRewardCatalogReadsOnlyCurrentDefinitionDocuments(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNF_ANTON_REWARD_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNF_ANTON_REWARD_REAL_PVF_SMOKE to verify current Anton reward definitions")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := dnfraid.LoadAntonConfig(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
itemRefs := antonRewardPVFRealItemRefs(t, archive)
|
||||
expectedPaths := make([]string, 0, len(antonRewardDefinitionOrder(config)))
|
||||
for _, definitionItemID := range antonRewardDefinitionOrder(config) {
|
||||
expectedPaths = append(expectedPaths, mustAntonRewardPVFTestDefinitionPath(t, itemRefs, definitionItemID))
|
||||
}
|
||||
allowed := make(map[string]struct{}, len(expectedPaths))
|
||||
for _, documentPath := range expectedPaths {
|
||||
allowed[documentPath] = struct{}{}
|
||||
}
|
||||
source := &antonRewardPVFRecordingSource{delegate: archive, allowed: allowed}
|
||||
items := &pvfDungeonDropCatalog{source: source, itemRefs: itemRefs}
|
||||
catalog, err := newAntonRewardPVFCatalog(items, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(source.paths, expectedPaths) {
|
||||
t.Fatalf("real definition reads=%v\nwant=%v", source.paths, expectedPaths)
|
||||
}
|
||||
if snapshot := catalog.Snapshot(); snapshot.Definitions != len(antonRewardDefinitionOrder(config)) ||
|
||||
snapshot.PhasePools != 9 || snapshot.DefinitionEntries < 1128 || snapshot.PhasePoolEntries != 22 {
|
||||
t.Fatalf("real snapshot=%+v", snapshot)
|
||||
}
|
||||
|
||||
wantShape := map[uint32]struct {
|
||||
entries int
|
||||
weight uint64
|
||||
}{
|
||||
10094735: {entries: 4, weight: 106},
|
||||
10094733: {entries: 1, weight: 100},
|
||||
10157773: {entries: 6, weight: 600},
|
||||
10157775: {entries: 130, weight: 13000},
|
||||
10157776: {entries: 19, weight: 1260},
|
||||
10157777: {entries: 907, weight: 67000},
|
||||
10157778: {entries: 6, weight: 600},
|
||||
10161936: {entries: 6, weight: 124},
|
||||
10157783: {entries: 1, weight: 100},
|
||||
10157784: {entries: 37, weight: 370},
|
||||
10157785: {entries: 10, weight: 750},
|
||||
10157786: {entries: 1, weight: 1000},
|
||||
}
|
||||
for _, definitionItemID := range currentAntonRewardDefinitionOrder {
|
||||
definition, found := catalog.Definition(definitionItemID)
|
||||
if !found || len(definition.Entries) == 0 || definition.TotalWeight == 0 {
|
||||
t.Fatalf("real definition=%d got=%+v found=%t", definitionItemID, definition, found)
|
||||
}
|
||||
if shape, pinned := wantShape[definitionItemID]; pinned && (len(definition.Entries) != shape.entries || definition.TotalWeight != shape.weight) {
|
||||
t.Fatalf("real definition=%d entries=%d weight=%d want=%+v", definitionItemID, len(definition.Entries), definition.TotalWeight, shape)
|
||||
}
|
||||
for entryIndex, entry := range definition.Entries {
|
||||
if _, isDefinition := catalog.definitions[entry.ItemID]; isDefinition {
|
||||
t.Fatalf("real definition=%d entry=%d points to definition=%d", definitionItemID, entryIndex, entry.ItemID)
|
||||
}
|
||||
if _, found := itemRefs[entry.ItemID]; !found {
|
||||
t.Fatalf("real definition=%d entry=%d final item=%d is not indexed", definitionItemID, entryIndex, entry.ItemID)
|
||||
}
|
||||
}
|
||||
t.Logf("anton reward definition=%d path=%s entries=%d total_weight=%d", definitionItemID, definition.PVFPath, len(definition.Entries), definition.TotalWeight)
|
||||
}
|
||||
if definition, _ := catalog.Definition(10094735); !reflect.DeepEqual(definition.Entries, []antonRewardPVFEntry{
|
||||
{ItemID: 10093974, Weight: 100, Count: 5},
|
||||
{ItemID: 10093974, Weight: 3, Count: 6},
|
||||
{ItemID: 10093974, Weight: 2, Count: 7},
|
||||
{ItemID: 10093974, Weight: 1, Count: 8},
|
||||
}) {
|
||||
t.Fatalf("real phase-one party card=%+v", definition.Entries)
|
||||
}
|
||||
if definition, _ := catalog.Definition(10161936); !reflect.DeepEqual(definition.Entries, []antonRewardPVFEntry{
|
||||
{ItemID: 10093974, Weight: 100, Count: 10},
|
||||
{ItemID: 10093974, Weight: 10, Count: 11},
|
||||
{ItemID: 10093974, Weight: 5, Count: 12},
|
||||
{ItemID: 10093974, Weight: 4, Count: 13},
|
||||
{ItemID: 10093974, Weight: 3, Count: 14},
|
||||
{ItemID: 10093974, Weight: 2, Count: 15},
|
||||
}) {
|
||||
t.Fatalf("real phase-two party definition=%+v", definition.Entries)
|
||||
}
|
||||
}
|
||||
|
||||
type antonRewardPVFRecordingSource struct {
|
||||
documents map[string]string
|
||||
delegate dnfpvf.Source
|
||||
allowed map[string]struct{}
|
||||
paths []string
|
||||
}
|
||||
|
||||
func (source *antonRewardPVFRecordingSource) ReadText(documentPath string) (string, error) {
|
||||
source.paths = append(source.paths, documentPath)
|
||||
if source.allowed != nil {
|
||||
if _, allowed := source.allowed[documentPath]; !allowed {
|
||||
return "", fmt.Errorf("unexpected Anton reward PVF read %q", documentPath)
|
||||
}
|
||||
}
|
||||
if source.documents != nil {
|
||||
text, found := source.documents[documentPath]
|
||||
if !found {
|
||||
return "", fmt.Errorf("Anton reward PVF test document missing %q", documentPath)
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
if source.delegate == nil {
|
||||
return "", fmt.Errorf("Anton reward PVF source missing %q", documentPath)
|
||||
}
|
||||
return source.delegate.ReadText(documentPath)
|
||||
}
|
||||
|
||||
type antonRewardPVFSequenceRNG struct {
|
||||
values []uint64
|
||||
index int
|
||||
}
|
||||
|
||||
func (rng *antonRewardPVFSequenceRNG) Uint64() uint64 {
|
||||
if rng == nil || len(rng.values) == 0 {
|
||||
return 0
|
||||
}
|
||||
if rng.index >= len(rng.values) {
|
||||
return rng.values[len(rng.values)-1]
|
||||
}
|
||||
value := rng.values[rng.index]
|
||||
rng.index++
|
||||
return value
|
||||
}
|
||||
|
||||
func newAntonRewardPVFTestItems(t *testing.T) (*pvfDungeonDropCatalog, *antonRewardPVFRecordingSource, map[uint32]uint32) {
|
||||
t.Helper()
|
||||
source := &antonRewardPVFRecordingSource{documents: make(map[string]string)}
|
||||
itemRefs := make(map[uint32]dungeonDropItemReference, len(currentAntonRewardDefinitionOrder)*2+1)
|
||||
finalByDefinition := make(map[uint32]uint32, len(currentAntonRewardDefinitionOrder))
|
||||
for index, definitionItemID := range currentAntonRewardDefinitionOrder {
|
||||
itemRefs[definitionItemID] = dungeonDropItemReference{
|
||||
kind: dungeonDropItemStackable,
|
||||
path: fmt.Sprintf("anton_reward/%d.stk", definitionItemID),
|
||||
}
|
||||
finalItemID := uint32(20000001 + index)
|
||||
finalByDefinition[definitionItemID] = finalItemID
|
||||
kind := dungeonDropItemStackable
|
||||
extension := "stk"
|
||||
if index%2 == 1 {
|
||||
kind = dungeonDropItemEquipment
|
||||
extension = "equ"
|
||||
}
|
||||
itemRefs[finalItemID] = dungeonDropItemReference{
|
||||
kind: kind,
|
||||
path: fmt.Sprintf("anton_reward/final_%d.%s", finalItemID, extension),
|
||||
}
|
||||
}
|
||||
items := &pvfDungeonDropCatalog{source: source, itemRefs: itemRefs}
|
||||
for index, definitionItemID := range currentAntonRewardDefinitionOrder {
|
||||
documentPath := mustAntonRewardPVFTestDefinitionPath(t, itemRefs, definitionItemID)
|
||||
finalItemID := finalByDefinition[definitionItemID]
|
||||
if index == 0 {
|
||||
extraFinal := uint32(29000000)
|
||||
itemRefs[extraFinal] = dungeonDropItemReference{kind: dungeonDropItemStackable, path: "anton_reward/extra_final.stk"}
|
||||
source.documents[documentPath] = fmt.Sprintf("[int data]\n%d 2 5 %d 3 7\n[/int data]\n", finalItemID, extraFinal)
|
||||
continue
|
||||
}
|
||||
source.documents[documentPath] = fmt.Sprintf("[int data]\n%d %d %d\n[/int data]\n", finalItemID, index+1, index+1)
|
||||
}
|
||||
return items, source, finalByDefinition
|
||||
}
|
||||
|
||||
func antonRewardPVFTestConfig() *dnfraid.AntonConfig {
|
||||
return &dnfraid.AntonConfig{Phases: []dnfraid.AntonPhase{
|
||||
{Rewards: []dnfraid.AntonStateReward{
|
||||
{Kind: "party_card", Rank: -1, Weight: 100, DefinitionItemID: 10094735, Flag: 0},
|
||||
{Kind: "pcroom_card", Rank: -1, Weight: 100, DefinitionItemID: 10094733, Flag: 0},
|
||||
{Kind: "squad_item", Rank: -1, Weight: 50, DefinitionItemID: 10157773, Flag: 0},
|
||||
{Kind: "squad_item", Rank: -1, Weight: 1, DefinitionItemID: 10157775, Flag: 1},
|
||||
{Kind: "squad_item", Rank: -1, Weight: 1, DefinitionItemID: 10157776, Flag: 1},
|
||||
{Kind: "squad_item", Rank: -1, Weight: 48, DefinitionItemID: 10157777, Flag: 1},
|
||||
}},
|
||||
{Rewards: []dnfraid.AntonStateReward{
|
||||
{Kind: "party_card", Rank: -1, Weight: 100, DefinitionItemID: 10157778, Flag: 0},
|
||||
{Kind: "pcroom_card", Rank: -1, Weight: 100, DefinitionItemID: 10094733, Flag: 0},
|
||||
{Kind: "squad_item", Rank: -1, Weight: 775, DefinitionItemID: 10161936, Flag: 0},
|
||||
{Kind: "squad_item", Rank: -1, Weight: 145, DefinitionItemID: 10157783, Flag: 1},
|
||||
{Kind: "squad_item", Rank: -1, Weight: 50, DefinitionItemID: 10157784, Flag: 1},
|
||||
{Kind: "squad_item", Rank: -1, Weight: 25, DefinitionItemID: 10157785, Flag: 1},
|
||||
{Kind: "squad_item", Rank: -1, Weight: 5, DefinitionItemID: 10157786, Flag: 1},
|
||||
}},
|
||||
}}
|
||||
}
|
||||
|
||||
func antonRewardPVFTestDefinitionPaths(t *testing.T, itemRefs map[uint32]dungeonDropItemReference) []string {
|
||||
t.Helper()
|
||||
paths := make([]string, 0, len(currentAntonRewardDefinitionOrder))
|
||||
for _, definitionItemID := range currentAntonRewardDefinitionOrder {
|
||||
paths = append(paths, mustAntonRewardPVFTestDefinitionPath(t, itemRefs, definitionItemID))
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func mustAntonRewardPVFTestDefinitionPath(t *testing.T, itemRefs map[uint32]dungeonDropItemReference, definitionItemID uint32) string {
|
||||
t.Helper()
|
||||
reference, found := itemRefs[definitionItemID]
|
||||
if !found {
|
||||
t.Fatalf("definition item %d is not indexed", definitionItemID)
|
||||
}
|
||||
documentPath, err := antonRewardPVFReferencePath(reference)
|
||||
if err != nil {
|
||||
t.Fatalf("definition item %d path: %v", definitionItemID, err)
|
||||
}
|
||||
return documentPath
|
||||
}
|
||||
|
||||
func antonRewardPVFRealItemRefs(t *testing.T, source dnfpvf.Source) map[uint32]dungeonDropItemReference {
|
||||
t.Helper()
|
||||
stackablePaths, _, err := loadDungeonDropList(source, dungeonDropStackableList)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
equipmentPaths, _, err := loadDungeonDropList(source, dungeonDropEquipmentList)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
itemRefs := make(map[uint32]dungeonDropItemReference, len(stackablePaths)+len(equipmentPaths))
|
||||
for itemID, itemPath := range stackablePaths {
|
||||
if itemID > 0 && itemID <= math.MaxUint32 {
|
||||
itemRefs[uint32(itemID)] = dungeonDropItemReference{kind: dungeonDropItemStackable, path: itemPath}
|
||||
}
|
||||
}
|
||||
for itemID, itemPath := range equipmentPaths {
|
||||
if itemID <= 0 || itemID > math.MaxUint32 {
|
||||
continue
|
||||
}
|
||||
if _, found := itemRefs[uint32(itemID)]; !found {
|
||||
itemRefs[uint32(itemID)] = dungeonDropItemReference{kind: dungeonDropItemEquipment, path: itemPath}
|
||||
}
|
||||
}
|
||||
return itemRefs
|
||||
}
|
||||
Reference in new issue
Block a user