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,461 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func TestCurrentCargoTransportRequestIsExactNineByteWire(t *testing.T) {
|
||||
body := make([]byte, 9)
|
||||
binary.LittleEndian.PutUint16(body[0:2], 71)
|
||||
binary.LittleEndian.PutUint16(body[2:4], 29)
|
||||
body[4] = 2
|
||||
binary.LittleEndian.PutUint32(body[5:9], 4)
|
||||
request, err := decodeCurrentCargoTransportRequest(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if request.StoneSlot != 71 || request.TargetSlot != 29 || request.Mode != 2 || request.RecipientSelect != 4 {
|
||||
t.Fatalf("request=%+v", request)
|
||||
}
|
||||
for _, invalid := range [][]byte{body[:8], append(append([]byte(nil), body...), 0)} {
|
||||
if _, err := decodeCurrentCargoTransportRequest(invalid); err == nil {
|
||||
t.Fatalf("accepted length=%d", len(invalid))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentTransportStoneDefinitionAcceptsDirectAndActionTypeLayouts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
cargo bool
|
||||
cargoType int64
|
||||
creature bool
|
||||
creatureType int64
|
||||
}{
|
||||
{name: "current cargo action", text: "[action type]\n`[cargo transport stone]` 8\n[/action type]\n", cargo: true, cargoType: 8},
|
||||
{name: "current creature action", text: "[action type]\n`[creature transport stone]` 2\n[/action type]\n", creature: true, creatureType: 2},
|
||||
{name: "legacy cargo section", text: "[cargo transport stone]\n5\n", cargo: true, cargoType: 5},
|
||||
{name: "unrelated action fails closed", text: "[action type]\n`[cargo something else]` 8\n[/action type]\n"},
|
||||
{name: "missing action integer fails closed", text: "[action type]\n`[cargo transport stone]`\n[/action type]\n"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
document, err := dnfpvf.Parse("stackable/test.stk", test.text)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
definition := dungeonDropItemDefinition{}
|
||||
parseCurrentTransportStoneDefinition(document, &definition)
|
||||
if definition.CargoTransportStone != test.cargo || definition.CargoTransportStoneType != test.cargoType ||
|
||||
definition.CreatureTransportStone != test.creature || definition.CreatureTransportStoneType != test.creatureType {
|
||||
t.Fatalf("definition=%+v", definition)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentCargoTransportDirectStoneMovesEquipmentToAccountCargo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
mustSaveCargoTransportAccount(t, ctx, repositories, "account-1", 8)
|
||||
if err := repositories.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{AccountID: "account-1", Slots: map[string]dnfrepo.ItemStack{}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "1", Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:70": {ItemID: 9001, Count: 2},
|
||||
"0:12": {ItemID: 1001, Count: 1},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := &pvfDungeonDropCatalog{source: finishBridgePVFSource{}, itemCache: map[uint32]dungeonDropItemDefinition{
|
||||
9001: {ItemID: 9001, Kind: dungeonDropItemStackable, CargoTransportStone: true, CargoTransportStoneType: 3},
|
||||
1001: {ItemID: 1001, Kind: dungeonDropItemEquipment, EquipmentType: "[weapon]", MinimumLevel: 85, Rarity: 4},
|
||||
}}
|
||||
config := currentCargoTransportConfig{Rules: map[int64]currentCargoTransportRule{
|
||||
3: {StoneType: 3, GradeWeights: []currentPVFRangeWeight{{Minimum: 1, Maximum: 90, Weight: 1}}, RarityWeights: map[int64]int64{4: 1}, EquipmentTypes: map[string]bool{"weapon": true}, ExcludeItemIDs: map[uint32]bool{}, IncludeItemIDs: map[uint32]bool{}},
|
||||
}}
|
||||
service := &Service{}
|
||||
if err := service.commitCurrentStoneToAccountCargo(ctx, repositories, catalog, config, "account-1", "1", currentCargoTransportRequest{StoneSlot: 70, TargetSlot: 12}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inventory, _, _ := repositories.Inventory.Load(ctx, "1")
|
||||
if inventory.Slots["0:70"].Count != 1 {
|
||||
t.Fatalf("stone=%+v", inventory.Slots["0:70"])
|
||||
}
|
||||
if _, found := inventory.Slots["0:12"]; found {
|
||||
t.Fatal("target remained in character inventory")
|
||||
}
|
||||
cargo, found, err := repositories.AccountInventory.Load(ctx, "account-1")
|
||||
if err != nil || !found || cargo.Slots["12:0"].ItemID != 1001 {
|
||||
t.Fatalf("cargo found=%t err=%v slots=%+v", found, err, cargo.Slots)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentGrandiTransportConsumesExactPVFCostsAtomically(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
for _, row := range []dnfrepo.CharacterRecord{
|
||||
{CharacterID: "1", AccountID: "account-1", Slot: 0},
|
||||
{CharacterID: "2", AccountID: "account-1", Slot: 4},
|
||||
} {
|
||||
if err := repositories.Character.Save(ctx, row); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "1", Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:12": {ItemID: 100240061, Count: 1},
|
||||
"0:70": {ItemID: 3167, Count: 500},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "2", Slots: map[string]dnfrepo.ItemStack{}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{AccountID: "account-1", Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:364": {ItemID: 10158124, Count: 200},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.EpicBook.Save(ctx, dnfrepo.EpicBookRecord{AccountID: "account-1", Balances: map[uint32]uint32{100240061: 200}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := &pvfDungeonDropCatalog{source: finishBridgePVFSource{}, itemCache: map[uint32]dungeonDropItemDefinition{
|
||||
100240061: {ItemID: 100240061, Kind: dungeonDropItemEquipment, EquipmentType: "[coat]", MinimumLevel: 85, Rarity: 4},
|
||||
}}
|
||||
rule := currentCargoTransportRule{
|
||||
StoneType: 7, GradeWeights: []currentPVFRangeWeight{{Minimum: 85, Maximum: 85, Weight: 5}},
|
||||
RarityWeights: map[int64]int64{4: 9}, EquipmentTypes: map[string]bool{"coat": true}, IncludeItemIDs: map[uint32]bool{}, ExcludeItemIDs: map[uint32]bool{},
|
||||
SoulItemIDs: map[int64]uint32{4: 10158124}, SoulWeights: map[int64][]int64{4: {3, 3, 3, 3, 3}},
|
||||
ElementItemID: 3167, ElementWeights: map[int64][]int64{4: {10, 10, 10, 10}}, EpicPieceWeight: []int64{3, 3, 3, 3, 3},
|
||||
}
|
||||
service := &Service{}
|
||||
// RecipientSelect is the roster index, not the slot: character "2" sits
|
||||
// in slot 4 but is the second row of the account roster, so the client
|
||||
// sends 1. This is exactly the gap the slot reading used to get wrong.
|
||||
if err := service.commitCurrentGrandiEquipmentTransport(ctx, repositories, catalog, currentCargoTransportConfig{Rules: map[int64]currentCargoTransportRule{7: rule}}, "account-1", "1", currentCargoTransportRequest{TargetSlot: 12, Mode: 2, RecipientSelect: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source, _, _ := repositories.Inventory.Load(ctx, "1")
|
||||
destination, _, _ := repositories.Inventory.Load(ctx, "2")
|
||||
account, _, _ := repositories.AccountInventory.Load(ctx, "account-1")
|
||||
book, _, _ := repositories.EpicBook.Load(ctx, "account-1")
|
||||
if _, found := source.Slots["0:12"]; found {
|
||||
t.Fatalf("transferred equipment stayed in the sender bag: %+v", source.Slots)
|
||||
}
|
||||
// The recipient must be reached by mail, never straight into the bag.
|
||||
if len(destination.Slots) != 0 {
|
||||
t.Fatalf("equipment went into the recipient backpack: %+v", destination.Slots)
|
||||
}
|
||||
mailbox, found, err := repositories.Mailbox.Load(ctx, "2")
|
||||
if err != nil || !found || len(mailbox.Mails) != 1 {
|
||||
t.Fatalf("recipient mailbox found=%t err=%v mails=%d", found, err, len(mailbox.Mails))
|
||||
}
|
||||
for _, mail := range mailbox.Mails {
|
||||
if len(mail.Attachments) != 1 || mail.Attachments[0].ItemID != 100240061 || mail.Attachments[0].Count != 1 {
|
||||
t.Fatalf("mail attachments=%+v", mail.Attachments)
|
||||
}
|
||||
if mail.Attachments[0].Extra["source"] != "grandi_equipment_transport" {
|
||||
t.Fatalf("mail attachment extra=%+v", mail.Attachments[0].Extra)
|
||||
}
|
||||
}
|
||||
if source.Slots["0:70"].Count != 50 || account.Slots["0:364"].Count != 65 || book.Balances[100240061] != 65 {
|
||||
t.Fatalf("element=%d soul=%d fragments=%d", source.Slots["0:70"].Count, account.Slots["0:364"].Count, book.Balances[100240061])
|
||||
}
|
||||
}
|
||||
|
||||
// A rejection must reach the client as one of the seven codes the op557
|
||||
// handler sub_1E785B0 recognises. An unlisted code leaves its dstr register
|
||||
// zeroed and the client pops dstr 0 -- in this build the unrelated
|
||||
// "已超出机器人放置的个数上限".
|
||||
func TestCurrentCargoTransportFailureCodesAreClientRecognised(t *testing.T) {
|
||||
// code -> the dstr text docs/client-dstr-zh.txt gives for it. Every code
|
||||
// this server can send must be in here; anything else renders dstr 0.
|
||||
recognised := map[byte]string{
|
||||
0x04: "帐号金库空间不足",
|
||||
0x07: "无法转移该道具",
|
||||
0x11: "该道具不存在",
|
||||
0x13: "帐号金库未激活",
|
||||
0x15: "无法使用。",
|
||||
0x16: "所需材料不足",
|
||||
0xD6: "在物品已锁定状态下",
|
||||
}
|
||||
for _, code := range []byte{
|
||||
currentCargoTransportErrorNotTransferable,
|
||||
currentCargoTransportErrorItemMissing,
|
||||
currentCargoTransportErrorCargoInactive,
|
||||
currentCargoTransportErrorCargoFull,
|
||||
currentCargoTransportErrorMaterial,
|
||||
} {
|
||||
if _, ok := recognised[code]; !ok {
|
||||
t.Fatalf("code %#x is not in the current-EXE op557 table", code)
|
||||
}
|
||||
}
|
||||
// The material message is the one the player sees most, so pin the code
|
||||
// that carries it: 0x16, not ServerS4A21's "source missing" reading.
|
||||
if currentCargoTransportErrorMaterial != 0x16 {
|
||||
t.Fatalf("material code=%#x want 0x16 (所需材料不足)", currentCargoTransportErrorMaterial)
|
||||
}
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
err error
|
||||
want byte
|
||||
}{
|
||||
{name: "target missing", err: currentCargoTransportReject(currentCargoTransportErrorItemMissing, "x"), want: currentCargoTransportErrorItemMissing},
|
||||
{name: "material insufficient", err: currentCargoTransportReject(currentCargoTransportErrorMaterial, "x"), want: currentCargoTransportErrorMaterial},
|
||||
{name: "wrapped", err: fmt.Errorf("commit: %w", currentCargoTransportReject(currentCargoTransportErrorCargoFull, "x")), want: currentCargoTransportErrorCargoFull},
|
||||
{name: "uncoded repository fault", err: errors.New("boom"), want: currentCargoTransportErrorNotTransferable},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
if got := currentCargoTransportFailureCode(testCase.err); got != testCase.want {
|
||||
t.Fatalf("code=%#x want %#x", got, testCase.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The success payload is the four u16 plus mode byte sub_1E785B0 reads at
|
||||
// 0x1E78621. Mode 2 makes it close the 装备跨界 window (UI 0x435) and return
|
||||
// before touching any item row, which is why the count and parameter are 0.
|
||||
func TestCurrentCargoTransportSuccessBodyIsFourWordsAndAModeByte(t *testing.T) {
|
||||
body := buildCurrentCargoTransportSuccessBody(0, 20, 0, 0, currentCargoTransportGrandiMode)
|
||||
if len(body) != 9 {
|
||||
t.Fatalf("body=% X", body)
|
||||
}
|
||||
if binary.LittleEndian.Uint16(body[0:2]) != 0 ||
|
||||
binary.LittleEndian.Uint16(body[2:4]) != 20 ||
|
||||
binary.LittleEndian.Uint16(body[4:6]) != 0 ||
|
||||
binary.LittleEndian.Uint16(body[6:8]) != 0 ||
|
||||
body[8] != 2 {
|
||||
t.Fatalf("body=% X", body)
|
||||
}
|
||||
if got := upperSuccessBody(body); got[0] != 1 || len(got) != 10 {
|
||||
t.Fatalf("upper body=% X", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The dropdown value is a roster position. The recipient must be picked by
|
||||
// that position over the roster ordered by slot then character id, and the
|
||||
// sender's own row must never resolve.
|
||||
func TestCurrentCargoRecipientIsResolvedByRosterIndexNotSlot(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
for _, row := range []dnfrepo.CharacterRecord{
|
||||
{CharacterID: "1", AccountID: "account-1", Slot: 0},
|
||||
{CharacterID: "9", AccountID: "account-1", Slot: 7},
|
||||
{CharacterID: "5", AccountID: "account-1", Slot: 13},
|
||||
} {
|
||||
if err := repositories.Character.Save(ctx, row); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
service := &Service{}
|
||||
for _, testCase := range []struct {
|
||||
selection uint32
|
||||
want string
|
||||
}{{selection: 1, want: "9"}, {selection: 2, want: "5"}} {
|
||||
picked, err := service.currentCargoRecipientBySelection(ctx, repositories.Character, "account-1", "1", testCase.selection)
|
||||
if err != nil || picked.CharacterID != testCase.want {
|
||||
t.Fatalf("selection=%d picked=%q err=%v want %q", testCase.selection, picked.CharacterID, err, testCase.want)
|
||||
}
|
||||
}
|
||||
if _, err := service.currentCargoRecipientBySelection(ctx, repositories.Character, "account-1", "1", 0); err == nil {
|
||||
t.Fatal("the sender's own roster position must not resolve as a recipient")
|
||||
}
|
||||
if _, err := service.currentCargoRecipientBySelection(ctx, repositories.Character, "account-1", "1", 7); err == nil {
|
||||
t.Fatal("a slot number past the roster length must not resolve")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentPetTransportMovesDurableCreatureState(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
for _, row := range []dnfrepo.CharacterRecord{{CharacterID: "1", AccountID: "account-1", Slot: 0}, {CharacterID: "2", AccountID: "account-1", Slot: 1}} {
|
||||
if err := repositories.Character.Save(ctx, row); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "1", Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:70": {ItemID: 8001, Count: 1}, "7:4": {ItemID: 63000, Count: 1, Extra: map[string]string{"creature_key": "77"}},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "2", Slots: map[string]dnfrepo.ItemStack{}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Pet.Save(ctx, dnfrepo.PetRecord{CharacterID: "1", Entries: map[string]dnfrepo.PetEntry{
|
||||
"77": {PetKey: "77", ItemID: 63000, SourceListType: 7, SourceSlotIndex: 4},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Pet.Save(ctx, dnfrepo.PetRecord{CharacterID: "2", Entries: map[string]dnfrepo.PetEntry{}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := &pvfDungeonDropCatalog{source: finishBridgePVFSource{}, itemCache: map[uint32]dungeonDropItemDefinition{
|
||||
8001: {ItemID: 8001, Kind: dungeonDropItemStackable, CreatureTransportStone: true, CreatureTransportStoneType: 1},
|
||||
63000: {ItemID: 63000, Kind: dungeonDropItemEquipment, EquipmentType: "[creature]"},
|
||||
}}
|
||||
service := &Service{}
|
||||
if err := service.commitCurrentPetTransport(ctx, repositories, catalog, "account-1", "1", currentCargoTransportRequest{StoneSlot: 70, TargetSlot: 4, Mode: 1, RecipientSelect: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source, _, _ := repositories.Inventory.Load(ctx, "1")
|
||||
destination, _, _ := repositories.Inventory.Load(ctx, "2")
|
||||
sourcePets, _, _ := repositories.Pet.Load(ctx, "1")
|
||||
destinationPets, _, _ := repositories.Pet.Load(ctx, "2")
|
||||
if len(source.Slots) != 0 || destination.Slots["7:0"].ItemID != 63000 || len(sourcePets.Entries) != 0 || destinationPets.Entries["77"].SourceSlotIndex != 0 {
|
||||
t.Fatalf("source=%+v destination=%+v sourcePets=%+v destinationPets=%+v", source.Slots, destination.Slots, sourcePets, destinationPets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentResealCostAndWaxKinds(t *testing.T) {
|
||||
config := currentResealConfig{
|
||||
GradeWeights: []currentPVFRangeWeight{{Minimum: 85, Maximum: 85, Weight: 5}},
|
||||
RarityWeights: map[int64]float64{4: 9}, ExtendedWeights: map[int64]float64{4: 1}, SoulItemIDs: map[int64]uint32{4: 10158124},
|
||||
SoulCostWeights: map[int64]float64{4: 3}, ResealConstants: map[int64]float64{4: 1.5}, ResealCountLimits: []currentPVFRangeWeight{{Minimum: 1, Maximum: 90, Weight: 7}},
|
||||
}
|
||||
definition := dungeonDropItemDefinition{Kind: dungeonDropItemEquipment, MinimumLevel: 85, Rarity: 4}
|
||||
if item, cost, ok := config.materialAndCost(definition, 0); !ok || item != 10158124 || cost != 135 {
|
||||
t.Fatalf("soul item=%d cost=%d ok=%t", item, cost, ok)
|
||||
}
|
||||
if cost, ok := config.waxCost(definition, 0); !ok || cost != 45 {
|
||||
t.Fatalf("wax cost=%d ok=%t", cost, ok)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
definition dungeonDropItemDefinition
|
||||
oneShot bool
|
||||
valid bool
|
||||
}{
|
||||
{dungeonDropItemDefinition{PVFPath: "stackable/cash/candle_goldwax.stk"}, false, true},
|
||||
{dungeonDropItemDefinition{PVFPath: "stackable/cash/candle_goldwax_oneshot.stk"}, true, true},
|
||||
{dungeonDropItemDefinition{PVFPath: "stackable/event/chn_candle_goldwax_oneshot_490000345.stk"}, true, true},
|
||||
{dungeonDropItemDefinition{PVFPath: "stackable/other.stk"}, false, false},
|
||||
} {
|
||||
oneShot, valid := currentPVFWaxKind(test.definition)
|
||||
if oneShot != test.oneShot || valid != test.valid {
|
||||
t.Fatalf("definition=%+v oneShot=%t valid=%t", test.definition, oneShot, valid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCargoTransportAndPetSealModulesAreRegistered(t *testing.T) {
|
||||
for _, opcode := range []uint16{uint16(dnfenum.CmdPacketCargoTransportItem), uint16(dnfenum.CmdPacketSealCreature)} {
|
||||
if _, found := currentGameplayModules.legacyHandlers[opcode]; !found {
|
||||
t.Fatalf("legacy opcode=%d missing", opcode)
|
||||
}
|
||||
if _, found := currentGameplayModules.upperHandlers[opcode]; !found {
|
||||
t.Fatalf("upper opcode=%d missing", opcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealPVFCargoTransportAndResealRules(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify cargo transport and reseal rules")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog, err := newPVFDungeonDropCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
transport, err := loadCurrentCargoTransportConfig(catalog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, found := transport.Rules[7]; !found {
|
||||
t.Fatal("runtime PVF has no Grandi stone type 7")
|
||||
}
|
||||
for _, direct := range []struct {
|
||||
stoneID uint32
|
||||
stoneType int64
|
||||
targetID uint32
|
||||
}{
|
||||
{stoneID: 10304349, stoneType: 8, targetID: 100140069},
|
||||
{stoneID: 490002430, stoneType: 4016, targetID: 100060476},
|
||||
} {
|
||||
stone, resolveErr := catalog.ResolveItem(direct.stoneID)
|
||||
if resolveErr != nil {
|
||||
t.Fatal(resolveErr)
|
||||
}
|
||||
if !stone.CargoTransportStone || stone.CargoTransportStoneType != direct.stoneType {
|
||||
t.Fatalf("runtime direct cargo stone=%d not parsed: %+v", direct.stoneID, stone)
|
||||
}
|
||||
directRule, ruleFound := transport.Rules[stone.CargoTransportStoneType]
|
||||
if !ruleFound {
|
||||
t.Fatalf("runtime direct cargo stone=%d type=%d has no rule", direct.stoneID, stone.CargoTransportStoneType)
|
||||
}
|
||||
directTarget, resolveErr := catalog.ResolveItem(direct.targetID)
|
||||
if resolveErr != nil {
|
||||
t.Fatal(resolveErr)
|
||||
}
|
||||
if !currentCargoRuleAllows(directRule, directTarget, dnfrepo.ItemStack{ItemID: int64(directTarget.ItemID), Count: 1}) {
|
||||
t.Fatalf("runtime screenshot cargo target rejected stone=%+v target=%+v rule=%+v", stone, directTarget, directRule)
|
||||
}
|
||||
}
|
||||
creatureStone, err := catalog.ResolveItem(490700208)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !creatureStone.CreatureTransportStone || creatureStone.CreatureTransportStoneType != 16 {
|
||||
t.Fatalf("runtime creature transport stone not parsed: %+v", creatureStone)
|
||||
}
|
||||
rule := transport.Rules[7]
|
||||
target, err := catalog.ResolveItem(100240061)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
category := currentCargoEquipmentCategory(target.EquipmentType)
|
||||
base := rangeWeightFor(rule.GradeWeights, target.MinimumLevel) * rule.RarityWeights[target.Rarity]
|
||||
if len(rule.SoulWeights[target.Rarity]) <= category || len(rule.ElementWeights[target.Rarity]) <= category || len(rule.EpicPieceWeight) <= category {
|
||||
t.Fatalf("runtime Grandi cost vectors missing target=%+v category=%d base=%d rule=%+v", target, category, base, rule)
|
||||
}
|
||||
if category != 1 || base != 45 || rule.SoulItemIDs[target.Rarity] != 10158124 || rule.SoulWeights[target.Rarity][category]*base != 135 || rule.ElementWeights[target.Rarity][category]*base != 450 || rule.EpicPieceWeight[category]*base != 135 {
|
||||
t.Fatalf("runtime Grandi rule target=%+v category=%d base=%d rule=%+v", target, category, base, rule)
|
||||
}
|
||||
reseal, err := loadCurrentResealConfig(catalog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resealable := dungeonDropItemDefinition{Kind: dungeonDropItemEquipment, MinimumLevel: 85, Rarity: 3}
|
||||
if item, cost, ok := reseal.materialAndCost(resealable, 0); !ok || item != 10099774 || cost != 80 {
|
||||
t.Fatalf("runtime Grandi reseal item=%d cost=%d ok=%t config=%+v", item, cost, ok, reseal)
|
||||
}
|
||||
if cost, ok := reseal.waxCost(resealable, 0); !ok || cost != 8 {
|
||||
t.Fatalf("runtime golden wax cost=%d ok=%t", cost, ok)
|
||||
}
|
||||
for _, itemID := range []uint32{14, 2660238, 10098656, 490000345, 10157922, 10164812} {
|
||||
definition, resolveErr := catalog.ResolveItem(itemID)
|
||||
if resolveErr != nil {
|
||||
t.Fatal(resolveErr)
|
||||
}
|
||||
oneShot, valid := currentPVFWaxKind(definition)
|
||||
if !valid || (itemID == 14 && oneShot) || (itemID != 14 && !oneShot) {
|
||||
t.Fatalf("runtime wax item=%d one_shot=%t valid=%t definition=%+v", itemID, oneShot, valid, definition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustSaveCargoTransportAccount(t *testing.T, ctx context.Context, repositories dnfrepo.Group, accountID string, capacity int) {
|
||||
t.Helper()
|
||||
if err := repositories.Account.Save(ctx, dnfrepo.AccountRecord{AccountID: accountID, Metadata: map[string]string{
|
||||
"account_cargo_created": "true", "account_cargo_level": strconv.Itoa(capacity),
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user