Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

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)
}
}