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,612 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user