服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
232 lines
8.7 KiB
Go
232 lines
8.7 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
platformpvf "longheng.io/server/internal/platform/pvf"
|
|
)
|
|
|
|
func TestResolveCurrentChronicleRefineMetadataUsesImportedCheckAndSkill(t *testing.T) {
|
|
source := bridgePVFSource{
|
|
"monster/monster.lst": "",
|
|
"stackable/stackable.lst": "3037 `cube.stk`\n3311 `fragment.stk`\n10090760 `10090001/10090760.stk`\n",
|
|
"equipment/equipment.lst": "116010135 `character/gunblader/weapon/sblade/116010135.equ`\n",
|
|
"stackable/cube.stk": "[stackable type] `[material]`\n[stack limit] 999\n",
|
|
"stackable/fragment.stk": "[stackable type] `[material]`\n[stack limit] 999\n",
|
|
"equipment/pricetable.tbl": "[rate]\n1000 150 30\n",
|
|
"etc/disjoint.etc": "[cube index]\n`[no element]` 3037\n[/cube index]\n[cube creation const]\n150 1 1 1 1 1 1\n[/cube creation const]\n[additional result]\n0 0 0 0 0 0\n[/additional result]\n[additional result const]\n0 0 0 0 0 0\n[/additional result const]\n[additional result expand]\n0 0 0 0 0 0\n[/additional result expand]\n[additional result expand const]\n0 0 0 0 0 0\n[/additional result expand const]\n[avatar disjoint info]\n[fighter]\n2559000 2559003 2559006 2559009 2559006 2559001 2559004 2559007 2559010 2559007 2559002 2559005 2559008 2559011 2559014\n[/fighter]\n[/avatar disjoint info]\n",
|
|
"etc/emblemcompound.etc": "[emblem compound info]\n1 2 3037\n[/emblem compound info]\n",
|
|
"etc/avatardisjoint/emblemlistinfo_0.etc": "[result info]\n1 3037 100 1 0\n[/result info]\n",
|
|
"etc/avatardisjoint/emblemlistinfo_1.etc": "[result info]\n1 3037 100 1 0\n[/result info]\n",
|
|
"etc/avatardisjoint/emblemlistinfo_2.etc": "[min max value]\n50 69\n[result info]\n1 3037 100 1 0\n[/result info]\n",
|
|
"stackable/10090001/10090760.stk": `
|
|
[stackable type]
|
|
` + "`[etc]`" + `
|
|
[import script]
|
|
` + "`DimensionForce_G.stk`" + `
|
|
[type]
|
|
2
|
|
[probability]
|
|
100 100
|
|
[need material]
|
|
3311 1
|
|
`,
|
|
"stackable/DimensionForce_G.stk": `
|
|
[3choro enchant]
|
|
[check]
|
|
15 2 ` + "`weapon`" + `
|
|
[skill]
|
|
11 ` + "`[gun blader]`" + ` 123 456
|
|
[/skill]
|
|
[/check]
|
|
[check]
|
|
5 2 ` + "`weapon`" + `
|
|
[skill]
|
|
0 ` + "`[at gunner]`" + ` 123 456
|
|
[/skill]
|
|
[/check]
|
|
[/3choro enchant]
|
|
`,
|
|
"equipment/character/gunblader/weapon/sblade/116010135.equ": `
|
|
[rarity]
|
|
5
|
|
[durability]
|
|
45
|
|
[minimum level]
|
|
70
|
|
[value]
|
|
300
|
|
[equipment type]
|
|
` + "`[weapon]`" + `
|
|
[character item check]
|
|
15 2 ` + "`weapon`" + ` 5 2 ` + "`weapon`" + `
|
|
[/character item check]
|
|
`,
|
|
}
|
|
catalog, err := newPVFDungeonDropCatalog(source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resolution, err := resolveCurrentChronicleRefineMetadata(catalog, source, 10090760, 116010135, 15, 2, 11)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !resolution.Matched || resolution.AuraType != 2 || resolution.CanonicalAuraOptionID != 1256 ||
|
|
resolution.Probabilities != [2]byte{100, 100} || resolution.TargetEquipmentTypeCode != 12 ||
|
|
resolution.TargetDurability != 45 || resolution.FailureFragmentItemID != 3311 ||
|
|
resolution.FailureFragmentItem.ItemID != 3311 || resolution.FailureCubeItem.ItemID != 3037 ||
|
|
resolution.FailureCubeCount != 2 {
|
|
t.Fatalf("resolution = %+v", resolution)
|
|
}
|
|
if strings.ToLower(resolution.ImportedScriptPVFPath) != "stackable/dimensionforce_g.stk" {
|
|
t.Fatalf("imported path = %q", resolution.ImportedScriptPVFPath)
|
|
}
|
|
|
|
rejected, err := resolveCurrentChronicleRefineMetadata(catalog, source, 10090760, 116010135, 15, 2, 10)
|
|
if err != nil || rejected.Matched {
|
|
t.Fatalf("unsupported option resolution = %+v err=%v", rejected, err)
|
|
}
|
|
rejected, err = resolveCurrentChronicleRefineMetadata(catalog, source, 10090760, 116010135, 15, 1, 11)
|
|
if err != nil || rejected.Matched {
|
|
t.Fatalf("wrong grow resolution = %+v err=%v", rejected, err)
|
|
}
|
|
|
|
femaleGunner, err := resolveCurrentChronicleRefineMetadata(catalog, source, 10090760, 116010135, 5, 2, 0)
|
|
if err != nil || !femaleGunner.Matched {
|
|
t.Fatalf("shared female-gunner target resolution = %+v err=%v", femaleGunner, err)
|
|
}
|
|
}
|
|
|
|
func TestRealPVFChronicleRefineObservedGunnerWeapons(t *testing.T) {
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify current chronicle aura data")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := newPVFDungeonDropCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, materialID := range []int64{1256, 10090760} {
|
|
resolution, resolveErr := resolveCurrentChronicleRefineMetadata(catalog, archive, materialID, 116010135, 15, 2, 11)
|
|
if resolveErr != nil {
|
|
t.Fatalf("material %d: %v", materialID, resolveErr)
|
|
}
|
|
if !resolution.Matched || resolution.AuraType != 2 || resolution.CanonicalAuraOptionID != 1256 ||
|
|
resolution.TargetEquipmentTypeCode != 12 || resolution.FailureFragmentItemID != 3311 ||
|
|
resolution.FailureFragmentItem.ItemID != 3311 || resolution.FailureCubeItem.ItemID != 3037 ||
|
|
resolution.FailureCubeCount <= 0 {
|
|
t.Fatalf("material %d resolution = %+v", materialID, resolution)
|
|
}
|
|
t.Logf("material=%d material_path=%s import=%s probabilities=%v fragment=%d cube=(%d,%d) target=%s",
|
|
materialID, resolution.MaterialPVFPath, resolution.ImportedScriptPVFPath,
|
|
resolution.Probabilities, resolution.FailureFragmentItemID,
|
|
resolution.FailureCubeItem.ItemID, resolution.FailureCubeCount, resolution.TargetPVFPath)
|
|
}
|
|
|
|
femaleTargets := []struct {
|
|
itemID int64
|
|
code byte
|
|
path string
|
|
}{
|
|
{itemID: 625000, code: 12, path: "equipment/character/gunner/weapon/revolver/rev_3choro1.equ"},
|
|
{itemID: 115000, code: 14, path: "equipment/character/common/jacket/leather/coat_lt_3choro1.equ"},
|
|
{itemID: 445072, code: 22, path: "equipment/character/common/support/support_3choro73.equ"},
|
|
}
|
|
for _, target := range femaleTargets {
|
|
for _, materialID := range []int64{1254, 1255, 1256} {
|
|
resolution, resolveErr := resolveCurrentChronicleRefineMetadata(catalog, archive, materialID, target.itemID, 5, 1, 0)
|
|
if resolveErr != nil {
|
|
t.Fatalf("female gunner target %d material %d: %v", target.itemID, materialID, resolveErr)
|
|
}
|
|
if !resolution.Matched || resolution.AuraType != byte(materialID-currentChronicleCanonicalAuraBase) ||
|
|
resolution.CanonicalAuraOptionID != int32(materialID) || resolution.TargetEquipmentTypeCode != target.code ||
|
|
resolution.TargetPVFPath != target.path {
|
|
t.Fatalf("female gunner target %d material %d resolution = %+v", target.itemID, materialID, resolution)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCurrentChronicleEquipmentTypeUsesClientDefinitionTypes(t *testing.T) {
|
|
tests := map[string]byte{
|
|
"weapon": 12,
|
|
"coat": 14,
|
|
"shoulder": 15,
|
|
"pants": 16,
|
|
"shoes": 17,
|
|
"waist": 18,
|
|
"amulet": 19,
|
|
"wrist": 20,
|
|
"ring": 21,
|
|
"support": 22,
|
|
"magic stone": 23,
|
|
"earring": 25,
|
|
}
|
|
for raw, want := range tests {
|
|
t.Run(raw, func(t *testing.T) {
|
|
name, got, ok := currentChronicleEquipmentType(raw)
|
|
if !ok || name != raw || got != want {
|
|
t.Fatalf("currentChronicleEquipmentType(%q) = (%q,%d,%t), want (%q,%d,true)", raw, name, got, ok, raw, want)
|
|
}
|
|
})
|
|
}
|
|
for _, unsupported := range []string{"title", "support weapon", "unknown"} {
|
|
if _, _, ok := currentChronicleEquipmentType(unsupported); ok {
|
|
t.Fatalf("unsupported equipment type %q accepted", unsupported)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestChronicleOptionVectorSurvivesCurrentItemProjections(t *testing.T) {
|
|
raw := make([]byte, currentItemListEntryWireSize)
|
|
raw[0x27] = 1
|
|
binary.LittleEndian.PutUint32(raw[0x28:0x2C], 1256)
|
|
raw[0x30] = 15
|
|
raw[0x32] = 2
|
|
raw[0x34] = 12
|
|
raw[0x36] = 11
|
|
vector := append([]byte(nil), raw[0x27:0x38]...)
|
|
|
|
stack := dnfrepo.ItemStack{
|
|
ItemID: 116010135,
|
|
Count: 1,
|
|
RawEntry: append([]byte(nil), raw...),
|
|
Extra: map[string]string{
|
|
"item_kind": "equipment",
|
|
"middle_data_1a": hex.EncodeToString(vector),
|
|
},
|
|
}
|
|
bag := currentItemListEntryFromStack(dnfrepo.MainInventoryListType, 29, stack)
|
|
if got := bag.data[0x27:0x38]; !bytes.Equal(got, vector) {
|
|
t.Fatalf("list0 current-item projection vector=%x want=%x", got, vector)
|
|
}
|
|
|
|
equipped := dnfrepo.EquipmentEntry{
|
|
SlotIndex: 11,
|
|
ItemID: stack.ItemID,
|
|
RawEntry: append([]byte(nil), stack.RawEntry...),
|
|
Extra: stack.Extra,
|
|
}
|
|
login, ok := currentItemListEntryFromEquipment(equipped)
|
|
if !ok {
|
|
t.Fatal("list3 current-item projection rejected Chronicle weapon")
|
|
}
|
|
if got := login.data[0x27:0x38]; !bytes.Equal(got, vector) {
|
|
t.Fatalf("list3 current-item projection vector=%x want=%x", got, vector)
|
|
}
|
|
update := currentEquipmentUpdateEntryFromEquipment(equipped)
|
|
if got := update.data[0x1A:0x2B]; !bytes.Equal(got, vector) {
|
|
t.Fatalf("equipment-update projection vector=%x want=%x", got, vector)
|
|
}
|
|
}
|