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

334 lines
16 KiB
Go

package dnfbridge
import (
"os"
"path"
"strings"
"testing"
"longheng.io/server/internal/modules/dnf/alignedcmd"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func testForgingConfigText() string {
return "[table] 2 1 10000 1 3 2 5000 1.11 [/table]\n" +
"[separate upgrade max] 2\n[level] 8\n" +
"[item weights by grade] 90 3326 45 [/item weights by grade]\n" +
"[item weights by rarity] 0.4 0.7 1 1.25 1.4 1.1 1.3 [/item weights by rarity]\n" +
"[stat weights by rarity] 0.5 0.75 1 1.25 1.5 1.75 2 [/stat weights by rarity]\n" +
"[weapon weights] 1 1 1 1 1 [/weapon weights]\n"
}
func TestParseAndResolveCurrentForgingConfig(t *testing.T) {
document, err := dnfpvf.Parse(currentForgingPVFPath, testForgingConfigText())
if err != nil {
t.Fatal(err)
}
config, err := parseCurrentForgingConfig(document)
if err != nil {
t.Fatal(err)
}
if config.MaxLevel != 2 || len(config.Levels) != 2 || config.MaterialsByGrade[90].ItemID != 3326 || config.ItemRarityWeights[4] != 1.4 {
t.Fatalf("config = %+v", config)
}
source := dungeonDropCatalogTestSource{
"monster/monster.lst": "",
"stackable/stackable.lst": "3326 `material/strong_breath.stk`\n9100 `ticket/fixed2.stk`\n9101 `ticket/add1.stk`\n9102 `ticket/portable_forge.stk`\n9103 `ticket/random_forge.stk`\n9104 `ticket/random_enchant.stk`\n",
"equipment/equipment.lst": "700 `weapon/test.equ`\n701 `coat/test.equ`\n",
"stackable/material/strong_breath.stk": "[stackable type] `[material]`\n[stack limit] 1000\n",
"stackable/ticket/fixed2.stk": "[stackable type] `[etc]`\n[equipment separate reinforcement ticket]\n2 75\n`fixed` -1\n",
"stackable/ticket/add1.stk": "[stackable type] `[etc]`\n[equipment separate reinforcement ticket]\n1 100\n`additional` 1\n",
"stackable/ticket/portable_forge.stk": "[stackable type] `[waste]`\n[action type] `[portable genuine damage upgrade upgrede]`\n[/action type]\n[check usable itemlevel] 1 85\n",
"stackable/ticket/random_forge.stk": "[stackable type] `[etc]`\n[enchant random]\n[er_grade]\n0 1 2 3 5 6\n[er_slot]\n1\n[er_seal]\n0\n[er_enchant]\n2 1 40000 2 60000\n[/er_enchant]\n[/enchant random]\n[action type] `[random upgrade separate]`\n[/action type]\n",
"stackable/ticket/random_enchant.stk": "[stackable type] `[etc]`\n[enchant random]\n[er_slot]\n1\n[er_enchant]\n1 10 100000\n[/er_enchant]\n[/enchant random]\n",
"equipment/weapon/test.equ": "[equipment type] `[weapon]`\n[grade] 90\n[rarity] 4\n[minimum level] 16\n[durability] 45\n",
"equipment/coat/test.equ": "[equipment type] `[coat]`\n[grade] 90\n[rarity] 4\n[durability] 45\n",
}
catalog, err := newPVFDungeonDropCatalog(source)
if err != nil {
t.Fatal(err)
}
first, err := resolveCurrentForgingMetadata(catalog, config, 700, 0)
if err != nil {
t.Fatal(err)
}
if first.TargetLevel != 1 || first.IndependentAttack != 9 || first.SuccessWeight != 10000 || first.MaterialItemID != 3326 || first.MaterialCount != 63 || first.TargetDurability != 45 {
t.Fatalf("first resolution = %+v", first)
}
second, err := resolveCurrentForgingMetadata(catalog, config, 700, 1)
if err != nil || second.TargetLevel != 2 || second.IndependentAttack != 13 || second.SuccessWeight != 5000 || second.MaterialCount != 69 {
t.Fatalf("second resolution = %+v err=%v", second, err)
}
nonWeapon, err := resolveCurrentForgingMetadata(catalog, config, 701, 0)
if err != nil || nonWeapon.TargetLevel != 0 || nonWeapon.MaterialItemID != 0 {
t.Fatalf("non-weapon resolution = %+v err=%v", nonWeapon, err)
}
fixed, err := resolveCurrentForgingRequestMetadata(catalog, config, source, 9100, 700, 1)
if err != nil || fixed.TicketMode != "fixed" || fixed.TargetLevel != 2 || fixed.SuccessWeight != 7500 || fixed.MaterialItemID != 9100 || fixed.MaterialCount != 1 || fixed.IndependentAttack != 13 {
t.Fatalf("fixed ticket resolution = %+v err=%v", fixed, err)
}
additional, err := resolveCurrentForgingRequestMetadata(catalog, config, source, 9101, 700, 1)
if err != nil || additional.TicketMode != "additional" || additional.TargetLevel != 2 || additional.TicketApplyValue != 1 || additional.MaterialItemID != 9101 || additional.MaterialCount != 1 {
t.Fatalf("additional ticket resolution = %+v err=%v", additional, err)
}
capped, err := resolveCurrentForgingRequestMetadata(catalog, config, source, 9101, 700, 2)
if err != nil || capped.TicketMode != "additional" || capped.TargetLevel != 0 {
t.Fatalf("capped additional ticket resolution = %+v err=%v", capped, err)
}
ordinary, err := resolveCurrentForgingRequestMetadata(catalog, config, source, 3326, 700, 1)
if err != nil || ordinary.TicketMode != "" || ordinary.TargetLevel != 2 || ordinary.MaterialItemID != 3326 || ordinary.MaterialCount != 69 {
t.Fatalf("ordinary request resolution = %+v err=%v", ordinary, err)
}
portable, err := resolveCurrentForgingRequestMetadata(catalog, config, source, 9102, 700, 1)
if err != nil || portable.TicketMode != "" || portable.TargetLevel != 2 || portable.MaterialItemID != 9102 || portable.MaterialCount != 1 || portable.SuccessWeight != 5000 {
t.Fatalf("portable forging resolution = %+v err=%v", portable, err)
}
random, err := resolveCurrentForgingRequestMetadata(catalog, config, source, 9103, 700, 1)
if err != nil {
t.Fatal(err)
}
if !random.TicketRandom || random.TicketMode != "" || random.MaterialItemID != 9103 || random.MaterialCount != 1 {
t.Fatalf("random forging ticket resolution = %+v", random)
}
// The domain owns the roll, so no fixed level or weight may leak through.
if random.TargetLevel != 0 || random.SuccessWeight != 0 || random.IndependentAttack != 0 {
t.Fatalf("random forging ticket carried a pre-picked level: %+v", random)
}
if random.RandomTicket.SlotRestriction != 1 || random.RandomTicket.SealRestriction != 0 ||
len(random.RandomTicket.Rarities) != 6 || random.TargetRarity != 4 {
t.Fatalf("random forging ticket restrictions = %+v rarity=%d", random.RandomTicket, random.TargetRarity)
}
wantCandidates := []alignedcmd.UpgradeTicketRandomEntry{{TargetLevel: 1, Weight: 40000}, {TargetLevel: 2, Weight: 60000}}
if len(random.RandomTicket.Entries) != len(wantCandidates) {
t.Fatalf("random forging candidates = %+v", random.RandomTicket.Entries)
}
for index, want := range wantCandidates {
if random.RandomTicket.Entries[index] != want {
t.Fatalf("random forging candidate %d = %+v, want %+v", index, random.RandomTicket.Entries[index], want)
}
}
// Every candidate level carries its own independent attack, matching the
// ordinary per-level resolutions above.
if random.RandomIndependentAttacks[1] != 9 || random.RandomIndependentAttacks[2] != 13 {
t.Fatalf("random forging independent attacks = %+v", random.RandomIndependentAttacks)
}
// An [enchant random] stackable without the forging [action type] belongs
// to the op50 reinforcement ticket scene and must not be claimed here.
reinforcement, err := resolveCurrentForgingRequestMetadata(catalog, config, source, 9104, 700, 1)
if err != nil || reinforcement.TicketRandom || reinforcement.MaterialItemID != 3326 {
t.Fatalf("op50 random ticket claimed by op438: %+v err=%v", reinforcement, err)
}
}
// TestResolveCurrentUpgradeTicketMetadataSkipsForgingRandomTickets pins the
// other half of the split: the op50 resolver must leave a
// `[random upgrade separate]` coupon alone so it cannot move the
// reinforcement level.
func TestResolveCurrentUpgradeTicketMetadataSkipsForgingRandomTickets(t *testing.T) {
source := dungeonDropCatalogTestSource{
"monster/monster.lst": "",
"stackable/stackable.lst": "9103 `ticket/random_forge.stk`\n9104 `ticket/random_enchant.stk`\n",
"equipment/equipment.lst": "700 `weapon/test.equ`\n",
"stackable/ticket/random_forge.stk": "[stackable type] `[etc]`\n[enchant random]\n[er_slot]\n1\n[er_enchant]\n1 6 100000\n[/er_enchant]\n[/enchant random]\n[action type] `[random upgrade separate]`\n[/action type]\n",
"stackable/ticket/random_enchant.stk": "[stackable type] `[etc]`\n[enchant random]\n[er_slot]\n1\n[er_enchant]\n1 10 100000\n[/er_enchant]\n[/enchant random]\n",
"equipment/weapon/test.equ": "[equipment type] `[weapon]`\n[grade] 90\n[rarity] 4\n[minimum level] 16\n[durability] 45\n",
}
catalog, err := newPVFDungeonDropCatalog(source)
if err != nil {
t.Fatal(err)
}
forging, err := resolveCurrentUpgradeTicketMetadata(catalog, source, 9103, 700)
if err != nil {
t.Fatal(err)
}
if forging.TicketRandom || forging.TicketMode != "" {
t.Fatalf("forging random coupon claimed by op50: %+v", forging)
}
reinforcement, err := resolveCurrentUpgradeTicketMetadata(catalog, source, 9104, 700)
if err != nil {
t.Fatal(err)
}
if !reinforcement.TicketRandom || reinforcement.TicketMode != "reinforce" || len(reinforcement.RandomTicket.Entries) != 1 {
t.Fatalf("op50 random ticket = %+v", reinforcement)
}
}
func TestRealPVFCurrentForgingConfig(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify runtime upgrade_separate.etc")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
config, err := currentForgingConfigFromSource(archive)
if err != nil {
t.Fatal(err)
}
if config.MaxLevel != 8 || len(config.Levels) != 10 || len(config.MaterialsByGrade) != 99 || len(config.ItemRarityWeights) != 7 || len(config.StatRarityWeights) != 7 || len(config.WeaponWeights) == 0 {
t.Fatalf("runtime config max=%d levels=%d grades=%d itemRarities=%d statRarities=%d weaponWeights=%d", config.MaxLevel, len(config.Levels), len(config.MaterialsByGrade), len(config.ItemRarityWeights), len(config.StatRarityWeights), len(config.WeaponWeights))
}
if config.MaterialsByGrade[90].ItemID != 3326 || config.MaterialsByGrade[90].BaseCount != 45 || config.Levels[7].SuccessWeight != 750 {
t.Fatalf("runtime config grade90=%+v level8=%+v", config.MaterialsByGrade[90], config.Levels[7])
}
}
func TestRealPVFCurrentForgingTickets(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify runtime forging tickets")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
listText, err := archive.ReadText("stackable/stackable.lst")
if err != nil {
t.Fatal(err)
}
listDocument, err := dnfpvf.Parse("stackable/stackable.lst", listText)
if err != nil {
t.Fatal(err)
}
count := 0
for _, entry := range dnfpvf.ParseList(listDocument) {
pvfPath := path.Join("stackable", entry.Path)
text, readErr := archive.ReadText(pvfPath)
if readErr != nil || !strings.Contains(strings.ToLower(text), "[equipment separate reinforcement ticket]") {
continue
}
document, parseErr := dnfpvf.Parse(pvfPath, text)
if parseErr != nil {
t.Fatalf("parse forging ticket item=%d path=%s: %v", entry.ID, pvfPath, parseErr)
}
ticket, found, ticketErr := parseCurrentForgingTicket(document)
if ticketErr != nil || !found {
t.Fatalf("unsupported forging ticket item=%d path=%s ticket=%+v err=%v", entry.ID, pvfPath, ticket, ticketErr)
}
count++
}
if count == 0 {
t.Fatal("current PVF contains no [equipment separate reinforcement ticket] documents")
}
t.Logf("current PVF forging tickets supported count=%d", count)
}
// TestRealPVFRandomForgingTicket pins the shipped +6锻造券 2683788: an
// [enchant random] stackable whose [action type] is `[random upgrade
// separate]`, with a single er_enchant candidate at forging level 6.
func TestRealPVFRandomForgingTicket(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify the random forging ticket")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
catalog, err := newPVFDungeonDropCatalog(archive)
if err != nil {
t.Fatal(err)
}
config, err := currentForgingConfigFromSource(archive)
if err != nil {
t.Fatal(err)
}
const weaponItemID = 100240061
got, err := resolveCurrentForgingRequestMetadata(catalog, config, archive, 2683788, weaponItemID, 0)
if err != nil {
t.Fatal(err)
}
if !got.TicketRandom || got.MaterialItemID != 2683788 || got.MaterialCount != 1 {
t.Fatalf("random forging ticket = %+v", got)
}
if got.RandomTicket.SlotRestriction != 1 || got.RandomTicket.SealRestriction != 0 || len(got.RandomTicket.Rarities) != 6 {
t.Fatalf("random forging ticket restrictions = %+v", got.RandomTicket)
}
want := []alignedcmd.UpgradeTicketRandomEntry{{TargetLevel: 6, Weight: 100000}}
if len(got.RandomTicket.Entries) != 1 || got.RandomTicket.Entries[0] != want[0] {
t.Fatalf("random forging candidates = %+v, want %+v", got.RandomTicket.Entries, want)
}
if _, ok := got.RandomIndependentAttacks[6]; !ok {
t.Fatalf("no independent attack resolved for candidate level 6: %+v", got.RandomIndependentAttacks)
}
// The same coupon must stay invisible to the op50 reinforcement scene.
ticket, err := resolveCurrentUpgradeTicketMetadata(catalog, archive, 2683788, weaponItemID)
if err != nil {
t.Fatal(err)
}
if ticket.TicketRandom || ticket.TicketMode != "" {
t.Fatalf("random forging coupon claimed by op50: %+v", ticket)
}
}
func TestRealPVFPortableForgingDevicesUseTheirOwnMaterialContract(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify the portable forging device")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
catalog, err := newPVFDungeonDropCatalog(archive)
if err != nil {
t.Fatal(err)
}
config, err := currentForgingConfigFromSource(archive)
if err != nil {
t.Fatal(err)
}
for _, itemID := range []int64{10003912, 490702021, 490702282} {
got, resolveErr := resolveCurrentForgingRequestMetadata(catalog, config, archive, itemID, 101040191, 0)
if resolveErr != nil {
t.Fatalf("item=%d: %v", itemID, resolveErr)
}
if got.TargetLevel != 1 || got.MaterialItemID != itemID || got.MaterialCount != 1 || got.SuccessWeight <= 0 {
t.Fatalf("portable forging item=%d resolution=%+v", itemID, got)
}
}
}
func TestCurrentEquipmentForgingLevelForStatsOnlyAcceptsWornMainWeapon(t *testing.T) {
raw := make([]byte, currentItemListEntryWireSize)
raw[currentItemListForgingLevelOffset] = 4
for _, test := range []struct {
name string
entry dnfrepo.EquipmentEntry
want byte
}{
{name: "main_weapon_raw", entry: dnfrepo.EquipmentEntry{SlotIndex: currentRepositoryMainWeaponSlot, RawEntry: raw}, want: 4},
{name: "main_weapon_extra", entry: dnfrepo.EquipmentEntry{SlotIndex: currentRepositoryMainWeaponSlot, Extra: map[string]string{"forging_level": "6"}}, want: 6},
{name: "non_weapon_slot", entry: dnfrepo.EquipmentEntry{SlotIndex: currentRepositoryMainWeaponSlot + 1, RawEntry: raw, Extra: map[string]string{"forging_level": "6"}}, want: 0},
} {
t.Run(test.name, func(t *testing.T) {
if got := currentEquipmentForgingLevelForStats(test.entry); got != test.want {
t.Fatalf("forging level for stats = %d, want %d", got, test.want)
}
})
}
}
func TestCurrentItemListEquipmentProjectionUsesCurrentForgingOffset(t *testing.T) {
raw := make([]byte, currentItemListEntryWireSize)
raw[currentItemListForgingLevelOffset] = 4
raw[0x6D] = 0xC7
projected, ok := currentItemListEntryFromEquipment(dnfrepo.EquipmentEntry{
SlotIndex: 11,
ItemID: 700,
RawEntry: raw,
})
if !ok {
t.Fatal("equipment projection rejected main weapon")
}
if projected.data[currentItemListForgingLevelOffset] != 4 || projected.data[0x6D] != 0xC7 {
t.Fatalf("projected forging=%d opaque6d=%02x", projected.data[currentItemListForgingLevelOffset], projected.data[0x6D])
}
}