服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
613 lines
24 KiB
Go
613 lines
24 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/modules/dnf/adventuregroup"
|
|
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
|
dnfenum "longheng.io/server/internal/modules/dnf/dnfenum"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
"longheng.io/server/internal/modules/dnf/repository/memory"
|
|
platformpvf "longheng.io/server/internal/platform/pvf"
|
|
)
|
|
|
|
type currentCustomAbilityTestSource map[string]string
|
|
|
|
func (source currentCustomAbilityTestSource) ReadText(path string) (string, error) {
|
|
text, ok := source[path]
|
|
if !ok {
|
|
return "", errors.New("missing test PVF path: " + path)
|
|
}
|
|
return text, nil
|
|
}
|
|
|
|
func currentCustomAbilityOptionTestText() string {
|
|
return "[parameter]\n[custom grade] `D`\n[/parameter]\n" +
|
|
"[parameter]\n[custom grade] `C`\n[/parameter]\n" +
|
|
"[parameter]\n[custom grade] `B`\n[/parameter]\n" +
|
|
"[parameter]\n[custom grade] `A`\n[/parameter]\n"
|
|
}
|
|
|
|
func currentCustomAbilityModifyTestText(optionID int, itemID int) string {
|
|
return "[info]\n[index]\n" + strconv.Itoa(optionID) + "\n" +
|
|
"[material]\n[grade] `D`\n[list]\n" + strconv.Itoa(itemID) + " 28\n[/list]\n[/material]\n" +
|
|
"[material]\n[grade] `C`\n[list]\n" + strconv.Itoa(itemID) + " 40\n[/list]\n[/material]\n" +
|
|
"[material]\n[grade] `B`\n[list]\n" + strconv.Itoa(itemID) + " 60\n[/list]\n[/material]\n" +
|
|
"[material]\n[grade] `A`\n[list]\n" + strconv.Itoa(itemID) + " 96\n[/list]\n[/material]\n" +
|
|
"[/info]\n"
|
|
}
|
|
|
|
func TestParseCurrentCustomAbilityCatalogIncludesModifyAndUpgradeRules(t *testing.T) {
|
|
source := currentCustomAbilityTestSource{
|
|
currentCustomAbilityListPath: "1 `Options/one.etc`\n2 `Options/two.etc`\n",
|
|
"etc/customability/Options/one.etc": currentCustomAbilityOptionTestText(),
|
|
"etc/customability/Options/two.etc": currentCustomAbilityOptionTestText(),
|
|
currentCustomAbilityInfoPath: "[custom ability type] `warriors dungeon`\n" +
|
|
"[group]\n[indexes]\n1 2\n[/indexes]\n[/group]\n" +
|
|
"[grade probability]\n`D` 6000 `C` 3000 `B` 800 `A` 200\n[/grade probability]\n" +
|
|
currentCustomAbilityModifyTestText(1, 10161533) +
|
|
currentCustomAbilityModifyTestText(2, 10161534) +
|
|
"[upgrade max] 4\n" +
|
|
"[upgrade info]\n[grade] `D`\n[assign material]\n10161536 11\n[/assign material]\n[/upgrade info]\n" +
|
|
"[upgrade info]\n[grade] `C`\n[assign material]\n10161536 11\n[/assign material]\n[/upgrade info]\n" +
|
|
"[upgrade info]\n[grade] `B`\n[assign material]\n10161536 18\n[/assign material]\n[/upgrade info]\n" +
|
|
"[upgrade info]\n[grade] `A`\n[assign material]\n10161536 34\n[/assign material]\n[/upgrade info]\n",
|
|
}
|
|
catalog, err := parseCurrentCustomAbilityCatalog(source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if catalog.upgradeMax != 4 || len(catalog.upgradeMaterial) != 4 {
|
|
t.Fatalf("upgrade max=%d rows=%d", catalog.upgradeMax, len(catalog.upgradeMaterial))
|
|
}
|
|
if len(catalog.modifyMaterial) != 2 || catalog.modifyMaterial[1][3] != (currentCustomAbilityMaterialCost{itemID: 10161533, count: 96}) ||
|
|
catalog.modifyMaterial[2][0] != (currentCustomAbilityMaterialCost{itemID: 10161534, count: 28}) {
|
|
t.Fatalf("modify materials=%+v", catalog.modifyMaterial)
|
|
}
|
|
for grade, want := range map[byte]int64{0: 11, 1: 11, 2: 18, 3: 34} {
|
|
cost := catalog.upgradeMaterial[grade]
|
|
if cost.itemID != 10161536 || cost.count != want {
|
|
t.Fatalf("grade=%d cost=%+v", grade, cost)
|
|
}
|
|
}
|
|
|
|
broken := currentCustomAbilityTestSource{}
|
|
for path, text := range source {
|
|
broken[path] = text
|
|
}
|
|
broken["etc/customability/Options/two.etc"] = "[parameter]\n[custom grade] `D`\n[/parameter]\n"
|
|
if _, err := parseCurrentCustomAbilityCatalog(broken); err == nil {
|
|
t.Fatal("option missing C/B/A grades was accepted")
|
|
}
|
|
}
|
|
|
|
func TestCommitCurrentCustomAbilityUpgradeConsumesAcrossStacksAndPersistsGrade(t *testing.T) {
|
|
repositories := memory.NewMemoryGroup()
|
|
catalog := ¤tCustomAbilityCatalog{
|
|
optionIDs: map[byte]struct{}{4: {}, 12: {}},
|
|
upgradeMax: 4,
|
|
upgradeMaterial: map[byte]currentCustomAbilityMaterialCost{1: {itemID: 10161536, count: 11}},
|
|
}
|
|
raw := make([]byte, currentItemListEntryWireSize)
|
|
binary.LittleEndian.PutUint16(raw[0:2], 9)
|
|
binary.LittleEndian.PutUint32(raw[2:6], 100130573)
|
|
raw[currentCustomAbilityOption0Offset] = 12
|
|
raw[currentCustomAbilityOption1Offset] = 4
|
|
raw[currentCustomAbilityGradeOffset] = 0
|
|
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
|
|
CharacterID: "4",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
"0:9": {ItemID: 100130573, Count: 1, RawEntry: raw},
|
|
"0:151": {ItemID: 10161536, Count: 6},
|
|
"0:152": {ItemID: 10161536, Count: 10},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := commitCurrentCustomAbilityUpgrade(context.Background(), repositories, "4", 9, 100130573, catalog)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.oldGrade != 0 || result.newGrade != 1 || result.materialRemaining != 5 {
|
|
t.Fatalf("upgrade result=%+v", result)
|
|
}
|
|
if len(result.refreshes) != 3 || result.refreshes[0].SlotIndex != 9 || result.refreshes[1].SlotIndex != 151 || result.refreshes[2].SlotIndex != 152 {
|
|
t.Fatalf("refreshes=%+v", result.refreshes)
|
|
}
|
|
stored, _, err := repositories.Inventory.Load(context.Background(), "4")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := stored.Slots["0:9"].RawEntry[currentCustomAbilityGradeOffset]; got != 1 {
|
|
t.Fatalf("persisted grade=%d", got)
|
|
}
|
|
if _, exists := stored.Slots["0:151"]; exists {
|
|
t.Fatal("first material stack was not deleted")
|
|
}
|
|
if got := stored.Slots["0:152"].Count; got != 5 {
|
|
t.Fatalf("remaining material=%d", got)
|
|
}
|
|
}
|
|
|
|
func TestCommitCurrentCustomAbilityUpgradeMaterialFailureRollsBack(t *testing.T) {
|
|
repositories := memory.NewMemoryGroup()
|
|
catalog := ¤tCustomAbilityCatalog{
|
|
optionIDs: map[byte]struct{}{4: {}, 12: {}},
|
|
upgradeMax: 4,
|
|
upgradeMaterial: map[byte]currentCustomAbilityMaterialCost{1: {itemID: 10161536, count: 11}},
|
|
}
|
|
raw := make([]byte, currentItemListEntryWireSize)
|
|
raw[currentCustomAbilityOption0Offset] = 12
|
|
raw[currentCustomAbilityOption1Offset] = 4
|
|
seed := dnfrepo.InventoryRecord{CharacterID: "4", Slots: map[string]dnfrepo.ItemStack{
|
|
"0:9": {ItemID: 100130573, Count: 1, RawEntry: raw},
|
|
"0:151": {ItemID: 10161536, Count: 10},
|
|
}}
|
|
if err := repositories.Inventory.Save(context.Background(), seed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := commitCurrentCustomAbilityUpgrade(context.Background(), repositories, "4", 9, 100130573, catalog)
|
|
if !errors.Is(err, errCurrentCustomAbilityUpgradeMaterial) {
|
|
t.Fatalf("upgrade error=%v", err)
|
|
}
|
|
stored, _, _ := repositories.Inventory.Load(context.Background(), "4")
|
|
if stored.Slots["0:151"].Count != 10 || stored.Slots["0:9"].RawEntry[currentCustomAbilityGradeOffset] != 0 {
|
|
t.Fatalf("failed upgrade mutated inventory=%+v", stored.Slots)
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentCustomAbilityUpgradeSuccessBodyIncludesExpeditionState(t *testing.T) {
|
|
now := time.Unix(1_700_000_000, 0)
|
|
state := adventuregroup.RuntimeState{Expeditions: map[string]adventuregroup.Expedition{
|
|
"2": {
|
|
Area: 2,
|
|
State: 1,
|
|
StartedAt: now.Unix() - 10,
|
|
EndsAt: now.Unix() + 10,
|
|
Attributes: []byte{4, 7},
|
|
},
|
|
}}
|
|
wantState := buildCurrentAdventureExpeditionStateBody(state, now)
|
|
body := buildCurrentCustomAbilityUpgradeSuccessBody(state, now)
|
|
if len(body) != len(wantState)+1 || body[0] != 1 || !bytes.Equal(body[1:], wantState) {
|
|
t.Fatalf("custom ability upgrade success body=%x expedition=%x", body, wantState)
|
|
}
|
|
}
|
|
|
|
func TestInitializeCurrentCustomAbilityInventoryPersistsPVFDrawOnce(t *testing.T) {
|
|
catalog := ¤tCustomAbilityCatalog{
|
|
optionIDs: map[byte]struct{}{1: {}, 2: {}, 11: {}, 21: {}},
|
|
groups: [][]byte{{1, 2}, {11}, {21}},
|
|
gradeWeight: []currentCustomAbilityGradeWeight{
|
|
{grade: 3, weight: 200},
|
|
{grade: 2, weight: 800},
|
|
{grade: 1, weight: 3000},
|
|
{grade: 0, weight: 6000},
|
|
},
|
|
totalWeight: 10000,
|
|
}
|
|
raw := make([]byte, currentItemListEntryWireSize)
|
|
binary.LittleEndian.PutUint16(raw[0:2], 9)
|
|
binary.LittleEndian.PutUint32(raw[2:6], 100130573)
|
|
binary.LittleEndian.PutUint32(raw[6:10], 123456)
|
|
record := dnfrepo.InventoryRecord{
|
|
CharacterID: "4",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
"0:9": {ItemID: 100130573, Count: 1, RawEntry: raw},
|
|
"0:10": {ItemID: 3340, Count: 100},
|
|
},
|
|
}
|
|
resolve := func(itemID uint32) (currentCustomAbilityItemProfile, error) {
|
|
return currentCustomAbilityItemProfile{applicable: itemID == 100130573, optionCount: 2, partSetIndex: 1}, nil
|
|
}
|
|
draws := []int{0, 1, 2, 0, 1000}
|
|
drawCount := 0
|
|
draw := func(limit int) (int, error) {
|
|
value := draws[drawCount]
|
|
drawCount++
|
|
if value >= limit {
|
|
t.Fatalf("draw value %d exceeds limit %d", value, limit)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
updated, changed, err := initializeCurrentCustomAbilityInventory(record, catalog, resolve, draw)
|
|
if err != nil || changed != 1 {
|
|
t.Fatalf("initialize changed=%d err=%v", changed, err)
|
|
}
|
|
stack := updated.Slots["0:9"]
|
|
if got := stack.RawEntry[currentCustomAbilityOption0Offset]; got != 2 {
|
|
t.Fatalf("option0=%d want=2", got)
|
|
}
|
|
if got := stack.RawEntry[currentCustomAbilityOption1Offset]; got != 21 {
|
|
t.Fatalf("option1=%d want=21", got)
|
|
}
|
|
if got := stack.RawEntry[currentCustomAbilitySetRecordOffset]; got != 0 {
|
|
t.Fatalf("set record=%d want=0", got)
|
|
}
|
|
if got := stack.RawEntry[currentCustomAbilityGradeOffset]; got != 1 {
|
|
t.Fatalf("grade=%d want=1", got)
|
|
}
|
|
if stack.Extra["custom_ability_option_0"] != "2" || stack.Extra["custom_ability_option_1"] != "21" || stack.Extra["custom_ability_grade"] != "1" {
|
|
t.Fatalf("custom ability extra=%+v", stack.Extra)
|
|
}
|
|
if bytes.Equal(record.Slots["0:9"].RawEntry, stack.RawEntry) {
|
|
t.Fatal("initializer mutated the input repository snapshot")
|
|
}
|
|
|
|
drawCountBeforeReplay := drawCount
|
|
replayed, replayChanged, err := initializeCurrentCustomAbilityInventory(updated, catalog, resolve, draw)
|
|
if err != nil || replayChanged != 0 || drawCount != drawCountBeforeReplay {
|
|
t.Fatalf("replay changed=%d draw_count=%d->%d err=%v", replayChanged, drawCountBeforeReplay, drawCount, err)
|
|
}
|
|
if !bytes.Equal(replayed.Slots["0:9"].RawEntry, stack.RawEntry) {
|
|
t.Fatal("replay changed persisted custom ability state")
|
|
}
|
|
}
|
|
|
|
func TestInitializeCurrentCustomAbilityInventorySlotsRepairsOnlyIncrementalGrantRows(t *testing.T) {
|
|
catalog := ¤tCustomAbilityCatalog{
|
|
optionIDs: map[byte]struct{}{1: {}, 2: {}, 11: {}, 21: {}},
|
|
groups: [][]byte{{1, 2}, {11}, {21}},
|
|
gradeWeight: []currentCustomAbilityGradeWeight{
|
|
{grade: 0, weight: 1},
|
|
},
|
|
totalWeight: 1,
|
|
}
|
|
makeRaw := func(slot uint16, itemID uint32) []byte {
|
|
raw := make([]byte, currentItemListEntryWireSize)
|
|
binary.LittleEndian.PutUint16(raw[0:2], slot)
|
|
binary.LittleEndian.PutUint32(raw[2:6], itemID)
|
|
return raw
|
|
}
|
|
record := dnfrepo.InventoryRecord{
|
|
CharacterID: "4",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
"0:9": {ItemID: 100130573, Count: 1, RawEntry: makeRaw(9, 100130573)},
|
|
"0:10": {ItemID: 100130574, Count: 1, RawEntry: makeRaw(10, 100130574)},
|
|
"0:11": {ItemID: 3340, Count: 100, RawEntry: makeRaw(11, 3340)},
|
|
},
|
|
}
|
|
resolve := func(itemID uint32) (currentCustomAbilityItemProfile, error) {
|
|
return currentCustomAbilityItemProfile{
|
|
applicable: itemID == 100130573 || itemID == 100130574,
|
|
optionCount: 2,
|
|
}, nil
|
|
}
|
|
draws := []int{0, 0, 2, 0, 0}
|
|
drawCount := 0
|
|
draw := func(limit int) (int, error) {
|
|
value := draws[drawCount]
|
|
drawCount++
|
|
if value >= limit {
|
|
t.Fatalf("draw value %d exceeds limit %d", value, limit)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
updated, changed, err := initializeCurrentCustomAbilityInventorySlots(
|
|
record,
|
|
map[int16]struct{}{10: {}},
|
|
catalog,
|
|
resolve,
|
|
draw,
|
|
)
|
|
if err != nil || changed != 1 {
|
|
t.Fatalf("incremental initialize changed=%d err=%v", changed, err)
|
|
}
|
|
if got := updated.Slots["0:10"].RawEntry[currentCustomAbilityRawBlockStart:currentCustomAbilityRawBlockEnd]; !bytes.Equal(got, []byte{1, 21, 0, 0, 0}) {
|
|
t.Fatalf("target custom tail=%x", got)
|
|
}
|
|
if got := updated.Slots["0:9"].RawEntry[currentCustomAbilityRawBlockStart:currentCustomAbilityRawBlockEnd]; !bytes.Equal(got, make([]byte, currentEpicProductionAbilityTailSize)) {
|
|
t.Fatalf("unrefreshed custom slot was redrawn: %x", got)
|
|
}
|
|
if got := updated.Slots["0:11"].RawEntry[currentCustomAbilityRawBlockStart:currentCustomAbilityRawBlockEnd]; !bytes.Equal(got, make([]byte, currentEpicProductionAbilityTailSize)) {
|
|
t.Fatalf("ordinary material row was changed: %x", got)
|
|
}
|
|
if bytes.Equal(record.Slots["0:10"].RawEntry, updated.Slots["0:10"].RawEntry) {
|
|
t.Fatal("incremental initializer mutated or reused the input snapshot")
|
|
}
|
|
|
|
drawCountBeforeReplay := drawCount
|
|
replayed, replayChanged, err := initializeCurrentCustomAbilityInventorySlots(
|
|
updated,
|
|
map[int16]struct{}{10: {}},
|
|
catalog,
|
|
resolve,
|
|
draw,
|
|
)
|
|
if err != nil || replayChanged != 0 || drawCount != drawCountBeforeReplay {
|
|
t.Fatalf("incremental replay changed=%d draw_count=%d->%d err=%v", replayChanged, drawCountBeforeReplay, drawCount, err)
|
|
}
|
|
if !bytes.Equal(replayed.Slots["0:10"].RawEntry, updated.Slots["0:10"].RawEntry) {
|
|
t.Fatal("incremental replay changed the persisted custom ability tail")
|
|
}
|
|
}
|
|
|
|
func TestInitializeCurrentCustomAbilityInventoryMigratesLegacyOffsetsWithoutRedraw(t *testing.T) {
|
|
catalog := ¤tCustomAbilityCatalog{
|
|
optionIDs: map[byte]struct{}{4: {}, 12: {}},
|
|
groups: [][]byte{{4, 12}},
|
|
gradeWeight: []currentCustomAbilityGradeWeight{
|
|
{grade: 0, weight: 1},
|
|
},
|
|
totalWeight: 1,
|
|
}
|
|
raw := make([]byte, currentItemListEntryWireSize)
|
|
raw[currentCustomAbilityLegacyOption0Offset] = 12
|
|
raw[currentCustomAbilityLegacyOption1Offset] = 4
|
|
raw[currentCustomAbilityLegacyGradeOffset] = 1
|
|
record := dnfrepo.InventoryRecord{CharacterID: "4", Slots: map[string]dnfrepo.ItemStack{
|
|
"0:9": {
|
|
ItemID: 100130573,
|
|
Count: 1,
|
|
RawEntry: raw,
|
|
Extra: map[string]string{
|
|
"custom_ability_option_0": "12",
|
|
"custom_ability_option_1": "4",
|
|
"custom_ability_set_record": "0",
|
|
"custom_ability_grade": "1",
|
|
},
|
|
},
|
|
}}
|
|
resolve := func(itemID uint32) (currentCustomAbilityItemProfile, error) {
|
|
return currentCustomAbilityItemProfile{applicable: itemID == 100130573, optionCount: 2}, nil
|
|
}
|
|
drawCalled := false
|
|
updated, changed, err := initializeCurrentCustomAbilityInventory(record, catalog, resolve, func(int) (int, error) {
|
|
drawCalled = true
|
|
return 0, nil
|
|
})
|
|
if err != nil || changed != 1 || drawCalled {
|
|
t.Fatalf("legacy migration changed=%d draw=%t err=%v", changed, drawCalled, err)
|
|
}
|
|
stack := updated.Slots["0:9"]
|
|
if stack.RawEntry[currentCustomAbilityOption0Offset] != 12 ||
|
|
stack.RawEntry[currentCustomAbilityOption1Offset] != 4 ||
|
|
stack.RawEntry[currentCustomAbilityGradeOffset] != 1 {
|
|
t.Fatalf("migrated tail=%x", stack.RawEntry[currentCustomAbilityRawBlockStart:currentCustomAbilityRawBlockEnd])
|
|
}
|
|
if stack.Extra["tail_data_72"] != "0c04000001" {
|
|
t.Fatalf("migrated extra=%+v", stack.Extra)
|
|
}
|
|
}
|
|
|
|
func TestCurrentCustomAbilityInventoryRefreshEntriesOnlyIncludesPVFCustomEquipment(t *testing.T) {
|
|
catalog := ¤tCustomAbilityCatalog{optionIDs: map[byte]struct{}{4: {}, 12: {}}}
|
|
customRaw := make([]byte, currentItemListEntryWireSize)
|
|
customRaw[currentCustomAbilityOption0Offset] = 12
|
|
customRaw[currentCustomAbilityOption1Offset] = 4
|
|
customRaw[currentCustomAbilityGradeOffset] = 1
|
|
ordinaryRaw := append([]byte(nil), customRaw...)
|
|
record := dnfrepo.InventoryRecord{
|
|
CharacterID: "77",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
"0:9": {ItemID: 100130573, Count: 1, RawEntry: customRaw},
|
|
"0:10": {ItemID: 100000001, Count: 1, RawEntry: ordinaryRaw},
|
|
"0:11": {ItemID: 100130573, Count: 2, RawEntry: customRaw},
|
|
},
|
|
}
|
|
resolve := func(itemID uint32) (currentCustomAbilityItemProfile, error) {
|
|
return currentCustomAbilityItemProfile{applicable: itemID == 100130573, optionCount: 2}, nil
|
|
}
|
|
|
|
entries, err := currentCustomAbilityInventoryRefreshEntries(record, catalog, resolve)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(entries) != 1 {
|
|
t.Fatalf("custom ability refresh entry count=%d", len(entries))
|
|
}
|
|
if got := binary.LittleEndian.Uint16(entries[0].data[0:2]); got != 9 {
|
|
t.Fatalf("custom ability refresh slot=%d want=9", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint32(entries[0].data[2:6]); got != 100130573 {
|
|
t.Fatalf("custom ability refresh item=%d want=100130573", got)
|
|
}
|
|
if got := entries[0].data[currentCustomAbilityRawBlockStart:currentCustomAbilityRawBlockEnd]; !bytes.Equal(got, customRaw[currentCustomAbilityRawBlockStart:currentCustomAbilityRawBlockEnd]) {
|
|
t.Fatalf("custom ability refresh raw=%x want=%x", got, customRaw[currentCustomAbilityRawBlockStart:currentCustomAbilityRawBlockEnd])
|
|
}
|
|
}
|
|
|
|
func TestCurrentMode1EquipmentRawBlocksFallBackToCurrentItemState(t *testing.T) {
|
|
var entry currentItemListEntry
|
|
copy(entry.data[currentEpicProductionAbilityTailOffset:currentEpicProductionAbilityTailOffset+currentEpicProductionAbilityTailSize], []byte{4, 20, 0, 0, 0})
|
|
|
|
lateRawE := currentMode1EquipmentRawBlockOrCurrentItem(nil, entry, currentEpicProductionAbilityTailOffset, currentEpicProductionAbilityTailOffset+currentEpicProductionAbilityTailSize)
|
|
if !bytes.Equal(lateRawE, []byte{4, 20, 0, 0, 0}) {
|
|
t.Fatalf("late rawE=%x", lateRawE)
|
|
}
|
|
|
|
explicit := map[string]string{"mode1_raw_d_hex": "aabb"}
|
|
if got := currentMode1EquipmentRawBlockOrCurrentItem(explicit, entry, currentCustomAbilityRawBlockStart, currentCustomAbilityRawBlockEnd, "mode1_raw_d_hex"); !bytes.Equal(got, []byte{0xaa, 0xbb}) {
|
|
t.Fatalf("explicit mode1 rawD did not retain precedence: %x", got)
|
|
}
|
|
}
|
|
|
|
func TestCurrentCustomAbilityRealPVF(t *testing.T) {
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify current customability PVF")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := parseCurrentCustomAbilityCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(catalog.optionIDs) != 16 || len(catalog.groups) != 3 || catalog.totalWeight != 10000 {
|
|
t.Fatalf("customability options=%d groups=%d grade_weight=%d", len(catalog.optionIDs), len(catalog.groups), catalog.totalWeight)
|
|
}
|
|
if catalog.upgradeMax != 4 || len(catalog.upgradeMaterial) != 4 {
|
|
t.Fatalf("customability upgrade max=%d rows=%d", catalog.upgradeMax, len(catalog.upgradeMaterial))
|
|
}
|
|
if len(catalog.modifyMaterial) != 16 {
|
|
t.Fatalf("customability modify options=%d", len(catalog.modifyMaterial))
|
|
}
|
|
for optionID, grades := range catalog.modifyMaterial {
|
|
if len(grades) != 4 {
|
|
t.Fatalf("customability modify option=%d grades=%d", optionID, len(grades))
|
|
}
|
|
}
|
|
if got := catalog.modifyMaterial[1][3]; got != (currentCustomAbilityMaterialCost{itemID: 10161533, count: 96}) {
|
|
t.Fatalf("customability option1 gradeA material=%+v", got)
|
|
}
|
|
if got := catalog.modifyMaterial[14][0]; got != (currentCustomAbilityMaterialCost{itemID: 10161534, count: 23}) {
|
|
t.Fatalf("customability option14 gradeD material=%+v", got)
|
|
}
|
|
if got := catalog.modifyMaterial[25][2]; got != (currentCustomAbilityMaterialCost{itemID: 10161535, count: 60}) {
|
|
t.Fatalf("customability option25 gradeB material=%+v", got)
|
|
}
|
|
if len(catalog.groups[0]) != 7 || len(catalog.groups[1]) != 4 || len(catalog.groups[2]) != 5 {
|
|
t.Fatalf("customability groups=%v", catalog.groups)
|
|
}
|
|
service := &Service{initialEquipmentArchive: archive}
|
|
profile, err := service.resolveCurrentCustomAbilityItemProfile(100130573)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !profile.applicable || profile.optionCount != 2 || profile.partSetIndex != 1 {
|
|
t.Fatalf("real PVF customability profile=%+v", profile)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonPickupPersistsCustomAbilityBeforeFirstRefreshRealPVF(t *testing.T) {
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify fighting-spirit pickup initialization")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
items, err := newPVFDungeonDropCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
definition, err := items.ResolveItem(100130573)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.EqualFold(strings.TrimSpace(definition.CustomAbilityType), "warriors dungeon") ||
|
|
definition.CustomAbilityCount != 2 {
|
|
t.Fatalf("real PVF fighting-spirit definition=%+v", definition)
|
|
}
|
|
|
|
repositories := memory.NewMemoryGroup()
|
|
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
|
|
CharacterID: "4",
|
|
Slots: map[string]dnfrepo.ItemStack{},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := &Service{initialEquipmentArchive: archive}
|
|
slot, update, err := service.grantCurrentDungeonPickupItem(
|
|
context.Background(),
|
|
repositories.CharacterItems,
|
|
"4",
|
|
definition,
|
|
1,
|
|
time.Now().UTC(),
|
|
nil,
|
|
123456,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := parseCurrentCustomAbilityCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wireState, valid := currentCustomAbilityStateFromRaw(update.data[:], catalog)
|
|
if !valid {
|
|
t.Fatalf("first pickup update has no custom ability state: %x", update.data[currentCustomAbilityRawBlockStart:currentCustomAbilityRawBlockEnd])
|
|
}
|
|
stored, found, err := repositories.Inventory.Load(context.Background(), "4")
|
|
if err != nil || !found {
|
|
t.Fatalf("load pickup inventory found=%t err=%v", found, err)
|
|
}
|
|
stack := stored.Slots[currentDungeonPickupMainSlotKey(int16(slot))]
|
|
storedRow := currentItemListEntryFromStack(dnfrepo.MainInventoryListType, int16(slot), stack)
|
|
storedState, valid := currentCustomAbilityStateFromRaw(storedRow.data[:], catalog)
|
|
if !valid || storedState != wireState || stack.Extra["tail_data_72"] == "" {
|
|
t.Fatalf("persisted pickup state=%+v valid=%t wire=%+v extra=%+v", storedState, valid, wireState, stack.Extra)
|
|
}
|
|
}
|
|
|
|
func TestCurrentCustomAbilityIncrementalRefreshInitializesGrantRealPVF(t *testing.T) {
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify incremental customability grant refresh")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repositories := memory.NewMemoryGroup()
|
|
raw := make([]byte, currentItemListEntryWireSize)
|
|
binary.LittleEndian.PutUint16(raw[0:2], 9)
|
|
binary.LittleEndian.PutUint32(raw[2:6], 100130573)
|
|
binary.LittleEndian.PutUint32(raw[6:10], 123456)
|
|
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
|
|
CharacterID: "4",
|
|
Slots: map[string]dnfrepo.ItemStack{
|
|
"0:9": {ItemID: 100130573, Count: 1, RawEntry: raw},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := &bufferConn{}
|
|
service := &Service{
|
|
initialEquipmentArchive: archive,
|
|
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
|
}
|
|
session := &gameSession{conn: connection, connID: "custom-ability-incremental-grant", selectedCharacterID: 4}
|
|
if err := service.sendSelectedIncrementalItemSlotRefreshes(session, "dungeon_pickup", []alignedcmd.ItemSlotRefresh{{
|
|
ListType: dnfrepo.MainInventoryListType,
|
|
SlotIndex: 9,
|
|
}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
packet, trailing := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if len(trailing) != 0 || packet.Header.Classification != 0 ||
|
|
packet.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) ||
|
|
len(packet.Body) != 3+currentItemListEntryWireSize ||
|
|
packet.Body[0] != dnfrepo.MainInventoryListType ||
|
|
binary.LittleEndian.Uint16(packet.Body[1:3]) != 1 {
|
|
t.Fatalf("incremental packet header=%+v body_len=%d body_prefix=%x trailing=%d", packet.Header, len(packet.Body), packet.Body[:minInt(len(packet.Body), 16)], len(trailing))
|
|
}
|
|
catalog, err := parseCurrentCustomAbilityCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wireRow := packet.Body[3:]
|
|
wireState, valid := currentCustomAbilityStateFromRaw(wireRow, catalog)
|
|
if !valid {
|
|
t.Fatalf("incremental row did not contain a valid custom ability tail: %x", wireRow[currentCustomAbilityRawBlockStart:currentCustomAbilityRawBlockEnd])
|
|
}
|
|
stored, found, err := repositories.Inventory.Load(context.Background(), "4")
|
|
if err != nil || !found {
|
|
t.Fatalf("load persisted incremental state found=%t err=%v", found, err)
|
|
}
|
|
storedRow := currentItemListEntryFromStack(dnfrepo.MainInventoryListType, 9, stored.Slots["0:9"])
|
|
storedState, valid := currentCustomAbilityStateFromRaw(storedRow.data[:], catalog)
|
|
if !valid || storedState != wireState {
|
|
t.Fatalf("persisted state=%+v valid=%t wire=%+v", storedState, valid, wireState)
|
|
}
|
|
if stored.Slots["0:9"].Extra["tail_data_72"] == "" {
|
|
t.Fatalf("incremental state extra was not persisted: %+v", stored.Slots["0:9"].Extra)
|
|
}
|
|
}
|