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,975 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
)
|
||||
|
||||
func assertCurrentEquipmentVectorEquals(t *testing.T, want, got []byte) {
|
||||
t.Helper()
|
||||
end := currentEquipmentVectorOffset + currentEquipmentEmblemDataBytes
|
||||
if len(want) < end || len(got) < end {
|
||||
t.Fatalf("raw row too short for current vector: want=%d got=%d", len(want), len(got))
|
||||
}
|
||||
if !bytes.Equal(want[currentEquipmentVectorOffset:end], got[currentEquipmentVectorOffset:end]) {
|
||||
t.Fatalf("current vector changed: got=%x want=%x", got[currentEquipmentVectorOffset:end], want[currentEquipmentVectorOffset:end])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentSocketOpenAndEmblemAttachPersistsPVFFields(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:5": {ItemID: 9001, Count: 1},
|
||||
"0:8": {ItemID: 6001, Count: 2},
|
||||
"0:9": {ItemID: 5001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "socket-test", conn: &bufferConn{}}
|
||||
|
||||
result, err := service.commitCurrentEquipmentSocketOpen(session, currentSocketOpenRequest{
|
||||
TargetSlot: 5,
|
||||
TargetItemID: 9001,
|
||||
MaterialSlot: 8,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("equipment socket open: %v", err)
|
||||
}
|
||||
if result.Target != (currentSocketChangedSlot{ListType: 0, Slot: 5}) || len(result.Consumed) != 1 || result.Consumed[0].Slot != 8 {
|
||||
t.Fatalf("open result=%+v", result)
|
||||
}
|
||||
|
||||
inventory := mustLoadCurrentSocketInventory(t, repositories, "77")
|
||||
if got := inventory.Slots["0:8"].Count; got != 1 {
|
||||
t.Fatalf("material count after open=%d want=1", got)
|
||||
}
|
||||
equipmentData := currentEquipmentEmblemData(inventory.Slots["0:5"].Extra, inventory.Slots["0:5"].RawEntry)
|
||||
if equipmentData[0] != 2 ||
|
||||
binary.LittleEndian.Uint32(equipmentData[1:5]) != 0xFFFFFFFF ||
|
||||
binary.LittleEndian.Uint32(equipmentData[5:9]) != 0xFFFFFFFF {
|
||||
t.Fatalf("opened equipment emblem data=%x", equipmentData)
|
||||
}
|
||||
// Current EXE vector at raw+0x3C: count=2 and two contiguous empty emblem
|
||||
// IDs.
|
||||
wantRaw := make([]byte, currentItemListEntryWireSize)
|
||||
wantRaw[currentEquipmentVectorOffset] = 2
|
||||
binary.LittleEndian.PutUint32(wantRaw[currentEquipmentVectorOffset+1:currentEquipmentVectorOffset+5], 0xFFFFFFFF)
|
||||
binary.LittleEndian.PutUint32(wantRaw[currentEquipmentVectorOffset+5:currentEquipmentVectorOffset+9], 0xFFFFFFFF)
|
||||
assertCurrentEquipmentVectorEquals(t, wantRaw, inventory.Slots["0:5"].RawEntry)
|
||||
if inventory.Slots["0:5"].Extra["equipment_emblem_data"] == "" ||
|
||||
inventory.Slots["0:5"].Extra["equipment_socket_data"] == "" ||
|
||||
inventory.Slots["0:5"].Extra["tail_data_2f"] != "" ||
|
||||
inventory.Slots["0:5"].Extra["tailData2F"] != "" {
|
||||
t.Fatalf("opened equipment wrote unexpected socket/tail aliases: %+v", inventory.Slots["0:5"].Extra)
|
||||
}
|
||||
|
||||
result, err = service.commitCurrentEquipmentEmblemAttach(session, currentEmblemAttachRequest{
|
||||
TargetSlot: 5,
|
||||
TargetItemID: 9001,
|
||||
Emblems: []currentEmblemApplyRequest{{
|
||||
EmblemSlot: 9,
|
||||
EmblemItemID: 5001,
|
||||
SocketIndex: 0,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("equipment emblem attach: %v", err)
|
||||
}
|
||||
if result.Target != (currentSocketChangedSlot{ListType: 0, Slot: 5}) || len(result.Consumed) != 1 || result.Consumed[0].Slot != 9 {
|
||||
t.Fatalf("attach result=%+v", result)
|
||||
}
|
||||
|
||||
inventory = mustLoadCurrentSocketInventory(t, repositories, "77")
|
||||
if _, ok := inventory.Slots["0:9"]; ok {
|
||||
t.Fatalf("emblem stack was not consumed: %+v", inventory.Slots["0:9"])
|
||||
}
|
||||
equipmentData = currentEquipmentEmblemData(inventory.Slots["0:5"].Extra, inventory.Slots["0:5"].RawEntry)
|
||||
if equipmentData[0] != 2 ||
|
||||
binary.LittleEndian.Uint32(equipmentData[1:5]) != 5001 ||
|
||||
binary.LittleEndian.Uint32(equipmentData[5:9]) != 0xFFFFFFFF {
|
||||
t.Fatalf("attached equipment emblem data=%x", equipmentData)
|
||||
}
|
||||
// After emblem attach: count=2, emblem[0]=5001, emblem[1]=FFFFFFFF.
|
||||
wantAttached := make([]byte, currentItemListEntryWireSize)
|
||||
wantAttached[currentEquipmentVectorOffset] = 2
|
||||
binary.LittleEndian.PutUint32(wantAttached[currentEquipmentVectorOffset+1:currentEquipmentVectorOffset+5], 5001)
|
||||
binary.LittleEndian.PutUint32(wantAttached[currentEquipmentVectorOffset+5:currentEquipmentVectorOffset+9], 0xFFFFFFFF)
|
||||
assertCurrentEquipmentVectorEquals(t, wantAttached, inventory.Slots["0:5"].RawEntry)
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentTailDataAliasesProjectSocketVectorIntoEquipmentRows(t *testing.T) {
|
||||
raw := make([]byte, currentItemListEntryWireSize)
|
||||
binary.LittleEndian.PutUint16(raw[0:2], 17)
|
||||
binary.LittleEndian.PutUint32(raw[2:6], 9001)
|
||||
zeroTail := make([]byte, currentEquipmentTailDataBytes)
|
||||
openTail := make([]byte, currentEquipmentTailDataBytes)
|
||||
openTail[0] = 2
|
||||
binary.LittleEndian.PutUint32(openTail[1:5], 0xFFFFFFFF)
|
||||
binary.LittleEndian.PutUint32(openTail[5:9], 0xFFFFFFFF)
|
||||
extra := map[string]string{"current_exe_equipment_type": "17"}
|
||||
currentSetHexExtra(extra, "tail_data_2f", zeroTail)
|
||||
currentSetHexExtra(extra, "tailData2F", openTail)
|
||||
equipped := dnfrepo.EquipmentEntry{SlotIndex: 17, ItemID: 9001, RawEntry: raw, Extra: extra}
|
||||
|
||||
row, ok := currentItemListEntryFromEquipment(equipped)
|
||||
if !ok {
|
||||
t.Fatal("currentItemListEntryFromEquipment rejected equipped row")
|
||||
}
|
||||
wantRaw := make([]byte, currentItemListEntryWireSize)
|
||||
copy(wantRaw, raw)
|
||||
wantRaw[currentEquipmentVectorOffset] = 2
|
||||
binary.LittleEndian.PutUint32(wantRaw[currentEquipmentVectorOffset+1:currentEquipmentVectorOffset+5], 0xFFFFFFFF)
|
||||
binary.LittleEndian.PutUint32(wantRaw[currentEquipmentVectorOffset+5:currentEquipmentVectorOffset+9], 0xFFFFFFFF)
|
||||
assertCurrentEquipmentVectorEquals(t, wantRaw, row.data[:])
|
||||
}
|
||||
|
||||
func TestCurrentMode1EquipmentCreateProjectsPersistedSocketVectorOnFirstLogin(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
raw := make([]byte, currentItemListEntryWireSize)
|
||||
raw[currentEquipmentVectorOffset] = 2
|
||||
binary.LittleEndian.PutUint32(raw[currentEquipmentVectorOffset+1:currentEquipmentVectorOffset+5], 5001)
|
||||
binary.LittleEndian.PutUint32(raw[currentEquipmentVectorOffset+5:currentEquipmentVectorOffset+9], math.MaxUint32)
|
||||
if err := repositories.Equipment.Save(ctx, dnfrepo.EquipmentRecord{
|
||||
CharacterID: "19",
|
||||
Entries: map[string]dnfrepo.EquipmentEntry{
|
||||
"17": {
|
||||
SlotIndex: 17,
|
||||
ItemID: 9001,
|
||||
RawEntry: raw,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service := &Service{repositoryProvider: func() (dnfrepo.Group, bool) {
|
||||
return repositories, true
|
||||
}}
|
||||
reader := csharpLegacyUserInfoReader{
|
||||
ctx: ctx,
|
||||
characterID: "19",
|
||||
service: service,
|
||||
}
|
||||
rows := reader.currentMode1EquipmentObjectRows()
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("mode1 equipment rows=%d want=1", len(rows))
|
||||
}
|
||||
want := []byte{0x89, 0x13, 0, 0, 0xff, 0xff, 0xff, 0xff}
|
||||
if !bytes.Equal(rows[0].vector225CCA0, want) {
|
||||
t.Fatalf("mode1 socket vector=%x want=%x", rows[0].vector225CCA0, want)
|
||||
}
|
||||
if len(currentMode1EquipmentCreateRows(rows)) != 1 {
|
||||
t.Fatal("socket-bearing equipment row was not accepted for mode1 create")
|
||||
}
|
||||
|
||||
var writer packetWriter
|
||||
writeCurrentMode1EquipmentCreateRow(&writer, rows[0])
|
||||
body := writer.bytes()
|
||||
pos := 24
|
||||
if body[pos] != 0 {
|
||||
t.Fatalf("mode1 sub_1D6E020 count=%d want=0", body[pos])
|
||||
}
|
||||
pos++ // sub_1D6E020 count
|
||||
pos += 4 // state112
|
||||
if body[pos] != 2 || !bytes.Equal(body[pos+1:pos+9], want) {
|
||||
t.Fatalf("mode1 sub_225CCA0 wire=%x want count2+%x", body[pos:pos+9], want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentEmblemDataReadsCurrentRawVectorWithoutLegacyExtra(t *testing.T) {
|
||||
raw := make([]byte, currentItemListEntryWireSize)
|
||||
raw[currentEquipmentVectorOffset] = 2
|
||||
binary.LittleEndian.PutUint32(raw[currentEquipmentVectorOffset+1:currentEquipmentVectorOffset+5], 5001)
|
||||
binary.LittleEndian.PutUint32(raw[currentEquipmentVectorOffset+5:currentEquipmentVectorOffset+9], math.MaxUint32)
|
||||
|
||||
data := currentEquipmentEmblemData(nil, raw)
|
||||
if data[0] != 2 ||
|
||||
binary.LittleEndian.Uint32(data[1:5]) != 5001 ||
|
||||
binary.LittleEndian.Uint32(data[5:9]) != math.MaxUint32 {
|
||||
t.Fatalf("current raw socket vector decoded as %x", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentVectorIsPreservedFromPersistedRow(t *testing.T) {
|
||||
raw := make([]byte, currentItemListEntryWireSize)
|
||||
raw[currentEquipmentVectorOffset] = 2
|
||||
binary.LittleEndian.PutUint32(raw[currentEquipmentVectorOffset+1:], math.MaxUint32)
|
||||
binary.LittleEndian.PutUint32(raw[currentEquipmentVectorOffset+5:], math.MaxUint32)
|
||||
stack := dnfrepo.ItemStack{ItemID: 9001, Count: 1, RawEntry: append([]byte(nil), raw...)}
|
||||
entry := currentItemListEntryFromStack(currentSocketListMain, 5, stack)
|
||||
assertCurrentEquipmentVectorEquals(t, raw, entry.data[:])
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentProjectionMigratesOnlyExactLegacyWrongVector(t *testing.T) {
|
||||
data := [currentEquipmentEmblemDataBytes]byte{2}
|
||||
binary.LittleEndian.PutUint32(data[1:5], math.MaxUint32)
|
||||
binary.LittleEndian.PutUint32(data[5:9], math.MaxUint32)
|
||||
extra := map[string]string{"item_kind": "equipment"}
|
||||
currentSetEquipmentEmblemDataExtra(extra, data)
|
||||
|
||||
var migrated currentItemListEntry
|
||||
oldOffset := currentLegacyWrongEquipmentVectorOffset
|
||||
migrated.data[oldOffset] = 2
|
||||
binary.LittleEndian.PutUint32(migrated.data[oldOffset+1:oldOffset+5], math.MaxUint32)
|
||||
binary.LittleEndian.PutUint32(migrated.data[oldOffset+5:oldOffset+9], math.MaxUint32)
|
||||
currentApplyEquipmentSocketVectorToEntry(&migrated, extra)
|
||||
if !bytes.Equal(migrated.data[oldOffset:oldOffset+17], make([]byte, 17)) {
|
||||
t.Fatalf("legacy wrong vector was not cleared: %x", migrated.data[oldOffset:oldOffset+17])
|
||||
}
|
||||
if migrated.data[currentEquipmentVectorOffset] != 2 ||
|
||||
binary.LittleEndian.Uint32(migrated.data[currentEquipmentVectorOffset+1:currentEquipmentVectorOffset+5]) != math.MaxUint32 ||
|
||||
binary.LittleEndian.Uint32(migrated.data[currentEquipmentVectorOffset+5:currentEquipmentVectorOffset+9]) != math.MaxUint32 {
|
||||
t.Fatalf("current emblem vector=%x", migrated.data[currentEquipmentVectorOffset:currentEquipmentVectorOffset+currentEquipmentEmblemDataBytes])
|
||||
}
|
||||
|
||||
var preserved currentItemListEntry
|
||||
preserved.data[oldOffset] = 2
|
||||
binary.LittleEndian.PutUint32(preserved.data[oldOffset+1:oldOffset+5], math.MaxUint32)
|
||||
binary.LittleEndian.PutUint32(preserved.data[oldOffset+5:oldOffset+9], math.MaxUint32)
|
||||
preserved.data[oldOffset+9] = 0x7A
|
||||
currentApplyEquipmentSocketVectorToEntry(&preserved, extra)
|
||||
if preserved.data[oldOffset+9] != 0x7A {
|
||||
t.Fatalf("nonmatching raw+0x27 record was modified: %x", preserved.data[oldOffset:oldOffset+17])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentNoBody796SuccessAckHasNoBusinessBody(t *testing.T) {
|
||||
if got := buildCurrentNoBody796AckBody(); len(got) != 0 {
|
||||
t.Fatalf("op796 success business body=%x want empty", got)
|
||||
}
|
||||
avatar := buildCurrentAvatarEmblemAttachAckBody(currentEmblemAttachRequest{TargetSlot: 2, TargetItemID: 9101, Emblems: []currentEmblemApplyRequest{{EmblemSlot: 8}}})
|
||||
if len(avatar) != 7 {
|
||||
t.Fatalf("avatar emblem success business body len=%d want=7", len(avatar))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentAvatarSocketOpenAndEmblemAttachPersistsOptionalBlob(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"1:2": {ItemID: 9101, Count: 1},
|
||||
"0:8": {ItemID: 6001, Count: 1},
|
||||
"0:9": {ItemID: 5001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "avatar-socket-test", conn: &bufferConn{}}
|
||||
|
||||
if _, err := service.commitCurrentAvatarSocketOpen(session, currentSocketOpenRequest{
|
||||
TargetSlot: 2,
|
||||
TargetItemID: 9101,
|
||||
MaterialSlot: 8,
|
||||
}); err != nil {
|
||||
t.Fatalf("avatar socket open: %v", err)
|
||||
}
|
||||
|
||||
inventory := mustLoadCurrentSocketInventory(t, repositories, "77")
|
||||
if _, ok := inventory.Slots["0:8"]; ok {
|
||||
t.Fatalf("avatar socket material was not consumed: %+v", inventory.Slots["0:8"])
|
||||
}
|
||||
avatarData := currentAvatarSocketData(inventory.Slots["1:2"].Extra)
|
||||
if avatarData[0] != 0x04 || avatarData[1] != 0x00 || avatarData[6] != 0x10 || avatarData[7] != 0x00 {
|
||||
t.Fatalf("opened avatar socket data=%x", avatarData)
|
||||
}
|
||||
|
||||
if _, err := service.commitCurrentAvatarEmblemAttach(session, currentEmblemAttachRequest{
|
||||
TargetSlot: 2,
|
||||
TargetItemID: 9101,
|
||||
Emblems: []currentEmblemApplyRequest{{
|
||||
EmblemSlot: 9,
|
||||
EmblemItemID: 5001,
|
||||
SocketIndex: 0,
|
||||
}},
|
||||
}); err != nil {
|
||||
t.Fatalf("avatar emblem attach: %v", err)
|
||||
}
|
||||
|
||||
inventory = mustLoadCurrentSocketInventory(t, repositories, "77")
|
||||
if _, ok := inventory.Slots["0:9"]; ok {
|
||||
t.Fatalf("avatar emblem stack was not consumed: %+v", inventory.Slots["0:9"])
|
||||
}
|
||||
avatarData = currentAvatarSocketData(inventory.Slots["1:2"].Extra)
|
||||
if avatarData[0] != 0x04 || binary.LittleEndian.Uint32(avatarData[2:6]) != 5001 {
|
||||
t.Fatalf("attached avatar socket data=%x", avatarData)
|
||||
}
|
||||
|
||||
entry := currentItemListEntryFromStack(1, 2, inventory.Slots["1:2"])
|
||||
body := buildCurrentItemUpdateBody(1, []currentItemListEntry{entry})
|
||||
if len(body) != 3+currentItemListEntryWireSize+4+currentAvatarSocketBytes+4 {
|
||||
t.Fatalf("list1 update body len=%d body=%x", len(body), body)
|
||||
}
|
||||
if !bytes.Equal(body[:3], []byte{1, 1, 0}) {
|
||||
t.Fatalf("list1 update header=%x", body[:3])
|
||||
}
|
||||
offset := 3 + currentItemListEntryWireSize
|
||||
if got := binary.LittleEndian.Uint32(body[offset : offset+4]); got != currentAvatarSocketBytes {
|
||||
t.Fatalf("avatar socket blob length=%d want=%d body=%x", got, currentAvatarSocketBytes, body[offset:])
|
||||
}
|
||||
blob := body[offset+4 : offset+4+currentAvatarSocketBytes]
|
||||
if !bytes.Equal(blob, avatarData[:]) {
|
||||
t.Fatalf("avatar socket blob=%x want=%x", blob, avatarData)
|
||||
}
|
||||
if got := binary.LittleEndian.Uint32(body[offset+4+currentAvatarSocketBytes:]); got != 0 {
|
||||
t.Fatalf("second avatar optional blob length=%d want=0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentNormalEquipmentOp14DoesNotAppendAvatarOptionalBlobs(t *testing.T) {
|
||||
var opened [currentEquipmentEmblemDataBytes]byte
|
||||
currentEnsureEquipmentEmblemSocketsOpen(&opened, 2, 2)
|
||||
equipped := dnfrepo.EquipmentEntry{
|
||||
SlotIndex: 13,
|
||||
ItemID: 9001,
|
||||
Extra: map[string]string{"current_exe_equipment_type": "14"},
|
||||
}
|
||||
currentApplyEquipmentEmblemDataToEquipment(&equipped, opened, 0x04)
|
||||
entry, ok := currentItemListEntryFromEquipment(equipped)
|
||||
if !ok {
|
||||
t.Fatal("normal equipment row was rejected")
|
||||
}
|
||||
|
||||
body := buildCurrentItemUpdateBody(currentSocketListEquipment, []currentItemListEntry{entry})
|
||||
if len(body) != 3+currentItemListEntryWireSize ||
|
||||
body[0] != currentSocketListEquipment ||
|
||||
binary.LittleEndian.Uint16(body[1:3]) != 1 {
|
||||
t.Fatalf("list-3 op14 body=%x", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentSocketRequestParsersMatch86JPLayouts(t *testing.T) {
|
||||
var socket packetWriter
|
||||
socket.writeUint16(5)
|
||||
socket.writeUint32(9001)
|
||||
socket.writeUint16(8)
|
||||
|
||||
open, err := decodeCurrentSocketOpenRequest(socket.bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("decode socket open: %v", err)
|
||||
}
|
||||
if open.TargetSlot != 5 || open.TargetItemID != 9001 || open.MaterialSlot != 8 {
|
||||
t.Fatalf("open=%+v", open)
|
||||
}
|
||||
if got := stripLegacySocketOpenTransportTrailer(append(socket.bytes(), 0, 0, 0, 0)); !bytes.Equal(got, socket.bytes()) {
|
||||
t.Fatalf("legacy socket strip=%x want=%x", got, socket.bytes())
|
||||
}
|
||||
|
||||
emblemBody := buildCurrentSocketTestEmblemBody(5, 9001,
|
||||
currentEmblemApplyRequest{EmblemSlot: 9, EmblemItemID: 5001, SocketIndex: 0},
|
||||
currentEmblemApplyRequest{EmblemSlot: 10, EmblemItemID: 5002, SocketIndex: 1},
|
||||
)
|
||||
attach, err := decodeCurrentEmblemAttachRequest(emblemBody)
|
||||
if err != nil {
|
||||
t.Fatalf("decode emblem attach: %v", err)
|
||||
}
|
||||
if attach.TargetSlot != 5 || attach.TargetItemID != 9001 || len(attach.Emblems) != 2 ||
|
||||
attach.Emblems[0].EmblemSlot != 9 || attach.Emblems[0].EmblemItemID != 5001 || attach.Emblems[0].SocketIndex != 0 ||
|
||||
attach.Emblems[1].EmblemSlot != 10 || attach.Emblems[1].EmblemItemID != 5002 || attach.Emblems[1].SocketIndex != 1 {
|
||||
t.Fatalf("attach=%+v", attach)
|
||||
}
|
||||
if got := stripLegacyEmblemAttachTransportTrailer(append(emblemBody, 0, 0, 0, 0)); !bytes.Equal(got, emblemBody) {
|
||||
t.Fatalf("legacy emblem strip=%x want=%x", got, emblemBody)
|
||||
}
|
||||
if want := []byte{2, 9, 0, 0x89, 0x13, 0, 0, 10, 0, 0x8A, 0x13, 0, 0}; !bytes.Equal(buildCurrentEquipmentEmblemAttachAckBody(attach), want) {
|
||||
t.Fatalf("equipment emblem op913 ack=%x want=%x", buildCurrentEquipmentEmblemAttachAckBody(attach), want)
|
||||
}
|
||||
|
||||
avatarPrefixed := append([]byte{currentSocketListAvatar}, emblemBody...)
|
||||
avatarAttach, err := decodeCurrentAvatarEmblemAttachRequest(avatarPrefixed)
|
||||
if err != nil {
|
||||
t.Fatalf("decode avatar-prefixed attach: %v", err)
|
||||
}
|
||||
if avatarAttach.TargetSlot != attach.TargetSlot || avatarAttach.TargetItemID != attach.TargetItemID || len(avatarAttach.Emblems) != len(attach.Emblems) {
|
||||
t.Fatalf("avatar attach=%+v want=%+v", avatarAttach, attach)
|
||||
}
|
||||
if got := stripLegacyEmblemAttachTransportTrailer(append(avatarPrefixed, 0, 0, 0, 0)); !bytes.Equal(got, avatarPrefixed) {
|
||||
t.Fatalf("legacy avatar emblem strip=%x want=%x", got, avatarPrefixed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentEmblemAttachDecodesCapturedOp913Body(t *testing.T) {
|
||||
body := []byte{
|
||||
0x0E, 0x00, 0x8D, 0x8A, 0xF9, 0x05, 0x02,
|
||||
0x28, 0x01, 0xB7, 0x25, 0x26, 0x00, 0x00,
|
||||
0x27, 0x01, 0xC1, 0x25, 0x26, 0x00, 0x01,
|
||||
}
|
||||
request, err := decodeCurrentEmblemAttachRequest(body)
|
||||
if err != nil {
|
||||
t.Fatalf("decode captured op913: %v", err)
|
||||
}
|
||||
if request.TargetSlot != 14 || request.TargetItemID != 100240013 || len(request.Emblems) != 2 ||
|
||||
request.Emblems[0] != (currentEmblemApplyRequest{EmblemSlot: 296, EmblemItemID: 2500023, SocketIndex: 0}) ||
|
||||
request.Emblems[1] != (currentEmblemApplyRequest{EmblemSlot: 295, EmblemItemID: 2500033, SocketIndex: 1}) {
|
||||
t.Fatalf("captured op913 request=%+v", request)
|
||||
}
|
||||
if want := []byte{
|
||||
0x02,
|
||||
0x28, 0x01, 0xB7, 0x25, 0x26, 0x00,
|
||||
0x27, 0x01, 0xC1, 0x25, 0x26, 0x00,
|
||||
}; !bytes.Equal(buildCurrentEquipmentEmblemAttachAckBody(request), want) {
|
||||
t.Fatalf("captured op913 ack=%x want=%x", buildCurrentEquipmentEmblemAttachAckBody(request), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentEmblemAttachHandlerMatchesCapturedOp913AndRefreshesRows(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
var opened [currentEquipmentEmblemDataBytes]byte
|
||||
currentEnsureEquipmentEmblemSocketsOpen(&opened, 2, 2)
|
||||
target := dnfrepo.ItemStack{ItemID: 9001, Count: 1}
|
||||
currentApplyEquipmentEmblemDataToStack(&target, currentSocketListMain, 5, opened, 0)
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:5": target,
|
||||
"0:9": {ItemID: 5001, Count: 1},
|
||||
"0:10": {ItemID: 5002, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "equipment-emblem-op913", conn: &bufferConn{}}
|
||||
requestBody := buildCurrentSocketTestEmblemBody(5, 9001,
|
||||
currentEmblemApplyRequest{EmblemSlot: 9, EmblemItemID: 5001, SocketIndex: 0},
|
||||
currentEmblemApplyRequest{EmblemSlot: 10, EmblemItemID: 5002, SocketIndex: 1},
|
||||
)
|
||||
|
||||
if err := service.handleCurrentEquipmentEmblemAttach(session, requestBody); err != nil {
|
||||
t.Fatalf("handle equipment emblem op913: %v", err)
|
||||
}
|
||||
ack, rest := splitGameServerUpperPacket(t, session.conn.(*bufferConn).write.Bytes())
|
||||
if ack.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
ack.Header.MsgID != currentEquipmentEmblemAttachOpcode {
|
||||
t.Fatalf("op913 ack header=%+v", ack.Header)
|
||||
}
|
||||
request, err := decodeCurrentEmblemAttachRequest(requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("decode request for expected ack: %v", err)
|
||||
}
|
||||
if want := upperSuccessBody(buildCurrentEquipmentEmblemAttachAckBody(request)); !bytes.Equal(ack.Body, want) {
|
||||
t.Fatalf("op913 ack body=%x want=%x", ack.Body, want)
|
||||
}
|
||||
targetRefresh, rest := splitGameServerUpperPacket(t, rest)
|
||||
if targetRefresh.Header.Classification != 0 ||
|
||||
targetRefresh.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) {
|
||||
t.Fatalf("target refresh header=%+v", targetRefresh.Header)
|
||||
}
|
||||
for index := 0; index < 2; index++ {
|
||||
consumed, next := splitGameServerUpperPacket(t, rest)
|
||||
rest = next
|
||||
if consumed.Header.Classification != 0 ||
|
||||
consumed.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) {
|
||||
t.Fatalf("consumed[%d] refresh header=%+v", index, consumed.Header)
|
||||
}
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("unexpected trailing packets after incremental op913 refreshes=%x", rest)
|
||||
}
|
||||
|
||||
inventory := mustLoadCurrentSocketInventory(t, repositories, "77")
|
||||
if _, ok := inventory.Slots["0:9"]; ok {
|
||||
t.Fatal("first emblem was not consumed")
|
||||
}
|
||||
if _, ok := inventory.Slots["0:10"]; ok {
|
||||
t.Fatal("second emblem was not consumed")
|
||||
}
|
||||
data := currentEquipmentEmblemData(inventory.Slots["0:5"].Extra, inventory.Slots["0:5"].RawEntry)
|
||||
if data[0] != 2 ||
|
||||
binary.LittleEndian.Uint32(data[1:5]) != 5001 ||
|
||||
binary.LittleEndian.Uint32(data[5:9]) != 5002 {
|
||||
t.Fatalf("attached equipment emblem data=%x", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentAvatarSocketOpenHandlerSendsAckAndVisibleSlotRefreshes(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"1:2": {ItemID: 9101, Count: 1},
|
||||
"0:8": {ItemID: 6001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "avatar-handler-test", conn: &bufferConn{}}
|
||||
var request packetWriter
|
||||
request.writeUint16(2)
|
||||
request.writeUint32(9101)
|
||||
request.writeUint16(8)
|
||||
|
||||
if err := service.handleCurrentAvatarSocketOpen(session, request.bytes()); err != nil {
|
||||
t.Fatalf("handle avatar socket open: %v", err)
|
||||
}
|
||||
|
||||
ack, rest := splitGameServerUpperPacket(t, session.conn.(*bufferConn).write.Bytes())
|
||||
if ack.Header.Classification != dnfproto.DefaultChannelClassification || ack.Header.MsgID != currentAvatarSocketOpenOpcode {
|
||||
t.Fatalf("ack header=%+v", ack.Header)
|
||||
}
|
||||
if want := upperSuccessBody(buildCurrentSocketOpenAckBody(currentSocketOpenRequest{TargetSlot: 2, TargetItemID: 9101, MaterialSlot: 8})); !bytes.Equal(ack.Body, want) {
|
||||
t.Fatalf("ack body=%x want=%x", ack.Body, want)
|
||||
}
|
||||
|
||||
target, rest := splitGameServerUpperPacket(t, rest)
|
||||
if target.Header.Classification != 0 || target.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) {
|
||||
t.Fatalf("target refresh header=%+v", target.Header)
|
||||
}
|
||||
if len(target.Body) != 3+currentItemListEntryWireSize+4+currentAvatarSocketBytes+4 ||
|
||||
target.Body[0] != currentSocketListAvatar ||
|
||||
binary.LittleEndian.Uint16(target.Body[1:3]) != 1 ||
|
||||
binary.LittleEndian.Uint16(target.Body[3:5]) != 2 ||
|
||||
binary.LittleEndian.Uint32(target.Body[5:9]) != 9101 {
|
||||
t.Fatalf("target refresh body=%x", target.Body)
|
||||
}
|
||||
offset := 3 + currentItemListEntryWireSize
|
||||
if got := binary.LittleEndian.Uint32(target.Body[offset : offset+4]); got != currentAvatarSocketBytes {
|
||||
t.Fatalf("target socket blob length=%d body=%x", got, target.Body[offset:])
|
||||
}
|
||||
blob := target.Body[offset+4 : offset+4+currentAvatarSocketBytes]
|
||||
if blob[0] != 0x04 || blob[1] != 0 || blob[6] != 0x10 || blob[7] != 0 {
|
||||
t.Fatalf("target socket blob=%x", blob)
|
||||
}
|
||||
if got := binary.LittleEndian.Uint32(target.Body[offset+4+currentAvatarSocketBytes:]); got != 0 {
|
||||
t.Fatalf("target color blob length=%d want=0", got)
|
||||
}
|
||||
|
||||
material, trailing := splitGameServerUpperPacket(t, rest)
|
||||
if material.Header.Classification != 0 || material.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) {
|
||||
t.Fatalf("material refresh header=%+v", material.Header)
|
||||
}
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("trailing packets=%x", trailing)
|
||||
}
|
||||
if len(material.Body) != 3+currentItemListEntryWireSize ||
|
||||
material.Body[0] != currentSocketListMain ||
|
||||
binary.LittleEndian.Uint16(material.Body[1:3]) != 1 ||
|
||||
binary.LittleEndian.Uint16(material.Body[3:5]) != 8 ||
|
||||
binary.LittleEndian.Uint32(material.Body[5:9]) != 0 ||
|
||||
binary.LittleEndian.Uint32(material.Body[9:13]) != 0 {
|
||||
t.Fatalf("material refresh body=%x", material.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentSocketOpenHandlerRejectsWornEquipmentWithOp914Failure(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:70": {ItemID: 6001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
if err := repositories.Equipment.Save(ctx, dnfrepo.EquipmentRecord{
|
||||
CharacterID: "77",
|
||||
Entries: map[string]dnfrepo.EquipmentEntry{
|
||||
"17": {
|
||||
SlotIndex: 17,
|
||||
ItemID: 9001,
|
||||
Extra: map[string]string{"current_exe_equipment_type": "17"},
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save equipment: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "equipment-socket-handler-test", conn: &bufferConn{}}
|
||||
request := currentSocketOpenRequest{TargetSlot: 17, TargetItemID: 9001, MaterialSlot: 70}
|
||||
var body packetWriter
|
||||
body.writeUint16(uint16(request.TargetSlot))
|
||||
body.writeUint32(uint32(request.TargetItemID))
|
||||
body.writeUint16(uint16(request.MaterialSlot))
|
||||
|
||||
if err := service.handleCurrentEquipmentSocketOpen(session, body.bytes()); err != nil {
|
||||
t.Fatalf("handle equipment socket open: %v", err)
|
||||
}
|
||||
|
||||
failure, trailing := splitGameServerUpperPacket(t, session.conn.(*bufferConn).write.Bytes())
|
||||
if failure.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
failure.Header.MsgID != uint16(dnfenum.CmdPacketAddEquipmentSocket) ||
|
||||
!bytes.Equal(failure.Body, []byte{0, 4}) ||
|
||||
len(trailing) != 0 {
|
||||
t.Fatalf("failure packet=%+v body=%x trailing=%x", failure.Header, failure.Body, trailing)
|
||||
}
|
||||
if inventory := mustLoadCurrentSocketInventory(t, repositories, "77"); inventory.Slots["0:70"].Count != 1 {
|
||||
t.Fatalf("socket opener was consumed on worn reject: %+v", inventory.Slots)
|
||||
}
|
||||
equipment, found, err := repositories.Equipment.Load(ctx, "77")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load equipment found=%v err=%v", found, err)
|
||||
}
|
||||
data := currentEquipmentEmblemData(equipment.Entries["17"].Extra, equipment.Entries["17"].RawEntry)
|
||||
if data[0] != 0 || equipment.Entries["17"].Extra["equipment_emblem_data"] != "" || equipment.Entries["17"].Extra["tail_data_2f"] != "" {
|
||||
t.Fatalf("worn equipment mutated after rejected open data=%x extra=%+v", data, equipment.Entries["17"].Extra)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentSocketOpenHandlerSucceedsAndConsumesMaterial(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:5": {ItemID: 9001, Count: 1},
|
||||
"0:70": {ItemID: 6001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "equipment-socket-success", conn: &bufferConn{}}
|
||||
var body packetWriter
|
||||
body.writeUint16(5)
|
||||
body.writeUint32(9001)
|
||||
body.writeUint16(70)
|
||||
|
||||
if err := service.handleCurrentEquipmentSocketOpen(session, body.bytes()); err != nil {
|
||||
t.Fatalf("handle equipment socket open: %v", err)
|
||||
}
|
||||
// Expect success ACK (class1/op914), the target list-0 op14 row, and the
|
||||
// authoritative material deletion row. Current sub_1D73120 decodes the
|
||||
// target row through sub_225CD00 and applies the socket vector via vtable
|
||||
// method +280, so no full op13 inventory rebuild is needed.
|
||||
written := session.conn.(*bufferConn).write.Bytes()
|
||||
if len(written) == 0 {
|
||||
t.Fatal("no packets written after successful socket open")
|
||||
}
|
||||
ack, rest := splitGameServerUpperPacket(t, written)
|
||||
if ack.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
ack.Header.MsgID != currentEquipmentSocketOpenOpcode {
|
||||
t.Fatalf("ack packet=%+v", ack.Header)
|
||||
}
|
||||
if want := upperSuccessBody(buildCurrentEquipmentSocketOpenAckBody(currentSocketOpenRequest{
|
||||
TargetSlot: 5,
|
||||
TargetItemID: 9001,
|
||||
MaterialSlot: 70,
|
||||
})); !bytes.Equal(ack.Body, want) {
|
||||
t.Fatalf("ack body=%x want=%x", ack.Body, want)
|
||||
}
|
||||
target, rest := splitGameServerUpperPacket(t, rest)
|
||||
if target.Header.Classification != 0 ||
|
||||
target.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) ||
|
||||
len(target.Body) != 3+currentItemListEntryWireSize ||
|
||||
target.Body[0] != currentSocketListMain ||
|
||||
binary.LittleEndian.Uint16(target.Body[1:3]) != 1 {
|
||||
t.Fatalf("target refresh header=%+v body=%x", target.Header, target.Body)
|
||||
}
|
||||
targetRow := target.Body[3:]
|
||||
if binary.LittleEndian.Uint16(targetRow[0:2]) != 5 ||
|
||||
binary.LittleEndian.Uint32(targetRow[2:6]) != 9001 ||
|
||||
targetRow[currentEquipmentVectorOffset] != 2 ||
|
||||
binary.LittleEndian.Uint32(targetRow[currentEquipmentVectorOffset+1:currentEquipmentVectorOffset+5]) != math.MaxUint32 ||
|
||||
binary.LittleEndian.Uint32(targetRow[currentEquipmentVectorOffset+5:currentEquipmentVectorOffset+9]) != math.MaxUint32 {
|
||||
t.Fatalf("target socket row=%x", targetRow)
|
||||
}
|
||||
material, rest := splitGameServerUpperPacket(t, rest)
|
||||
if material.Header.Classification != 0 ||
|
||||
material.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) ||
|
||||
len(material.Body) != 3+currentItemListEntryWireSize ||
|
||||
material.Body[0] != currentSocketListMain ||
|
||||
binary.LittleEndian.Uint16(material.Body[1:3]) != 1 ||
|
||||
binary.LittleEndian.Uint16(material.Body[3:5]) != 70 ||
|
||||
binary.LittleEndian.Uint32(material.Body[5:9]) != 0 {
|
||||
t.Fatalf("material refresh header=%+v body=%x", material.Header, material.Body)
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("unexpected trailing packets after incremental op914 refreshes=%x", rest)
|
||||
}
|
||||
// Material must be consumed.
|
||||
inventory := mustLoadCurrentSocketInventory(t, repositories, "77")
|
||||
if _, ok := inventory.Slots["0:70"]; ok {
|
||||
t.Fatalf("socket opener not consumed: %+v", inventory.Slots)
|
||||
}
|
||||
// Target must have socket data.
|
||||
data := currentEquipmentEmblemData(inventory.Slots["0:5"].Extra, inventory.Slots["0:5"].RawEntry)
|
||||
if data[0] != 2 {
|
||||
t.Fatalf("target socket count=%d want=2", data[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentNoBody796AcknowledgesWithoutInventoryMutation(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:5": {ItemID: 9001, Count: 1},
|
||||
"0:9": {ItemID: 5001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "no-body-796", conn: &bufferConn{}}
|
||||
|
||||
if err := service.handleCurrentNoBody796(session, nil); err != nil {
|
||||
t.Fatalf("handle no-body op796: %v", err)
|
||||
}
|
||||
success, trailing := splitGameServerUpperPacket(t, session.conn.(*bufferConn).write.Bytes())
|
||||
if success.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
success.Header.MsgID != currentNoBody796Opcode ||
|
||||
!bytes.Equal(success.Body, []byte{1}) ||
|
||||
len(trailing) != 0 {
|
||||
t.Fatalf("success packet=%+v body=%x trailing=%x", success.Header, success.Body, trailing)
|
||||
}
|
||||
inventory := mustLoadCurrentSocketInventory(t, repositories, "77")
|
||||
if inventory.Slots["0:9"].Count != 1 || inventory.Slots["0:5"].Extra["equipment_emblem_data"] != "" {
|
||||
t.Fatalf("no-body op796 mutated inventory=%+v", inventory.Slots)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentSocketOpenRejectsNonOpenerMaterial(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:5": {ItemID: 9001, Count: 1},
|
||||
"0:70": {ItemID: 5001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "equipment-socket-invalid-material", conn: &bufferConn{}}
|
||||
if _, err := service.commitCurrentEquipmentSocketOpen(session, currentSocketOpenRequest{TargetSlot: 5, TargetItemID: 9001, MaterialSlot: 70}); !errors.Is(err, errCurrentSocketMaterialInvalid) {
|
||||
t.Fatalf("open with non-opener material err=%v", err)
|
||||
}
|
||||
if inventory := mustLoadCurrentSocketInventory(t, repositories, "77"); inventory.Slots["0:70"].Count != 1 {
|
||||
t.Fatalf("non-opener material was consumed: %+v", inventory.Slots)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentSocketOpenResolvesClientKeyToUniqueInventoryItem(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:12": {ItemID: 9001, Count: 1},
|
||||
"0:70": {ItemID: 6001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "equipment-socket-client-key", conn: &bufferConn{}}
|
||||
|
||||
result, err := service.commitCurrentEquipmentSocketOpen(session, currentSocketOpenRequest{
|
||||
TargetSlot: 2, // current EXE client collection key, not the durable slot
|
||||
TargetItemID: 9001,
|
||||
MaterialSlot: 70,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("equipment socket open by client key: %v", err)
|
||||
}
|
||||
if result.Target != (currentSocketChangedSlot{ListType: currentSocketListMain, Slot: 12}) || len(result.Consumed) != 1 || result.Consumed[0].Slot != 70 {
|
||||
t.Fatalf("open result=%+v", result)
|
||||
}
|
||||
inventory := mustLoadCurrentSocketInventory(t, repositories, "77")
|
||||
if _, ok := inventory.Slots["0:70"]; ok {
|
||||
t.Fatalf("socket opener should be consumed: %+v", inventory.Slots)
|
||||
}
|
||||
data := currentEquipmentEmblemData(inventory.Slots["0:12"].Extra, inventory.Slots["0:12"].RawEntry)
|
||||
if data[0] != 2 || binary.LittleEndian.Uint32(data[1:5]) != 0xFFFFFFFF || binary.LittleEndian.Uint32(data[5:9]) != 0xFFFFFFFF {
|
||||
t.Fatalf("opened equipment data=%x", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentSocketOpenUsesExactSlotWithDuplicateTemplates(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:5": {ItemID: 9001, Count: 1},
|
||||
"0:6": {ItemID: 9001, Count: 1},
|
||||
"0:70": {ItemID: 6001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "equipment-socket-duplicate-template", conn: &bufferConn{}}
|
||||
|
||||
result, err := service.commitCurrentEquipmentSocketOpen(session, currentSocketOpenRequest{
|
||||
TargetSlot: 6,
|
||||
TargetItemID: 9001,
|
||||
MaterialSlot: 70,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("equipment socket open at exact duplicate slot: %v", err)
|
||||
}
|
||||
if result.Target != (currentSocketChangedSlot{ListType: currentSocketListMain, Slot: 6}) {
|
||||
t.Fatalf("open result=%+v", result)
|
||||
}
|
||||
inventory := mustLoadCurrentSocketInventory(t, repositories, "77")
|
||||
if data := currentEquipmentEmblemData(inventory.Slots["0:5"].Extra, inventory.Slots["0:5"].RawEntry); data[0] != 0 {
|
||||
t.Fatalf("unselected duplicate was mutated: %x", data)
|
||||
}
|
||||
if data := currentEquipmentEmblemData(inventory.Slots["0:6"].Extra, inventory.Slots["0:6"].RawEntry); data[0] != 2 {
|
||||
t.Fatalf("selected duplicate socket data=%x", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentSocketOpenRepairsAlreadyOpenedInventoryItem(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
var opened [currentEquipmentEmblemDataBytes]byte
|
||||
opened[0] = 2
|
||||
target := dnfrepo.ItemStack{ItemID: 9001, Count: 1}
|
||||
currentApplyEquipmentEmblemDataToStack(&target, currentSocketListMain, 5, opened, currentEquipmentJewelSocketType(currentEquipmentPlacementRule{targetSlot: 17}))
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:5": target,
|
||||
"0:70": {ItemID: 6001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "equipment-socket-already-open", conn: &bufferConn{}}
|
||||
|
||||
result, err := service.commitCurrentEquipmentSocketOpen(session, currentSocketOpenRequest{
|
||||
TargetSlot: 5,
|
||||
TargetItemID: 9001,
|
||||
MaterialSlot: 70,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("repair already-open socket: %v", err)
|
||||
}
|
||||
if result.Target != (currentSocketChangedSlot{ListType: currentSocketListMain, Slot: 5}) || len(result.Consumed) != 0 {
|
||||
t.Fatalf("repair result=%+v", result)
|
||||
}
|
||||
inventory := mustLoadCurrentSocketInventory(t, repositories, "77")
|
||||
if inventory.Slots["0:70"].Count != 1 {
|
||||
t.Fatalf("socket opener consumed on already-open repair: %+v", inventory.Slots)
|
||||
}
|
||||
data := currentEquipmentEmblemData(inventory.Slots["0:5"].Extra, inventory.Slots["0:5"].RawEntry)
|
||||
if data[0] != 2 || binary.LittleEndian.Uint32(data[1:5]) != 0xFFFFFFFF || binary.LittleEndian.Uint32(data[5:9]) != 0xFFFFFFFF {
|
||||
t.Fatalf("already-open repair data=%x", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentEquipmentEmblemAttachCanTargetLegacyStoredWornSlotViaActorSlot(t *testing.T) {
|
||||
service, repositories := newCurrentSocketTestService(t)
|
||||
ctx := context.Background()
|
||||
var opened [currentEquipmentEmblemDataBytes]byte
|
||||
currentEnsureEquipmentEmblemSocketsOpen(&opened, 2, 2)
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:9": {ItemID: 5101, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
equipped := dnfrepo.EquipmentEntry{
|
||||
SlotIndex: 11,
|
||||
ItemID: 9201,
|
||||
Extra: map[string]string{"source": "pvf_create_equipment_list"},
|
||||
}
|
||||
currentApplyEquipmentEmblemDataToEquipment(&equipped, opened, currentEquipmentJewelSocketType(currentEquipmentPlacementRule{targetSlot: 11}))
|
||||
if err := repositories.Equipment.Save(ctx, dnfrepo.EquipmentRecord{
|
||||
CharacterID: "77",
|
||||
Entries: map[string]dnfrepo.EquipmentEntry{
|
||||
"11": equipped,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save equipment: %v", err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 77, connID: "equipped-socket-test", conn: &bufferConn{}}
|
||||
|
||||
result, err := service.commitCurrentEquipmentEmblemAttach(session, currentEmblemAttachRequest{
|
||||
TargetSlot: 12,
|
||||
TargetItemID: 9201,
|
||||
Emblems: []currentEmblemApplyRequest{{
|
||||
EmblemSlot: 9,
|
||||
EmblemItemID: 5101,
|
||||
SocketIndex: 0,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("equipped equipment emblem attach: %v", err)
|
||||
}
|
||||
if !result.TargetEquipped || result.Target != (currentSocketChangedSlot{ListType: currentSocketListEquipment, Slot: 11}) {
|
||||
t.Fatalf("equipped attach result=%+v", result)
|
||||
}
|
||||
if inventory := mustLoadCurrentSocketInventory(t, repositories, "77"); len(inventory.Slots) != 0 {
|
||||
t.Fatalf("equipped attach did not consume emblem inventory=%+v", inventory.Slots)
|
||||
}
|
||||
equipment, found, err := repositories.Equipment.Load(ctx, "77")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load equipment found=%v err=%v", found, err)
|
||||
}
|
||||
data := currentEquipmentEmblemData(equipment.Entries["11"].Extra, equipment.Entries["11"].RawEntry)
|
||||
if data[0] != 2 || binary.LittleEndian.Uint32(data[1:5]) != 5101 {
|
||||
t.Fatalf("equipped emblem data=%x", data)
|
||||
}
|
||||
}
|
||||
|
||||
func newCurrentSocketTestService(t *testing.T) (*Service, dnfrepo.Group) {
|
||||
t.Helper()
|
||||
source := dungeonDropCatalogTestSource{
|
||||
"monster/monster.lst": "",
|
||||
"stackable/stackable.lst": "5001 `emblem/red_c.stk`\n5101 `emblem/red_s.stk`\n6001 `emblem/chn_makesocket.stk`\n",
|
||||
"equipment/equipment.lst": "9001 `armor/test_coat.equ`\n9101 `avatar/test_hat.equ`\n9201 `weapon/test_sword.equ`\n",
|
||||
"stackable/emblem/red_c.stk": "[name] `Red C`\n" +
|
||||
"[stackable type]\n `[avatar emblem]`\n" +
|
||||
"[stack limit]\n 999\n" +
|
||||
"[avatar emblem target type]\n `[C socket]`\n[/avatar emblem target type]\n",
|
||||
"stackable/emblem/red_s.stk": "[name] `Red S`\n" +
|
||||
"[stackable type]\n `[avatar emblem]`\n" +
|
||||
"[stack limit]\n 999\n" +
|
||||
"[avatar emblem target type]\n `[S socket]`\n[/avatar emblem target type]\n",
|
||||
"stackable/emblem/chn_makesocket.stk": "[name] `Socket Tool`\n[stackable type]\n `[etc]`\n[stack limit]\n 999\n",
|
||||
"equipment/armor/test_coat.equ": "[name] `Test Coat`\n[equipment type]\n `[coat]`\n[durability]\n 45\n",
|
||||
"equipment/avatar/test_hat.equ": "[name] `Test Hat Avatar`\n" +
|
||||
"[equipment type]\n `[hat avatar]`\n" +
|
||||
"[avatar type select]\n `[C socket]`\n `[S socket]`\n[/avatar type select]\n",
|
||||
"equipment/weapon/test_sword.equ": "[name] `Test Sword`\n[equipment type]\n `[weapon]`\n[durability]\n 45\n",
|
||||
}
|
||||
catalog, err := newPVFDungeonDropCatalog(source)
|
||||
if err != nil {
|
||||
t.Fatalf("new test catalog: %v", err)
|
||||
}
|
||||
repositories := testRepositoryGroup()
|
||||
service := &Service{
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
pvfItemCatalog: catalog,
|
||||
}
|
||||
return service, repositories
|
||||
}
|
||||
|
||||
func mustLoadCurrentSocketInventory(t *testing.T, repositories dnfrepo.Group, characterID string) dnfrepo.InventoryRecord {
|
||||
t.Helper()
|
||||
inventory, found, err := repositories.Inventory.Load(context.Background(), characterID)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load inventory character=%s found=%v err=%v", characterID, found, err)
|
||||
}
|
||||
return inventory
|
||||
}
|
||||
|
||||
func buildCurrentSocketTestEmblemBody(targetSlot int16, targetItemID int64, emblems ...currentEmblemApplyRequest) []byte {
|
||||
var body packetWriter
|
||||
body.writeUint16(uint16(targetSlot))
|
||||
body.writeUint32(uint32(targetItemID))
|
||||
body.writeByte(byte(len(emblems)))
|
||||
for _, emblem := range emblems {
|
||||
body.writeUint16(uint16(emblem.EmblemSlot))
|
||||
body.writeUint32(uint32(emblem.EmblemItemID))
|
||||
body.writeByte(emblem.SocketIndex)
|
||||
}
|
||||
return body.bytes()
|
||||
}
|
||||
Reference in new issue
Block a user