服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
974 lines
40 KiB
Go
974 lines
40 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"math"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
|
platformpvf "longheng.io/server/internal/platform/pvf"
|
|
)
|
|
|
|
func TestDecodeCurrentEpicProductionStartRequest(t *testing.T) {
|
|
body := make([]byte, currentEpicProductionStartRequestSize)
|
|
binary.LittleEndian.PutUint32(body, 116010124)
|
|
target, err := decodeCurrentEpicProductionStartRequest(body)
|
|
if err != nil || target != 116010124 {
|
|
t.Fatalf("decode target=%d err=%v", target, err)
|
|
}
|
|
for _, invalid := range [][]byte{nil, make([]byte, 3), make([]byte, 5), make([]byte, 4)} {
|
|
if _, err := decodeCurrentEpicProductionStartRequest(invalid); err == nil {
|
|
t.Fatalf("invalid body accepted: %x", invalid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseCurrentEpicProductionCatalogPreservesJobTargets(t *testing.T) {
|
|
source := initialEquipmentMemSource{currentEpicProductionPVFPath: `
|
|
[level limit]
|
|
55
|
|
[weekly limit]
|
|
7
|
|
[max charge point]
|
|
1380000
|
|
[must need item]
|
|
10163135 1
|
|
[need item type change]
|
|
10158124 30 10163135 100
|
|
[/need item type change]
|
|
[process material need count]
|
|
1
|
|
[big chance rate]
|
|
80000
|
|
[big chance max rate]
|
|
1000000
|
|
[big chance multiply]
|
|
2
|
|
[production item]
|
|
[info]
|
|
[job]
|
|
` + "`[gun blader]`" + `
|
|
[indexes]
|
|
116000143 116010124
|
|
[indexes]
|
|
[/info]
|
|
[info]
|
|
[job]
|
|
` + "`[swordman]`" + `
|
|
[indexes]
|
|
101000747
|
|
[indexes]
|
|
[/info]
|
|
[/production item]
|
|
[liquid list]
|
|
[item index]
|
|
10163134
|
|
[group key]
|
|
0
|
|
[get point]
|
|
20000
|
|
[need item list]
|
|
10163136
|
|
[/need item list]
|
|
[need item count]
|
|
1
|
|
[min make item count]
|
|
1
|
|
[max make item count]
|
|
1
|
|
[/item index]
|
|
[item index]
|
|
10163131
|
|
[group key]
|
|
1
|
|
[get point]
|
|
2000
|
|
[need item list]
|
|
3137
|
|
[/need item list]
|
|
[need item count]
|
|
50
|
|
[min make item count]
|
|
1
|
|
[max make item count]
|
|
1
|
|
[/item index]
|
|
[/liquid list]
|
|
`}
|
|
catalog, err := parseCurrentEpicProductionCatalog(source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !catalog.allows("[gun blader]", 116010124) || catalog.allows("[swordman]", 116010124) || !catalog.allows("[swordman]", 101000747) {
|
|
t.Fatalf("unexpected target map: %+v", catalog.targetsByJob)
|
|
}
|
|
if catalog.changeMaterials[10158124] != 30 || catalog.changeMaterials[10163135] != 100 {
|
|
t.Fatalf("unexpected change materials: %+v", catalog.changeMaterials)
|
|
}
|
|
}
|
|
|
|
func TestValidateCurrentEpicProductionTargetRequiresPVFListAndAbility(t *testing.T) {
|
|
const targetID = uint32(116010124)
|
|
source := dungeonDropCatalogTestSource{
|
|
dungeonDropStackableList: "",
|
|
dungeonDropEquipmentList: "116010124 `character/gunBlader/weapon/sblade/116010124.equ`",
|
|
"monster/monster.lst": "",
|
|
"equipment/character/gunBlader/weapon/sblade/116010124.equ": `
|
|
[equipment type]
|
|
` + "`[weapon]` 17" + `
|
|
[custom ability type]
|
|
` + "`epic production`" + `
|
|
`,
|
|
}
|
|
items, err := newPVFDungeonDropCatalog(source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog := ¤tEpicProductionCatalog{targetsByJob: map[string]map[uint32]struct{}{
|
|
"[gun blader]": {targetID: {}},
|
|
}}
|
|
definition, err := validateCurrentEpicProductionTarget(source, items, catalog, "[gun blader]", targetID)
|
|
if err != nil || definition.ItemID != targetID {
|
|
t.Fatalf("validate definition=%+v err=%v", definition, err)
|
|
}
|
|
if _, err := validateCurrentEpicProductionTarget(source, items, catalog, "[swordman]", targetID); err == nil {
|
|
t.Fatal("cross-job production target accepted")
|
|
}
|
|
}
|
|
|
|
func TestValidateCurrentEpicProductionCompoundAcceptsPVFTypeOnlyAdvancedCatalyst(t *testing.T) {
|
|
source := dungeonDropCatalogTestSource{
|
|
"monster/monster.lst": "",
|
|
dungeonDropStackableList: "10163132 `catalyst/advanced.stk`\n10099774 `material/unique_soul.stk`\n",
|
|
dungeonDropEquipmentList: "700 `weapon/unique85.equ`\n701 `weapon/epic85.equ`\n",
|
|
"stackable/catalyst/advanced.stk": "[stackable type] `[material]`\n[stack limit] 1000\n",
|
|
"stackable/material/unique_soul.stk": "[stackable type] `[material]`\n[stack limit] 1000\n",
|
|
"equipment/weapon/unique85.equ": "[equipment type] `[weapon]`\n[minimum level] 85\n[rarity] 3\n[durability] 50\n",
|
|
"equipment/weapon/epic85.equ": "[equipment type] `[weapon]`\n[minimum level] 85\n[rarity] 4\n[durability] 50\n",
|
|
}
|
|
items, err := newPVFDungeonDropCatalog(source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recipe := currentEpicProductionCatalyst{
|
|
ItemID: 10163132, GroupKey: 0, LevelLimit: 85,
|
|
NeedItemType: "equipment", NeedItemParts: "weapon", NeedItemGrade: "unique",
|
|
NeedItemMinLevel: 70, NeedItemMaxLevel: 85, NeedCount: 1,
|
|
MustNeedItemID: 10099774, MustNeedItemCount: 1,
|
|
MinMakeCount: 3, MaxMakeCount: 4, IsSeal: true,
|
|
}
|
|
catalog := ¤tEpicProductionCatalog{catalysts: map[uint32]currentEpicProductionCatalyst{10163132: recipe}}
|
|
request := currentEpicProductionCompoundRequest{
|
|
CatalystItemID: 10163132,
|
|
Materials: []currentEpicProductionCompoundMaterial{{ItemID: 700, Slot: 10, Count: 1}},
|
|
}
|
|
plan, err := validateCurrentEpicProductionCompoundPlan(source, items, catalog, request)
|
|
if err != nil || plan.Contribution != 1 || plan.Recipe.ItemID != 10163132 {
|
|
t.Fatalf("advanced catalyst plan=%+v err=%v", plan, err)
|
|
}
|
|
request.Materials[0].ItemID = 701
|
|
if _, err := validateCurrentEpicProductionCompoundPlan(source, items, catalog, request); !errors.Is(err, errCurrentEpicProductionMaterial) {
|
|
t.Fatalf("advanced catalyst accepted epic input: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStartCurrentEpicProductionPersistsAndIsIdempotent(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
character := dnfrepo.CharacterRecord{CharacterID: "2", AccountID: "account-1", Stats: map[string]int64{}}
|
|
if err := repositories.Character.Save(ctx, character); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
changed, err := startCurrentEpicProduction(ctx, repositories.Character, "account-1", "2", 116010124)
|
|
if err != nil || !changed {
|
|
t.Fatalf("first start changed=%t err=%v", changed, err)
|
|
}
|
|
saved, found, err := repositories.Character.Load(ctx, "2")
|
|
if err != nil || !found || saved.Stats[currentEpicProductionTargetStat] != 116010124 {
|
|
t.Fatalf("saved=%+v found=%t err=%v", saved.Stats, found, err)
|
|
}
|
|
changed, err = startCurrentEpicProduction(ctx, repositories.Character, "account-1", "2", 116010124)
|
|
if err != nil || changed {
|
|
t.Fatalf("idempotent retry changed=%t err=%v", changed, err)
|
|
}
|
|
if _, err := startCurrentEpicProduction(ctx, repositories.Character, "account-1", "2", 116020124); !errors.Is(err, errCurrentEpicProductionAlreadyActive) {
|
|
t.Fatalf("different active target err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentEpicProductionStartSuccessBody(t *testing.T) {
|
|
body := buildCurrentEpicProductionStartSuccessBody(116010124)
|
|
if len(body) != 8 || binary.LittleEndian.Uint32(body[0:4]) != 116010124 || binary.LittleEndian.Uint32(body[4:8]) != 0 {
|
|
t.Fatalf("success body=%x", body)
|
|
}
|
|
}
|
|
|
|
func TestDecodeCurrentEpicProductionProcessRequest(t *testing.T) {
|
|
body := []byte{
|
|
3, 0,
|
|
0xbe, 0x13, 0x9b, 0x00, 0x7e, 0, 0, 0,
|
|
0xbb, 0x13, 0x9b, 0x00, 0x7b, 0, 0, 0,
|
|
0xba, 0x13, 0x9b, 0x00, 0x7a, 0, 0, 0,
|
|
}
|
|
request, err := decodeCurrentEpicProductionProcessRequest(body)
|
|
if err != nil || len(request.Materials) != 3 {
|
|
t.Fatalf("decode request=%+v err=%v", request, err)
|
|
}
|
|
want := []currentEpicProductionProcessMaterial{{ItemID: 10163134, Slot: 126}, {ItemID: 10163131, Slot: 123}, {ItemID: 10163130, Slot: 122}}
|
|
for index := range want {
|
|
if request.Materials[index] != want[index] {
|
|
t.Fatalf("material[%d]=%+v want=%+v", index, request.Materials[index], want[index])
|
|
}
|
|
}
|
|
for _, invalid := range [][]byte{nil, {0, 0}, body[:len(body)-1], append(append([]byte(nil), body...), 0)} {
|
|
if _, err := decodeCurrentEpicProductionProcessRequest(invalid); err == nil {
|
|
t.Fatalf("invalid process body accepted: %x", invalid)
|
|
}
|
|
}
|
|
forged := append([]byte(nil), body...)
|
|
binary.LittleEndian.PutUint32(forged[6:10], math.MaxUint32)
|
|
if _, err := decodeCurrentEpicProductionProcessRequest(forged); err == nil {
|
|
t.Fatal("forged u32 slot accepted")
|
|
}
|
|
}
|
|
|
|
func TestDecodeCurrentEpicProductionChangeRequestFromLivePacket(t *testing.T) {
|
|
body := []byte{
|
|
0xca, 0x7a, 0xea, 0x06,
|
|
0x02, 0x00,
|
|
0x2c, 0x00, 0x9b, 0x00, 0x6d, 0x01, 0x00, 0x00,
|
|
0xbf, 0x13, 0x9b, 0x00, 0x7f, 0x00, 0x00, 0x00,
|
|
}
|
|
request, err := decodeCurrentEpicProductionChangeRequest(body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if request.TargetItemID != 116030154 || len(request.Materials) != 2 ||
|
|
request.Materials[0] != (currentEpicProductionProcessMaterial{ItemID: 10158124, Slot: 365}) ||
|
|
request.Materials[1] != (currentEpicProductionProcessMaterial{ItemID: 10163135, Slot: 127}) {
|
|
t.Fatalf("request=%+v", request)
|
|
}
|
|
for _, invalid := range [][]byte{nil, body[:5], body[:len(body)-1], append(append([]byte(nil), body...), 0)} {
|
|
if _, err := decodeCurrentEpicProductionChangeRequest(invalid); err == nil {
|
|
t.Fatalf("invalid change body accepted: %x", invalid)
|
|
}
|
|
}
|
|
forged := append([]byte(nil), body...)
|
|
binary.LittleEndian.PutUint32(forged[10:14], math.MaxUint32)
|
|
if _, err := decodeCurrentEpicProductionChangeRequest(forged); err == nil {
|
|
t.Fatal("forged change slot accepted")
|
|
}
|
|
}
|
|
|
|
func currentEpicProductionTestCatalog() *currentEpicProductionCatalog {
|
|
return ¤tEpicProductionCatalog{
|
|
catalysts: map[uint32]currentEpicProductionCatalyst{
|
|
10163134: {ItemID: 10163134, GroupKey: 0, GetPoint: 20000},
|
|
10163133: {ItemID: 10163133, GroupKey: 0, GetPoint: 10000},
|
|
10163131: {ItemID: 10163131, GroupKey: 1, GetPoint: 2000},
|
|
10163130: {
|
|
ItemID: 10163130, GroupKey: 2, LevelLimit: 55, GetPoint: 1000,
|
|
NeedItemIDs: map[uint32]struct{}{10158124: {}}, NeedItemType: "material",
|
|
NeedCount: 75000, MinMakeCount: 1, MaxMakeCount: 1, IsMaterialPoint: true,
|
|
},
|
|
},
|
|
targetsByJob: map[string]map[uint32]struct{}{
|
|
"[gun blader]": {116010124: {}, 116020124: {}, 116030154: {}},
|
|
},
|
|
changeMaterials: map[uint32]uint32{10158124: 30, 10163135: 100},
|
|
materialPointsByRarity: map[int64]uint32{4: 500},
|
|
levelLimit: 55,
|
|
weeklyLimit: 7,
|
|
maxChargePoint: 1380000,
|
|
processMaterialNeedCount: 1,
|
|
requiredMaterialItemID: 10163135,
|
|
requiredMaterialItemCount: 1,
|
|
bigChanceRate: 0,
|
|
bigChanceMaxRate: 1000000,
|
|
bigChanceMultiply: 2,
|
|
}
|
|
}
|
|
|
|
func currentEpicProductionAbilityTestCatalogText() string {
|
|
return "[custom ability type] `epic production`\n" +
|
|
"[group]\n[key] `1`\n[indexes] 1 2 3 4\n[/indexes]\n[/group]\n" +
|
|
"[group]\n[key] `2`\n[indexes] 11 12 13 14 15 16 17 18 19 20 21 22\n[/indexes]\n[/group]\n" +
|
|
"[info]\n[index] 4\n[material]\n[grade] `D`\n[list] 3173 50 10163135 50 10099774 20\n[/list]\n[/material]\n[/info]\n" +
|
|
"[info]\n[index] 20\n[material]\n[grade] `D`\n[list] 3173 100 10163135 100 10099774 50\n[/list]\n[/material]\n[/info]\n"
|
|
}
|
|
|
|
func TestDecodeAndParseCurrentEpicProductionAbility(t *testing.T) {
|
|
request, err := decodeCurrentEpicProductionAbilityRequest([]byte{0x1a, 0x00, 0x00, 0x04})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if request != (currentEpicProductionAbilityRequest{Slot: 26, Category: 0, Option: 4}) {
|
|
t.Fatalf("request=%+v", request)
|
|
}
|
|
for _, invalid := range [][]byte{nil, {0x1a, 0x00, 0x00}, {0x1a, 0x00, 0x00, 0x04, 0x00}, {0xff, 0xff, 0x00, 0x04}} {
|
|
if _, err := decodeCurrentEpicProductionAbilityRequest(invalid); err == nil {
|
|
t.Fatalf("invalid ability body accepted: %x", invalid)
|
|
}
|
|
}
|
|
|
|
source := initialEquipmentMemSource{currentEpicProductionAbilityPVFPath: currentEpicProductionAbilityTestCatalogText()}
|
|
catalog, err := parseCurrentEpicProductionAbilityCatalog(source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recipe, err := currentEpicProductionAbilityRecipeFor(catalog, request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []currentEpicProductionAbilityMaterial{{ItemID: 3173, Count: 50}, {ItemID: 10163135, Count: 50}, {ItemID: 10099774, Count: 20}}
|
|
if !reflect.DeepEqual(recipe.Materials, want) {
|
|
t.Fatalf("recipe materials=%+v want=%+v", recipe.Materials, want)
|
|
}
|
|
request.Category, request.Option = 1, 20
|
|
if _, err := currentEpicProductionAbilityRecipeFor(catalog, request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request.Option = 4
|
|
if _, err := currentEpicProductionAbilityRecipeFor(catalog, request); err == nil {
|
|
t.Fatal("cross-category ability option accepted")
|
|
}
|
|
}
|
|
|
|
func TestCommitCurrentEpicProductionAbilityConsumesRecipeAndPersistsRawTail(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "2", AccountID: "account-1", Level: 55,
|
|
Stats: map[string]int64{currentEpicProductionTargetStat: 116010124},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{
|
|
AccountID: "account-1",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
dnfrepo.AccountSharedInventorySlotKey(363): {ItemID: 10099774, Count: 100},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
|
CharacterID: "2",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
"0:26": {ItemID: 116010124, Count: 1},
|
|
"0:30": {ItemID: 3173, Count: 20},
|
|
"0:31": {ItemID: 3173, Count: 40},
|
|
"0:32": {ItemID: 10163135, Count: 70},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := Service{options: options{accountID: "stale-local-profile"}, repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }}
|
|
session := &gameSession{accountID: "account-1", selectedCharacterID: 2}
|
|
recipe := currentEpicProductionAbilityRecipe{
|
|
Category: 0,
|
|
Option: 4,
|
|
Materials: []currentEpicProductionAbilityMaterial{
|
|
{ItemID: 3173, Count: 50},
|
|
{ItemID: 10163135, Count: 50},
|
|
{ItemID: 10099774, Count: 20},
|
|
},
|
|
}
|
|
request := currentEpicProductionAbilityRequest{Slot: 26, Category: 0, Option: 4}
|
|
result, err := service.commitCurrentEpicProductionAbility(ctx, session, recipe, request, 116010124, time.Now().UTC())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !result.Applied || len(result.Updates) != 5 {
|
|
t.Fatalf("result=%+v", result)
|
|
}
|
|
inventory, _, _ := repositories.Inventory.Load(ctx, "2")
|
|
if _, found := inventory.Slots["0:30"]; found || inventory.Slots["0:31"].Count != 10 || inventory.Slots["0:32"].Count != 20 {
|
|
t.Fatalf("ordinary materials=%+v", inventory.Slots)
|
|
}
|
|
target := inventory.Slots["0:26"]
|
|
if len(target.RawEntry) != currentItemListEntryWireSize || target.RawEntry[currentEpicProductionAbilityTailOffset] != 4 || target.Extra["tail_data_72"] != "0400000000" {
|
|
t.Fatalf("target raw tail=%x extra=%+v", target.RawEntry[currentEpicProductionAbilityTailOffset:], target.Extra)
|
|
}
|
|
account, _, _ := repositories.AccountInventory.Load(ctx, "account-1")
|
|
if account.Slots[dnfrepo.AccountSharedInventorySlotKey(363)].Count != 80 {
|
|
t.Fatalf("unique soul=%+v", account.Slots)
|
|
}
|
|
|
|
replay, err := service.commitCurrentEpicProductionAbility(ctx, session, recipe, request, 116010124, time.Now().UTC())
|
|
if err != nil || replay.Applied || len(replay.Updates) != 0 {
|
|
t.Fatalf("replay=%+v err=%v", replay, err)
|
|
}
|
|
inventory, _, _ = repositories.Inventory.Load(ctx, "2")
|
|
account, _, _ = repositories.AccountInventory.Load(ctx, "account-1")
|
|
if inventory.Slots["0:31"].Count != 10 || inventory.Slots["0:32"].Count != 20 || account.Slots[dnfrepo.AccountSharedInventorySlotKey(363)].Count != 80 {
|
|
t.Fatal("idempotent replay consumed materials")
|
|
}
|
|
|
|
inventory.Slots["0:31"] = dnfrepo.ItemStack{ItemID: 3173, Count: 100}
|
|
inventory.Slots["0:32"] = dnfrepo.ItemStack{ItemID: 10163135, Count: 100}
|
|
if err := repositories.Inventory.Save(ctx, inventory); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secondRecipe := currentEpicProductionAbilityRecipe{
|
|
Category: 1,
|
|
Option: 20,
|
|
Materials: []currentEpicProductionAbilityMaterial{
|
|
{ItemID: 3173, Count: 100},
|
|
{ItemID: 10163135, Count: 100},
|
|
{ItemID: 10099774, Count: 50},
|
|
},
|
|
}
|
|
secondRequest := currentEpicProductionAbilityRequest{Slot: 26, Category: 1, Option: 20}
|
|
secondResult, err := service.commitCurrentEpicProductionAbility(ctx, session, secondRecipe, secondRequest, 116010124, time.Now().UTC())
|
|
if err != nil || !secondResult.Applied {
|
|
t.Fatalf("independent category assignment result=%+v err=%v", secondResult, err)
|
|
}
|
|
inventory, _, _ = repositories.Inventory.Load(ctx, "2")
|
|
target = inventory.Slots["0:26"]
|
|
if target.RawEntry[currentEpicProductionAbilityTailOffset] != 4 || target.RawEntry[currentEpicProductionAbilityTailOffset+1] != 20 ||
|
|
target.Extra["tail_data_72"] != "0414000000" {
|
|
t.Fatalf("independent categories did not persist together: tail=%x extra=%+v", target.RawEntry[currentEpicProductionAbilityTailOffset:], target.Extra)
|
|
}
|
|
|
|
recipe.Option = 3
|
|
recipe.Materials[0].Count = 999
|
|
request.Option = 3
|
|
if _, err := service.commitCurrentEpicProductionAbility(ctx, session, recipe, request, 116010124, time.Now().UTC()); !errors.Is(err, errCurrentEpicProductionMaterial) {
|
|
t.Fatalf("insufficient ability material err=%v", err)
|
|
}
|
|
inventory, _, _ = repositories.Inventory.Load(ctx, "2")
|
|
if inventory.Slots["0:26"].RawEntry[currentEpicProductionAbilityTailOffset] != 4 ||
|
|
inventory.Slots["0:26"].RawEntry[currentEpicProductionAbilityTailOffset+1] != 20 {
|
|
t.Fatal("failed ability assignment changed target")
|
|
}
|
|
}
|
|
|
|
func TestCommitCurrentEpicProductionAbilityDoesNotLeakOneCharacterStateIntoAnother(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
for _, characterID := range []string{"2", "3"} {
|
|
stats := map[string]int64{currentEpicProductionTargetStat: 116010124}
|
|
if characterID == "3" {
|
|
// A second character can legitimately have a zero machine state while
|
|
// holding an already-produced weapon. Ability authorization comes from
|
|
// that character's actual slot plus the PVF-validated target, not another
|
|
// character's active production target.
|
|
stats = map[string]int64{}
|
|
}
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: characterID, AccountID: "account-1", Level: 90,
|
|
Stats: stats,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
|
CharacterID: characterID,
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
"0:26": {ItemID: 116010124, Count: 1},
|
|
"0:30": {ItemID: 3173, Count: 50},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
service := Service{options: options{accountID: "stale-local-profile"}, repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }}
|
|
session := &gameSession{accountID: "account-1", selectedCharacterID: 2}
|
|
recipe := currentEpicProductionAbilityRecipe{Category: 0, Option: 4, Materials: []currentEpicProductionAbilityMaterial{{ItemID: 3173, Count: 50}}}
|
|
request := currentEpicProductionAbilityRequest{Slot: 26, Category: 0, Option: 4}
|
|
first, err := service.commitCurrentEpicProductionAbility(ctx, session, recipe, request, 116010124, time.Now().UTC())
|
|
if err != nil || !first.Applied {
|
|
t.Fatalf("first character result=%+v err=%v", first, err)
|
|
}
|
|
session.selectedCharacterID = 3
|
|
target, err := service.loadCurrentEpicProductionAbilityTarget(ctx, session, request)
|
|
if err != nil || target.ItemID != 116010124 {
|
|
t.Fatalf("second character target=%+v err=%v", target, err)
|
|
}
|
|
second, err := service.commitCurrentEpicProductionAbility(ctx, session, recipe, request, 116010124, time.Now().UTC())
|
|
if err != nil || !second.Applied {
|
|
t.Fatalf("second character result=%+v err=%v", second, err)
|
|
}
|
|
for _, characterID := range []string{"2", "3"} {
|
|
inventory, found, err := repositories.Inventory.Load(ctx, characterID)
|
|
if err != nil || !found {
|
|
t.Fatalf("load character %s inventory found=%t err=%v", characterID, found, err)
|
|
}
|
|
if got := inventory.Slots["0:26"].RawEntry[currentEpicProductionAbilityTailOffset]; got != 4 {
|
|
t.Fatalf("character %s ability=%d", characterID, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommitCurrentEpicProductionChangeConsumesAccountAndCharacterMaterialsAtomically(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "2", AccountID: "account-1", Level: 25,
|
|
Stats: map[string]int64{
|
|
currentEpicProductionTargetStat: 116010124,
|
|
currentEpicProductionChargeStat: 23000,
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{
|
|
AccountID: "account-1",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
dnfrepo.AccountSharedInventorySlotKey(365): {ItemID: 10158124, Count: 9999},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
|
CharacterID: "2",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
"0:127": {ItemID: 10163135, Count: 300},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := Service{options: options{accountID: "stale-local-profile"}, repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }}
|
|
session := &gameSession{accountID: "account-1", selectedCharacterID: 2}
|
|
request := currentEpicProductionChangeRequest{
|
|
TargetItemID: 116030154,
|
|
Materials: []currentEpicProductionProcessMaterial{
|
|
{ItemID: 10158124, Slot: 365},
|
|
{ItemID: 10163135, Slot: 127},
|
|
},
|
|
}
|
|
result, err := service.commitCurrentEpicProductionChange(ctx, session, currentEpicProductionTestCatalog(), "[gun blader]", request, time.Now().UTC())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.TargetItemID != 116030154 || len(result.Updates) != 2 {
|
|
t.Fatalf("result=%+v", result)
|
|
}
|
|
character, _, _ := repositories.Character.Load(ctx, "2")
|
|
if character.Stats[currentEpicProductionTargetStat] != 116030154 || character.Stats[currentEpicProductionChargeStat] != 23000 {
|
|
t.Fatalf("character stats=%+v", character.Stats)
|
|
}
|
|
account, _, _ := repositories.AccountInventory.Load(ctx, "account-1")
|
|
if got := account.Slots[dnfrepo.AccountSharedInventorySlotKey(365)].Count; got != 9969 {
|
|
t.Fatalf("epic soul count=%d want=9969", got)
|
|
}
|
|
inventory, _, _ := repositories.Inventory.Load(ctx, "2")
|
|
if got := inventory.Slots["0:127"].Count; got != 200 {
|
|
t.Fatalf("carbon crystal count=%d want=200", got)
|
|
}
|
|
|
|
stack := inventory.Slots["0:127"]
|
|
stack.Count = 99
|
|
inventory.Slots["0:127"] = stack
|
|
if err := repositories.Inventory.Save(ctx, inventory); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request.TargetItemID = 116020124
|
|
if _, err := service.commitCurrentEpicProductionChange(ctx, session, currentEpicProductionTestCatalog(), "[gun blader]", request, time.Now().UTC()); !errors.Is(err, errCurrentEpicProductionMaterial) {
|
|
t.Fatalf("insufficient change err=%v", err)
|
|
}
|
|
character, _, _ = repositories.Character.Load(ctx, "2")
|
|
account, _, _ = repositories.AccountInventory.Load(ctx, "account-1")
|
|
inventory, _, _ = repositories.Inventory.Load(ctx, "2")
|
|
if character.Stats[currentEpicProductionTargetStat] != 116030154 ||
|
|
account.Slots[dnfrepo.AccountSharedInventorySlotKey(365)].Count != 9969 ||
|
|
inventory.Slots["0:127"].Count != 99 {
|
|
t.Fatalf("failed change mutated state: stats=%+v account=%+v inventory=%+v", character.Stats, account.Slots, inventory.Slots)
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentEpicProductionChangeSuccessBody(t *testing.T) {
|
|
body := buildCurrentEpicProductionChangeSuccessBody(116030154)
|
|
if len(body) != 4 || binary.LittleEndian.Uint32(body) != 116030154 {
|
|
t.Fatalf("change success body=%x", body)
|
|
}
|
|
}
|
|
|
|
func TestDecodeCurrentEpicProductionCompoundRequestFromLivePacket(t *testing.T) {
|
|
body := []byte{
|
|
0xba, 0x13, 0x9b, 0x00,
|
|
0x01, 0x00,
|
|
0x2c, 0x00, 0x9b, 0x00,
|
|
0x6d, 0x01, 0x00, 0x00,
|
|
0xc8, 0x00, 0x00, 0x00,
|
|
}
|
|
request, err := decodeCurrentEpicProductionCompoundRequest(body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if request.CatalystItemID != 10163130 || len(request.Materials) != 1 ||
|
|
request.Materials[0] != (currentEpicProductionCompoundMaterial{ItemID: 10158124, Slot: 365, Count: 200}) {
|
|
t.Fatalf("request=%+v", request)
|
|
}
|
|
for _, invalid := range [][]byte{nil, body[:5], body[:len(body)-1], append(append([]byte(nil), body...), 0)} {
|
|
if _, err := decodeCurrentEpicProductionCompoundRequest(invalid); err == nil {
|
|
t.Fatalf("invalid compound body accepted: %x", invalid)
|
|
}
|
|
}
|
|
forged := append([]byte(nil), body...)
|
|
binary.LittleEndian.PutUint32(forged[10:14], math.MaxUint32)
|
|
if _, err := decodeCurrentEpicProductionCompoundRequest(forged); err == nil {
|
|
t.Fatal("forged compound slot accepted")
|
|
}
|
|
}
|
|
|
|
func TestCommitCurrentEpicProductionCompoundConsumesSharedSoulGrantsCatalystAndPersistsCarry(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "2", AccountID: "account-1", Level: 55, Stats: map[string]int64{},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{
|
|
AccountID: "account-1",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
dnfrepo.AccountSharedInventorySlotKey(365): {ItemID: 10158124, Count: 9999},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "2", Slots: map[string]dnfrepo.ItemStack{}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := Service{options: options{accountID: "stale-local-profile"}, repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }}
|
|
session := &gameSession{accountID: "account-1", selectedCharacterID: 2}
|
|
request := currentEpicProductionCompoundRequest{
|
|
CatalystItemID: 10163130,
|
|
Materials: []currentEpicProductionCompoundMaterial{{ItemID: 10158124, Slot: 365, Count: 200}},
|
|
}
|
|
plan := currentEpicProductionCompoundPlan{
|
|
Recipe: currentEpicProductionTestCatalog().catalysts[10163130],
|
|
OutputDefinition: dungeonDropItemDefinition{
|
|
ItemID: 10163130, Kind: dungeonDropItemStackable, StackableType: "[material]", SlotStart: 97, SlotEnd: 120,
|
|
},
|
|
Contribution: 100000,
|
|
}
|
|
result, err := service.commitCurrentEpicProductionCompound(ctx, session, plan, request, time.Now().UTC())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.CatalystItemID != 10163130 || result.OutputCount != 1 || result.GroupSelector != 2 || result.CarryValue != 25000 {
|
|
t.Fatalf("result=%+v", result)
|
|
}
|
|
account, _, _ := repositories.AccountInventory.Load(ctx, "account-1")
|
|
if got := account.Slots[dnfrepo.AccountSharedInventorySlotKey(365)].Count; got != 9799 {
|
|
t.Fatalf("epic soul count=%d want=9799", got)
|
|
}
|
|
inventory, _, _ := repositories.Inventory.Load(ctx, "2")
|
|
if got := inventory.Slots["0:97"]; got.ItemID != 10163130 || got.Count != 1 {
|
|
t.Fatalf("catalyst stack=%+v", got)
|
|
}
|
|
character, _, _ := repositories.Character.Load(ctx, "2")
|
|
if got := character.Stats[currentEpicProductionCarryGroup2Stat]; got != 25000 {
|
|
t.Fatalf("carry=%d want=25000", got)
|
|
}
|
|
body := buildCurrentEpicProductionCompoundSuccessBody(result)
|
|
if len(body) != 9 || binary.LittleEndian.Uint32(body[:4]) != 1 || body[4] != 2 || binary.LittleEndian.Uint32(body[5:9]) != 25000 {
|
|
t.Fatalf("compound body=%x", body)
|
|
}
|
|
|
|
request.Materials[0].Count = 10000
|
|
plan.Contribution = 5000000
|
|
if _, err := service.commitCurrentEpicProductionCompound(ctx, session, plan, request, time.Now().UTC()); !errors.Is(err, errCurrentEpicProductionMaterial) {
|
|
t.Fatalf("insufficient compound err=%v", err)
|
|
}
|
|
account, _, _ = repositories.AccountInventory.Load(ctx, "account-1")
|
|
character, _, _ = repositories.Character.Load(ctx, "2")
|
|
if account.Slots[dnfrepo.AccountSharedInventorySlotKey(365)].Count != 9799 || character.Stats[currentEpicProductionCarryGroup2Stat] != 25000 {
|
|
t.Fatalf("failed compound mutated state: account=%+v stats=%+v", account.Slots, character.Stats)
|
|
}
|
|
}
|
|
|
|
func TestCommitCurrentEpicProductionCompoundKeepsMultiOutputBatch(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "2", AccountID: "account-1", Level: 55,
|
|
Stats: map[string]int64{currentEpicProductionCarryGroup2Stat: 60000},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{
|
|
AccountID: "account-1",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
dnfrepo.AccountSharedInventorySlotKey(365): {ItemID: 10158124, Count: 9999},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "2", Slots: map[string]dnfrepo.ItemStack{}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := Service{options: options{accountID: "stale-local-profile"}, repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }}
|
|
request := currentEpicProductionCompoundRequest{
|
|
CatalystItemID: 10163130,
|
|
Materials: []currentEpicProductionCompoundMaterial{{ItemID: 10158124, Slot: 365, Count: 200}},
|
|
}
|
|
plan := currentEpicProductionCompoundPlan{
|
|
Recipe: currentEpicProductionTestCatalog().catalysts[10163130],
|
|
OutputDefinition: dungeonDropItemDefinition{
|
|
ItemID: 10163130, Kind: dungeonDropItemStackable, StackableType: "[material]", SlotStart: 97, SlotEnd: 120,
|
|
},
|
|
Contribution: 100000,
|
|
}
|
|
result, err := service.commitCurrentEpicProductionCompound(ctx, &gameSession{accountID: "account-1", selectedCharacterID: 2}, plan, request, time.Now().UTC())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.OutputCount != 2 || result.CarryValue != 10000 {
|
|
t.Fatalf("result=%+v, want output=2 carry=10000", result)
|
|
}
|
|
}
|
|
|
|
func TestCommitCurrentEpicProductionProcessValidatesAndConsumesAtomically(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "2", AccountID: "account-1", Level: 55,
|
|
Stats: map[string]int64{currentEpicProductionTargetStat: 116010124},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "2", Slots: map[string]dnfrepo.ItemStack{
|
|
"0:126": {ItemID: 10163134, Count: 100},
|
|
"0:123": {ItemID: 10163131, Count: 100},
|
|
"0:122": {ItemID: 10163130, Count: 100},
|
|
"0:127": {ItemID: 10163135, Count: 300},
|
|
}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := Service{options: options{accountID: "stale-local-profile"}, repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }}
|
|
session := &gameSession{accountID: "account-1", selectedCharacterID: 2}
|
|
request := currentEpicProductionProcessRequest{Materials: []currentEpicProductionProcessMaterial{
|
|
{ItemID: 10163134, Slot: 126}, {ItemID: 10163131, Slot: 123}, {ItemID: 10163130, Slot: 122},
|
|
}}
|
|
now := time.Date(2026, 7, 27, 23, 4, 29, 0, time.UTC)
|
|
result, err := service.commitCurrentEpicProductionProcess(ctx, session, currentEpicProductionTestCatalog(), request, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.ChargePoint != 23000 || result.WeeklyCount != 1 || result.BigSuccess || len(result.Updates) != 4 {
|
|
t.Fatalf("result=%+v", result)
|
|
}
|
|
character, found, err := repositories.Character.Load(ctx, "2")
|
|
if err != nil || !found || character.Stats[currentEpicProductionChargeStat] != 23000 || character.Stats[currentEpicProductionWeeklyCountStat] != 1 {
|
|
t.Fatalf("character stats=%+v found=%t err=%v", character.Stats, found, err)
|
|
}
|
|
inventory, found, err := repositories.Inventory.Load(ctx, "2")
|
|
if err != nil || !found {
|
|
t.Fatalf("inventory found=%t err=%v", found, err)
|
|
}
|
|
for slot, wantCount := range map[string]int64{"0:126": 99, "0:123": 99, "0:122": 99, "0:127": 299} {
|
|
if got := inventory.Slots[slot].Count; got != wantCount {
|
|
t.Fatalf("slot %s count=%d want=%d", slot, got, wantCount)
|
|
}
|
|
}
|
|
|
|
forged := request
|
|
forged.Materials = append([]currentEpicProductionProcessMaterial(nil), request.Materials...)
|
|
forged.Materials[0].Slot = 125
|
|
if _, err := service.commitCurrentEpicProductionProcess(ctx, session, currentEpicProductionTestCatalog(), forged, now); !errors.Is(err, errCurrentEpicProductionMaterial) {
|
|
t.Fatalf("forged process err=%v", err)
|
|
}
|
|
after, _, _ := repositories.Character.Load(ctx, "2")
|
|
if after.Stats[currentEpicProductionChargeStat] != 23000 || after.Stats[currentEpicProductionWeeklyCountStat] != 1 {
|
|
t.Fatalf("forged request mutated stats=%+v", after.Stats)
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentEpicProductionProcessAndInfoBodies(t *testing.T) {
|
|
process := buildCurrentEpicProductionProcessSuccessBody(currentEpicProductionProcessResult{ChargePoint: 23000, BigSuccess: true})
|
|
if len(process) != 6 || binary.LittleEndian.Uint32(process[:4]) != 23000 || process[4] != 1 || process[5] != 0 {
|
|
t.Fatalf("process body=%x", process)
|
|
}
|
|
info := buildCurrentEpicProductionInfoBody(2, 116010124, 23000, 1, 1234, 25000)
|
|
if len(info) != 32 || binary.LittleEndian.Uint32(info[0:4]) != 2 || binary.LittleEndian.Uint32(info[4:8]) != 1 ||
|
|
binary.LittleEndian.Uint32(info[8:12]) != 116010124 || binary.LittleEndian.Uint32(info[12:16]) != 23000 ||
|
|
binary.LittleEndian.Uint32(info[20:24]) != 1234 || binary.LittleEndian.Uint32(info[24:28]) != 25000 {
|
|
t.Fatalf("info body=%x", info)
|
|
}
|
|
}
|
|
|
|
func TestCurrentEpicProductionRemainingWeeklyCount(t *testing.T) {
|
|
now := time.Date(2026, time.July, 28, 12, 0, 0, 0, time.UTC)
|
|
weekStart := currentEpicProductionWeekStart(now)
|
|
for _, test := range []struct {
|
|
name string
|
|
used int64
|
|
storedStart int64
|
|
limit uint32
|
|
want uint32
|
|
}{
|
|
{name: "unused", used: 0, storedStart: weekStart, limit: 7, want: 7},
|
|
{name: "one used", used: 1, storedStart: weekStart, limit: 7, want: 6},
|
|
{name: "limit reached", used: 7, storedStart: weekStart, limit: 7, want: 0},
|
|
{name: "over limit", used: 8, storedStart: weekStart, limit: 7, want: 0},
|
|
{name: "new week", used: 7, storedStart: weekStart - 7*24*60*60, limit: 7, want: 7},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if got := currentEpicProductionRemainingWeeklyCount(test.used, test.storedStart, test.limit, now); got != test.want {
|
|
t.Fatalf("remaining=%d want=%d", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCurrentEpicProductionRealPVF(t *testing.T) {
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify the runtime epic production catalog")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := parseCurrentEpicProductionCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if catalog.changeMaterials[10158124] != 30 || catalog.changeMaterials[10163135] != 100 {
|
|
t.Fatalf("real PVF change materials=%+v", catalog.changeMaterials)
|
|
}
|
|
abilityCatalog, err := parseCurrentEpicProductionAbilityCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
darkElement, err := currentEpicProductionAbilityRecipeFor(abilityCatalog, currentEpicProductionAbilityRequest{Category: 0, Option: 4})
|
|
if err != nil || len(darkElement.Materials) != 3 || darkElement.Materials[0] != (currentEpicProductionAbilityMaterial{ItemID: 3173, Count: 50}) {
|
|
t.Fatalf("real PVF dark-element recipe=%+v err=%v", darkElement, err)
|
|
}
|
|
darkNormal, err := currentEpicProductionAbilityRecipeFor(abilityCatalog, currentEpicProductionAbilityRequest{Category: 1, Option: 20})
|
|
if err != nil || len(darkNormal.Materials) != 3 || darkNormal.Materials[2] != (currentEpicProductionAbilityMaterial{ItemID: 10099774, Count: 50}) {
|
|
t.Fatalf("real PVF dark-normal recipe=%+v err=%v", darkNormal, err)
|
|
}
|
|
items, err := newPVFDungeonDropCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
compoundRequest := currentEpicProductionCompoundRequest{
|
|
CatalystItemID: 10163130,
|
|
Materials: []currentEpicProductionCompoundMaterial{{ItemID: 10158124, Slot: 365, Count: 200}},
|
|
}
|
|
compoundPlan, err := validateCurrentEpicProductionCompoundPlan(archive, items, catalog, compoundRequest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if compoundPlan.Contribution != 100000 || compoundPlan.Recipe.NeedCount != 75000 ||
|
|
compoundPlan.Recipe.GroupKey != 2 || !compoundPlan.Recipe.IsMaterialPoint {
|
|
t.Fatalf("compound plan=%+v", compoundPlan)
|
|
}
|
|
advancedRecipe := catalog.catalysts[10163132]
|
|
if advancedRecipe.NeedItemType != "equipment" || advancedRecipe.NeedItemParts != "weapon" || advancedRecipe.NeedItemGrade != "unique" ||
|
|
advancedRecipe.NeedItemMinLevel != 70 || advancedRecipe.NeedItemMaxLevel != 85 || advancedRecipe.NeedCount != 1 ||
|
|
advancedRecipe.MustNeedItemID != 10099774 || advancedRecipe.MustNeedItemCount != 1 || !advancedRecipe.IsSeal {
|
|
t.Fatalf("real PVF advanced catalyst recipe=%+v", advancedRecipe)
|
|
}
|
|
definition, err := validateCurrentEpicProductionTarget(archive, items, catalog, "[gun blader]", 116010124)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if definition.PVFPath != "equipment/character/gunBlader/weapon/sblade/116010124.equ" {
|
|
t.Fatalf("target path=%q", definition.PVFPath)
|
|
}
|
|
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
|
|
CharacterID: "2",
|
|
AccountID: "account-1",
|
|
Job: "15",
|
|
Stats: map[string]int64{},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := &bufferConn{}
|
|
service := Service{
|
|
options: options{accountID: "stale-local-profile"},
|
|
initialEquipmentArchive: archive,
|
|
repositoryProvider: func() (dnfrepo.Group, bool) {
|
|
return repositories, true
|
|
},
|
|
}
|
|
session := &gameSession{conn: connection, accountID: "account-1", selectedCharacterID: 2}
|
|
if err := service.sendSelectedCurrentEpicProductionInfo(session, "real_pvf_no_target"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
packet, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if len(rest) != 0 || packet.Header.MsgID != currentEpicProductionInfoMsgID || packet.Header.Classification != 0 ||
|
|
binary.LittleEndian.Uint32(packet.Body[8:12]) != 0 {
|
|
t.Fatalf("zero-target info response msg=%d class=%d body=%x rest=%d", packet.Header.MsgID, packet.Header.Classification, packet.Body, len(rest))
|
|
}
|
|
connection.write.Reset()
|
|
request := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(request, 116010124)
|
|
if err := service.handleCurrentEpicProductionStart(session, request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
packet, rest = splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if len(rest) != 0 || packet.Header.MsgID != uint16(1417) || packet.Header.Classification != 1 {
|
|
t.Fatalf("response msg=%d class=%d rest=%d", packet.Header.MsgID, packet.Header.Classification, len(rest))
|
|
}
|
|
if want := upperSuccessBody(buildCurrentEpicProductionStartSuccessBody(116010124)); !bytes.Equal(packet.Body, want) {
|
|
t.Fatalf("response body=%x want=%x", packet.Body, want)
|
|
}
|
|
saved, found, err := repositories.Character.Load(context.Background(), "2")
|
|
if err != nil || !found || saved.Stats[currentEpicProductionTargetStat] != 116010124 {
|
|
t.Fatalf("persisted stats=%+v found=%t err=%v", saved.Stats, found, err)
|
|
}
|
|
connection.write.Reset()
|
|
if err := service.sendSelectedCurrentEpicProductionInfo(session, "real_pvf_authenticated_session_account"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
packet, rest = splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if len(rest) != 0 || packet.Header.MsgID != currentEpicProductionInfoMsgID || packet.Header.Classification != 0 {
|
|
t.Fatalf("info response msg=%d class=%d rest=%d", packet.Header.MsgID, packet.Header.Classification, len(rest))
|
|
}
|
|
if got := binary.LittleEndian.Uint32(packet.Body[8:12]); got != 116010124 {
|
|
t.Fatalf("info target=%d want=%d body=%x", got, uint32(116010124), packet.Body)
|
|
}
|
|
|
|
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
|
|
CharacterID: "3",
|
|
AccountID: "account-1",
|
|
Job: "15",
|
|
Stats: map[string]int64{},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
|
|
CharacterID: "3",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
"0:26": {ItemID: 116010124, Count: 1},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
session.selectedCharacterID = 3
|
|
connection.write.Reset()
|
|
if err := service.sendSelectedCurrentEpicProductionInfo(session, "real_pvf_second_character_zero_state"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
packet, rest = splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if len(rest) != 0 || packet.Header.MsgID != currentEpicProductionInfoMsgID || packet.Header.Classification != 0 ||
|
|
binary.LittleEndian.Uint32(packet.Body[0:4]) != 3 || binary.LittleEndian.Uint32(packet.Body[8:12]) != 0 {
|
|
t.Fatalf("second-character zero-state info body=%x rest=%d", packet.Body, len(rest))
|
|
}
|
|
target, err := service.loadCurrentEpicProductionAbilityTarget(context.Background(), session, currentEpicProductionAbilityRequest{Slot: 26})
|
|
if err != nil || target.ItemID != 116010124 || target.CharacterJob != "15" {
|
|
t.Fatalf("second-character ability target=%+v err=%v", target, err)
|
|
}
|
|
jobTag, err := currentRentalCharacterJobTag(archive, 15)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := validateCurrentEpicProductionTarget(archive, items, catalog, jobTag, target.ItemID); err != nil {
|
|
t.Fatalf("second-character PVF target rejected: %v", err)
|
|
}
|
|
}
|