服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
354 lines
19 KiB
Go
354 lines
19 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"testing"
|
|
|
|
dnfexpertjob "longheng.io/server/internal/modules/dnf/expertjob"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
|
)
|
|
|
|
func TestCurrentExpertJobRequestsMatchCurrentEXELayout(t *testing.T) {
|
|
compoundBody := make([]byte, 8)
|
|
binary.LittleEndian.PutUint32(compoundBody[0:4], 2600149)
|
|
binary.LittleEndian.PutUint16(compoundBody[4:6], 3)
|
|
binary.LittleEndian.PutUint16(compoundBody[6:8], uint16(0xffff))
|
|
compound, err := parseCurrentExpertJobCompoundRequest(compoundBody)
|
|
if err != nil || compound.RecipeItemID != 2600149 || compound.Count != 3 || compound.CardSlot != -1 {
|
|
t.Fatalf("compound=%+v error=%v", compound, err)
|
|
}
|
|
binary.LittleEndian.PutUint16(compoundBody[4:6], 1)
|
|
binary.LittleEndian.PutUint16(compoundBody[6:8], 12)
|
|
cardCompound, err := parseCurrentExpertJobCompoundRequest(compoundBody)
|
|
if err != nil || cardCompound.CardSlot != 12 || cardCompound.Count != 1 {
|
|
t.Fatalf("card compound=%+v error=%v", cardCompound, err)
|
|
}
|
|
// 【2026-09-18】原来"卡片槽 + 数量!=1"会被判非法(附魔机界面选 50/49 时必被拒,
|
|
// 回失败码 19)。制作宝珠改成按数量循环后,这里也必须放行同一个请求体。
|
|
binary.LittleEndian.PutUint16(compoundBody[4:6], 49)
|
|
cardBatch, err := parseCurrentExpertJobCompoundRequest(compoundBody)
|
|
if err != nil || cardBatch.CardSlot != 12 || cardBatch.Count != 49 {
|
|
t.Fatalf("card batch compound=%+v error=%v", cardBatch, err)
|
|
}
|
|
binary.LittleEndian.PutUint16(compoundBody[4:6], 0)
|
|
if _, err := parseCurrentExpertJobCompoundRequest(compoundBody); err == nil {
|
|
t.Fatal("zero-count compound request accepted")
|
|
}
|
|
extractionBody := []byte{2, 9, 0, 0, 10, 0}
|
|
extraction, err := parseCurrentExpertJobExtractionRequest(extractionBody)
|
|
if err != nil || extraction.ExtractorType != 2 || extraction.ExtractorSlot != 9 || extraction.TargetList != 0 || extraction.TargetSlot != 10 {
|
|
t.Fatalf("extraction=%+v error=%v", extraction, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentExpertJobModuleRegistersCompleteCurrentEXECommandChain(t *testing.T) {
|
|
module := expertJobGameplayModule()
|
|
opcodes := []struct {
|
|
name string
|
|
opcode uint16
|
|
normalized bool
|
|
}{
|
|
{name: "store disjoint", opcode: 235},
|
|
{name: "repair disjoint machine", opcode: 236},
|
|
{name: "compound", opcode: 238, normalized: true},
|
|
{name: "give up", opcode: 239, normalized: true},
|
|
{name: "upgrade disjoint machine", opcode: 240},
|
|
{name: "extract", opcode: 416, normalized: true},
|
|
{name: "create store", opcode: 598},
|
|
{name: "enter store", opcode: 599},
|
|
{name: "close store", opcode: 600},
|
|
{name: "use enchant store", opcode: 601},
|
|
{name: "repair expert-job store", opcode: 611},
|
|
}
|
|
if len(module.LegacyHandlers) != len(opcodes) || len(module.UpperHandlers) != len(opcodes) {
|
|
t.Fatalf("expert-job routes legacy=%d upper=%d want=%d", len(module.LegacyHandlers), len(module.UpperHandlers), len(opcodes))
|
|
}
|
|
for _, opcode := range opcodes {
|
|
t.Run(opcode.name, func(t *testing.T) {
|
|
if module.LegacyHandlers[opcode.opcode] == nil || module.UpperHandlers[opcode.opcode] == nil {
|
|
t.Fatalf("opcode %d is not registered in both gameplay routes", opcode.opcode)
|
|
}
|
|
if normalized := module.LegacyNormalizers[opcode.opcode] != nil; normalized != opcode.normalized {
|
|
t.Fatalf("opcode %d legacy normalizer=%t want=%t", opcode.opcode, normalized, opcode.normalized)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCurrentExpertJobStoreLayoutsMatch86JPReaders(t *testing.T) {
|
|
create := make([]byte, 18)
|
|
create[0] = currentExpertJobEnchantStoreKind
|
|
binary.LittleEndian.PutUint32(create[1:5], 3)
|
|
copy(create[5:8], []byte("abc"))
|
|
binary.LittleEndian.PutUint32(create[8:12], 500)
|
|
binary.LittleEndian.PutUint16(create[12:14], uint16(120))
|
|
binary.LittleEndian.PutUint16(create[14:16], uint16(220))
|
|
binary.LittleEndian.PutUint16(create[16:18], uint16(5))
|
|
request, err := parseCurrentExpertJobStoreCreateRequest(create)
|
|
if err != nil || request.Kind != currentExpertJobEnchantStoreKind || string(request.Name) != "abc" || request.Cost != 500 || request.PositionX != 120 || request.PositionY != 220 || request.OpaqueObjectLink != 5 {
|
|
t.Fatalf("create request=%+v error=%v", request, err)
|
|
}
|
|
store := ¤tExpertJobStore{OwnerCharacterID: 19, Kind: currentExpertJobEnchantStoreKind, Name: []byte("abc"), Cost: 500, TownID: 38, AreaID: 2, PositionX: 120, PositionY: 220, Endurance: 297, Qualifications: []byte{0, 1}}
|
|
// Trailing mode byte per the 86 reference: mode=0 turns the owner's existing
|
|
// town clone into the store (no second entity), mode=1/2 leave a residual
|
|
// clone. Verified live on 2026-09-22 with no client patch injected - the store
|
|
// stays visible, clickable and usable on both sides without the stand-in body;
|
|
// see the constant's comment.
|
|
wantCreate := []byte{3, 19, 0, 3, 0, 0, 0, 'a', 'b', 'c', 38, 2, 0, 0, 0, 120, 0, 220, 0, 0xf4, 1, 0, 0, 0, 2, 0, 1}
|
|
if got := buildCurrentExpertJobStoreCreateNotification(store); !bytes.Equal(got, wantCreate) {
|
|
t.Fatalf("create notification=%x want=%x", got, wantCreate)
|
|
}
|
|
wantUpdate := []byte{3, 19, 0, 3, 0, 0, 0, 'a', 'b', 'c', 2, 0, 1}
|
|
if got := buildCurrentExpertJobStoreUpdateNotification(store); !bytes.Equal(got, wantUpdate) {
|
|
t.Fatalf("update notification=%x want=%x", got, wantUpdate)
|
|
}
|
|
wantEnter := []byte{1, 3, 19, 0, 0x29, 1, 0, 0}
|
|
if got := buildCurrentExpertJobStoreEnterSuccess(store); !bytes.Equal(got, wantEnter) {
|
|
t.Fatalf("enter notification=%x want=%x", got, wantEnter)
|
|
}
|
|
disjoint, err := parseCurrentExpertJobStoreDisjointRequest([]byte{19, 0, 10, 0, 0})
|
|
if err != nil || disjoint.OwnerID != 19 || disjoint.TargetSlot != 10 || disjoint.TargetList != 0 {
|
|
t.Fatalf("disjoint request=%+v error=%v", disjoint, err)
|
|
}
|
|
// 【批量分解 2026-09-23】op235 批量信封的契约:
|
|
// u16 ownerID | u16 0xFFFF | u8 listType | u32 "B35\x01" | u8 count | i16 slots[count]
|
|
// 三条硬要求:(1) 合法信封被批量解析器接住;(2) 5 字节单件请求绝不被误判成批量;
|
|
// (3) 批量信封绝不被单件解析器接收(否则会当成 TargetSlot=-1 的怪请求)。
|
|
batchBody := make([]byte, currentExpertJobStoreDisjointBatchHeaderSize+2*3)
|
|
batchSentinel := currentExpertJobStoreDisjointBatchSentinel // via a variable: uint16(-1) as a constant is an overflow error
|
|
binary.LittleEndian.PutUint16(batchBody[0:2], 19)
|
|
binary.LittleEndian.PutUint16(batchBody[2:4], uint16(batchSentinel))
|
|
batchBody[4] = dnfrepo.MainInventoryListType
|
|
copy(batchBody[5:9], currentExpertJobStoreDisjointBatchMarker[:])
|
|
batchBody[9] = 3
|
|
for index, slot := range []int16{9, 10, 16} {
|
|
binary.LittleEndian.PutUint16(batchBody[10+index*2:12+index*2], uint16(slot))
|
|
}
|
|
batch, isBatch, err := parseCurrentExpertJobStoreDisjointBatchRequest(batchBody)
|
|
if err != nil || !isBatch || batch.OwnerID != 19 || batch.TargetList != dnfrepo.MainInventoryListType ||
|
|
len(batch.Slots) != 3 || batch.Slots[0] != 9 || batch.Slots[1] != 10 || batch.Slots[2] != 16 {
|
|
t.Fatalf("batch request=%+v isBatch=%t error=%v", batch, isBatch, err)
|
|
}
|
|
if _, isBatch, err := parseCurrentExpertJobStoreDisjointBatchRequest([]byte{19, 0, 10, 0, 0}); isBatch || err != nil {
|
|
t.Fatalf("single request misread as batch: isBatch=%t error=%v", isBatch, err)
|
|
}
|
|
if _, err := parseCurrentExpertJobStoreDisjointRequest(batchBody); err == nil {
|
|
t.Fatal("batch envelope must not be accepted by the single-item parser")
|
|
}
|
|
if _, isBatch, err := parseCurrentExpertJobStoreDisjointBatchRequest(batchBody[:len(batchBody)-1]); !isBatch || err == nil {
|
|
t.Fatalf("truncated batch isBatch=%t error=%v", isBatch, err)
|
|
}
|
|
duplicateSlot := append([]byte(nil), batchBody...)
|
|
binary.LittleEndian.PutUint16(duplicateSlot[12:14], 9)
|
|
if _, isBatch, err := parseCurrentExpertJobStoreDisjointBatchRequest(duplicateSlot); !isBatch || err == nil {
|
|
t.Fatalf("duplicate-slot batch isBatch=%t error=%v", isBatch, err)
|
|
}
|
|
if len(batchBody) != currentExpertJobStoreDisjointBatchHeaderSize+3*2 {
|
|
t.Fatalf("batch envelope length=%d", len(batchBody))
|
|
}
|
|
// 耐久失败码必须落在客户端 op235 显式表里:190 = dstr 44069。
|
|
// 原来的 189 在显式表和通用映射器里都不存在,客户端 `xor al,al; ret`,什么都不画。
|
|
if got := currentExpertJobStoreError(dnfexpertjob.ErrMachineEndurance); got != 190 {
|
|
t.Fatalf("endurance failure code=%d want 190 (dstr 44069); 189 renders no message", got)
|
|
}
|
|
enchantBody := make([]byte, 13)
|
|
binary.LittleEndian.PutUint16(enchantBody[0:2], 19)
|
|
binary.LittleEndian.PutUint32(enchantBody[2:6], 10015129)
|
|
enchantBody[6], enchantBody[7] = 2, 0
|
|
binary.LittleEndian.PutUint16(enchantBody[8:10], 10)
|
|
enchantBody[10] = 0
|
|
binary.LittleEndian.PutUint16(enchantBody[11:13], 11)
|
|
enchant, err := parseCurrentExpertJobStoreEnchantRequest(enchantBody)
|
|
if err != nil || enchant.OwnerID != 19 || enchant.RecipeID != 10015129 || enchant.TargetSlot != 10 || enchant.CardSlot != 11 {
|
|
t.Fatalf("enchant request=%+v error=%v", enchant, err)
|
|
}
|
|
disjointResult := buildCurrentExpertJobStoreDisjointSuccessBody(
|
|
currentExpertJobStoreDisjointRequest{OwnerID: 19, TargetSlot: 10, TargetList: 0},
|
|
currentExpertJobStoreUseResult{RequesterGold: 900, Endurance: 299, Materials: []currentExpertJobExtractionMaterial{{Slot: 11, ItemID: 2610024, Count: 5}}},
|
|
)
|
|
wantDisjointResult := []byte{1, 10, 0, 0, 19, 0, 1, 11, 0, 0x68, 0xd3, 0x27, 0, 5, 0, 0, 0, 0x84, 3, 0, 0, 0x2b, 1, 0, 0}
|
|
if !bytes.Equal(disjointResult, wantDisjointResult) {
|
|
t.Fatalf("disjoint result=%x want=%x", disjointResult, wantDisjointResult)
|
|
}
|
|
// Eleven bytes: the leading container byte the class-1 pump consumes as a2,
|
|
// then success/experience/reserved/endurance. Shortening this to ten made a
|
|
// live client render the machine's 耐久 as 805306369 - see the comment on
|
|
// buildCurrentExpertJobStoreEnchantSuccessBody.
|
|
enchantResult := buildCurrentExpertJobStoreEnchantSuccessBody(currentExpertJobStoreUseResult{Success: true, Experience: 42, Endurance: 297})
|
|
wantEnchantResult := []byte{1, 1, 42, 0, 0, 0, 0, 0x29, 1, 0, 0}
|
|
if !bytes.Equal(enchantResult, wantEnchantResult) {
|
|
t.Fatalf("enchant result=%x want=%x", enchantResult, wantEnchantResult)
|
|
}
|
|
}
|
|
|
|
func TestCurrentExpertJobGiveUpAndStoreNotificationContracts(t *testing.T) {
|
|
if currentExpertJobStoreCreateNotification != 538 || currentExpertJobStoreCloseNotification != 539 ||
|
|
currentExpertJobStoreUpdateNotification != 544 || currentExpertJobEnchantOwnerNotification != 533 {
|
|
t.Fatalf("store notifications create=%d close=%d update=%d owner=%d",
|
|
currentExpertJobStoreCreateNotification, currentExpertJobStoreCloseNotification,
|
|
currentExpertJobStoreUpdateNotification, currentExpertJobEnchantOwnerNotification)
|
|
}
|
|
body := buildCurrentExpertJobGiveUpSuccessBody(currentExpertJobGiveUpResult{FinalGold: 59000, State: 1, Cost: 1000})
|
|
want := []byte{1, 0x78, 0xe6, 0, 0, 1, 0}
|
|
if !bytes.Equal(body, want) {
|
|
t.Fatalf("give-up body=%x want=%x", body, want)
|
|
}
|
|
module := expertJobGameplayModule()
|
|
opcode := uint16(239)
|
|
if module.LegacyHandlers[opcode] == nil || module.UpperHandlers[opcode] == nil || module.LegacyNormalizers[opcode] == nil {
|
|
t.Fatalf("op239 is not registered in every gameplay route")
|
|
}
|
|
|
|
connection := &bufferConn{}
|
|
service := &Service{options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain}}
|
|
session := &gameSession{conn: connection, selectedCharacterID: 19}
|
|
if err := service.sendCurrentExpertJobClearedInfo(session, 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
packet, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
// The cleared form is two native bytes plus the mode block and the experience
|
|
// pair, and it is always silent: it restates an absent profession rather than
|
|
// announcing an acquisition. Byte 0 carries the give-up tier, which drives the
|
|
// escalating gold cost of giving the profession up again.
|
|
if len(rest) != 0 || packet.Header.Classification != 0 || packet.Header.MsgID != currentExpertJobInfoNotification ||
|
|
!bytes.Equal(packet.Body, []byte{
|
|
1, 0,
|
|
0x45, 0x4a, 0x58, 0x4d, 1, 0, 0, 0,
|
|
0x45, 0x4a, 0x58, 0x50, 0, 0, 0, 0,
|
|
}) {
|
|
t.Fatalf("cleared expert-job notification header=%+v body=%x trailing=%x", packet.Header, packet.Body, rest)
|
|
}
|
|
}
|
|
|
|
func TestCurrentExpertJobStorePairMutationCommitsAndRollsBackBothCharacters(t *testing.T) {
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
ctx := context.Background()
|
|
for _, id := range []string{"19", "20"} {
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: id, AccountID: "account-" + id, Stats: map[string]int64{"expert_job_exp": 0, "gold": 1000}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: id, Slots: map[string]dnfrepo.ItemStack{"0:10": {ItemID: 500, Count: 1}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := repositories.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{
|
|
AccountID: "account-19",
|
|
Slots: map[string]dnfrepo.ItemStack{"0:358": {ItemID: 3037, Count: 10}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := &Service{options: options{accountID: "account-19"}, repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }}
|
|
session := &gameSession{selectedCharacterID: 19}
|
|
store := ¤tExpertJobStore{OwnerCharacterID: 20}
|
|
if err := service.mutateCurrentExpertJobStorePair(ctx, session, store, func(requesterCharacter *dnfrepo.CharacterRecord, requesterInventory *dnfrepo.InventoryRecord, requesterAccount *dnfrepo.AccountInventoryRecord, ownerCharacter *dnfrepo.CharacterRecord, ownerInventory *dnfrepo.InventoryRecord) error {
|
|
requesterCharacter.Stats["expert_job_exp"] = 1
|
|
ownerCharacter.Stats["expert_job_exp"] = 2
|
|
currentExpertJobSetWalletGold(requesterCharacter, 900)
|
|
currentExpertJobSetWalletGold(ownerCharacter, 1100)
|
|
requesterAccount.Slots["0:358"] = dnfrepo.ItemStack{ItemID: 3037, Count: 15}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
requester, _, _ := repositories.Character.Load(ctx, "19")
|
|
owner, _, _ := repositories.Character.Load(ctx, "20")
|
|
if requester.Stats["gold"] != 900 || owner.Stats["gold"] != 1100 {
|
|
t.Fatalf("committed wallets requester=%d owner=%d", requester.Stats["gold"], owner.Stats["gold"])
|
|
}
|
|
requesterAccount, found, err := repositories.AccountInventory.Load(ctx, "account-19")
|
|
if err != nil || !found || requesterAccount.Slots["0:358"].Count != 15 {
|
|
t.Fatalf("committed requester account inventory found=%t err=%v slots=%+v", found, err, requesterAccount.Slots)
|
|
}
|
|
wantErr := errors.New("reject pair")
|
|
if err := service.mutateCurrentExpertJobStorePair(ctx, session, store, func(requesterCharacter *dnfrepo.CharacterRecord, _ *dnfrepo.InventoryRecord, requesterAccount *dnfrepo.AccountInventoryRecord, ownerCharacter *dnfrepo.CharacterRecord, _ *dnfrepo.InventoryRecord) error {
|
|
currentExpertJobSetWalletGold(requesterCharacter, 0)
|
|
currentExpertJobSetWalletGold(ownerCharacter, 0)
|
|
requesterAccount.Slots["0:358"] = dnfrepo.ItemStack{ItemID: 3037, Count: 0}
|
|
return wantErr
|
|
}); !errors.Is(err, wantErr) {
|
|
t.Fatalf("rollback error=%v", err)
|
|
}
|
|
requester, _, _ = repositories.Character.Load(ctx, "19")
|
|
owner, _, _ = repositories.Character.Load(ctx, "20")
|
|
if requester.Stats["gold"] != 900 || owner.Stats["gold"] != 1100 {
|
|
t.Fatalf("rolled-back wallets requester=%d owner=%d", requester.Stats["gold"], owner.Stats["gold"])
|
|
}
|
|
requesterAccount, found, err = repositories.AccountInventory.Load(ctx, "account-19")
|
|
if err != nil || !found || requesterAccount.Slots["0:358"].Count != 15 {
|
|
t.Fatalf("rolled-back requester account inventory found=%t err=%v slots=%+v", found, err, requesterAccount.Slots)
|
|
}
|
|
}
|
|
|
|
func TestConsumeCurrentExpertJobRecipeUsesCharacterGoldWithoutPersistedWalletRow(t *testing.T) {
|
|
character := &dnfrepo.CharacterRecord{Stats: map[string]int64{"gold": 1000}}
|
|
inventory := &dnfrepo.InventoryRecord{Slots: map[string]dnfrepo.ItemStack{
|
|
"0:10": {ItemID: 500, Count: 3},
|
|
}}
|
|
account := &dnfrepo.AccountInventoryRecord{Slots: map[string]dnfrepo.ItemStack{}}
|
|
accountChanged, err := consumeCurrentExpertJobRecipe(character, inventory, account, []dnfexpertjob.RecipeEntry{{ItemID: 500, Count: 2}}, 250)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if accountChanged {
|
|
t.Fatal("ordinary backpack material unexpectedly changed account inventory")
|
|
}
|
|
if character.Stats["gold"] != 750 || inventory.Slots["0:10"].Count != 1 {
|
|
t.Fatalf("committed gold=%d material=%d", character.Stats["gold"], inventory.Slots["0:10"].Count)
|
|
}
|
|
if _, exists := inventory.Slots["0:0"]; exists {
|
|
t.Fatal("expert-job recipe persisted a synthetic wallet row")
|
|
}
|
|
}
|
|
|
|
func TestConsumeCurrentExpertJobRecipeConsumesSharedCubeAndBackpackEssenceTogether(t *testing.T) {
|
|
character := &dnfrepo.CharacterRecord{Stats: map[string]int64{"gold": 1000}}
|
|
inventory := &dnfrepo.InventoryRecord{Slots: map[string]dnfrepo.ItemStack{
|
|
"0:10": {ItemID: 2610041, Count: 5},
|
|
}}
|
|
account := &dnfrepo.AccountInventoryRecord{Slots: map[string]dnfrepo.ItemStack{
|
|
dnfrepo.AccountSharedInventorySlotKey(359): {ItemID: 3262, Count: 20},
|
|
}}
|
|
changed, err := consumeCurrentExpertJobRecipe(character, inventory, account, []dnfexpertjob.RecipeEntry{
|
|
{ItemID: 3262, Count: 10},
|
|
{ItemID: 2610041, Count: 3},
|
|
}, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !changed || inventory.Slots["0:10"].Count != 2 || account.Slots[dnfrepo.AccountSharedInventorySlotKey(359)].Count != 10 {
|
|
t.Fatalf("shared/backpack consumption changed=%t inventory=%+v account=%+v", changed, inventory.Slots, account.Slots)
|
|
}
|
|
}
|
|
|
|
func TestCurrentExpertJobSuccessBodiesMatchCurrentEXEReaders(t *testing.T) {
|
|
compound := buildCurrentExpertJobCompoundSuccessBody(dnfexpertjob.CompoundPlan{AttemptedOutputs: []dnfexpertjob.RecipeEntry{{ItemID: 1110, Count: 3}}, SuccessCount: 2, FailureCount: 1})
|
|
wantCompound := []byte{1, 1, 0x56, 0x04, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0}
|
|
if !bytes.Equal(compound, wantCompound) {
|
|
t.Fatalf("compound body=%x want=%x", compound, wantCompound)
|
|
}
|
|
extraction := buildCurrentExpertJobExtractionSuccessBody(currentExpertJobExtractionResult{TargetList: 0, TargetSlot: 10, Materials: []currentExpertJobExtractionMaterial{{Slot: 11, ItemID: 2610024, Count: 5}}})
|
|
wantExtraction := []byte{1, 0, 10, 0, 1, 11, 0, 0x68, 0xd3, 0x27, 0, 5, 0, 0, 0}
|
|
if !bytes.Equal(extraction, wantExtraction) {
|
|
t.Fatalf("extraction body=%x want=%x", extraction, wantExtraction)
|
|
}
|
|
}
|
|
|
|
func TestCurrentExpertJobLearnedRecipeStatsAreTypedAndSorted(t *testing.T) {
|
|
stats := map[string]int64{"expert_job_recipe_2_2600150": 1, "expert_job_recipe_2_2600149": 1, "expert_job_recipe_4_2600083": 1, "expert_job_recipe_2_bad": 1, "expert_job_recipe_2_1": 0}
|
|
if got := currentExpertJobLearnedRecipes(stats, 2); !bytes.Equal(int64SliceBytes(got), int64SliceBytes([]int64{2600149, 2600150})) {
|
|
t.Fatalf("recipes=%v", got)
|
|
}
|
|
}
|
|
|
|
func int64SliceBytes(values []int64) []byte {
|
|
result := make([]byte, len(values)*8)
|
|
for index, value := range values {
|
|
binary.LittleEndian.PutUint64(result[index*8:], uint64(value))
|
|
}
|
|
return result
|
|
}
|