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,222 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/premium"
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func TestParseCurrentPremiumListDocumentMapsItemToTypeAndDuration(t *testing.T) {
|
||||
text := "[type]\n8\n[target item]\n16\n[attr]\n`[term]`\n[term]\n30\n[/type]\n" +
|
||||
"[type]\n22\n[target item]\n2\n[item]\n30\n[term]\n1\n[/term]\n[/item]\n[item]\n10096109\n[term]\n60\n[is term unit minute]\n1\n[/is term unit minute]\n[/item]\n[/type]\n" +
|
||||
"[type]\n27\n[target item]\n1\n[item]\n43\n[term]\n1\n[/term]\n[/item]\n[/type]\n" +
|
||||
"[type]\n84\n[item]\n2660409\n[term]\n3\n[/term]\n[/item]\n[bonus exp]\n20\n[/bonus exp]\n[quest item drop rate]\n20\n[/quest item drop rate]\n[independent drop rate]\n20 23 27 30\n[/independent drop rate]\n[/type]\n"
|
||||
document, err := dnfpvf.Parse("etc/premiumlist_new.etc", text)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := ¤tPremiumCatalog{contractsByItem: make(map[int64]currentPremiumContractInfo)}
|
||||
parseCurrentPremiumListDocument(document, catalog)
|
||||
privateStore, ok := catalog.contractsByItem[16]
|
||||
if !ok || privateStore.PremiumType != 8 || privateStore.DurationSeconds != 30*86400 {
|
||||
t.Fatalf("private-store license = %+v", privateStore)
|
||||
}
|
||||
|
||||
monarch, ok := catalog.contractsByItem[30]
|
||||
if !ok || monarch.PremiumType != 22 || monarch.DurationSeconds != 86400 {
|
||||
t.Fatalf("monarch = %+v", monarch)
|
||||
}
|
||||
minuteItem, ok := catalog.contractsByItem[10096109]
|
||||
if !ok || minuteItem.PremiumType != 22 || minuteItem.DurationSeconds != 3600 {
|
||||
t.Fatalf("minute item = %+v, want 3600s", minuteItem)
|
||||
}
|
||||
expert, ok := catalog.contractsByItem[43]
|
||||
if !ok || expert.PremiumType != 27 || expert.DurationSeconds != 86400 {
|
||||
t.Fatalf("expert = %+v", expert)
|
||||
}
|
||||
growth, ok := catalog.contractsByItem[2660409]
|
||||
if !ok || growth.PremiumType != 84 || growth.DurationSeconds != 3*86400 {
|
||||
t.Fatalf("growth = %+v", growth)
|
||||
}
|
||||
effect := catalog.effectsByType[84]
|
||||
if effect.BonusExperiencePercent != 20 ||
|
||||
effect.QuestItemDropRatePercent != 20 ||
|
||||
len(effect.IndependentDropRatePercents) != 4 ||
|
||||
effect.independentDropRatePercent(1) != 20 ||
|
||||
effect.independentDropRatePercent(4) != 30 ||
|
||||
effect.independentDropRatePercent(9) != 30 {
|
||||
t.Fatalf("growth effect = %+v", effect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCurrentPremiumDevilSlotsParsesAllPerks(t *testing.T) {
|
||||
text := "[selectable character premium]\n" +
|
||||
"-1 2681927 -1 7 0 2000 `魔王之契约` 0 -1 " +
|
||||
"100817 2682205 6 7 0 250 `自动修理` 0 -1 " +
|
||||
"100882 2682206 7 7 0 200 `高效开罐` 0 -1 " +
|
||||
"100542 2681934 0 7 0 200 `黄金卡牌免费翻` 0 -1 " +
|
||||
"100818 2682205 6 30 0 500 `自动修理` 0 -1\n" +
|
||||
"[/selectable character premium]\n"
|
||||
document, err := dnfpvf.Parse("etc/cerashop.etc", text)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := ¤tPremiumCatalog{devilSlots: make(map[uint32]currentPremiumDevilSlotInfo)}
|
||||
parseCurrentPremiumDevilSlots(document, catalog)
|
||||
|
||||
if len(catalog.devilSlots) != 4 {
|
||||
t.Fatalf("devil slots = %d, want 4: %+v", len(catalog.devilSlots), catalog.devilSlots)
|
||||
}
|
||||
repair, ok := catalog.devilSlots[100817]
|
||||
if !ok || repair.Slot != 6 || repair.Days != 7 || repair.CeraPrice != 250 || repair.ItemID != 2682205 {
|
||||
t.Fatalf("repair perk = %+v", repair)
|
||||
}
|
||||
card, ok := catalog.devilSlots[100542]
|
||||
if !ok || card.Slot != 0 || card.Days != 7 || card.CeraPrice != 200 {
|
||||
t.Fatalf("card perk = %+v", card)
|
||||
}
|
||||
if _, ok := catalog.devilSlots[2681927]; ok {
|
||||
t.Fatalf("display-only contract row must be skipped")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCurrentCrystalContractCubesUsesNativeSelectionOrder(t *testing.T) {
|
||||
text := "3037 `material/cubepiece_clear.stk`\n" +
|
||||
"3035 `material/cubepiece_red.stk`\n" +
|
||||
"3262 `material/cubepiece_gold.stk`\n" +
|
||||
"3033 `material/cubepiece_black.stk`\n" +
|
||||
"3036 `material/cubepiece_blue.stk`\n" +
|
||||
"3034 `material/cubepiece_white.stk`\n"
|
||||
document, err := dnfpvf.Parse("stackable/stackable.lst", text)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := ¤tPremiumCatalog{}
|
||||
if err := parseCurrentCrystalContractCubes(document, catalog); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := [6]int64{3033, 3034, 3035, 3036, 3037, 3262}
|
||||
if catalog.crystalCubeIDs != want {
|
||||
t.Fatalf("crystal cube IDs = %v, want %v", catalog.crystalCubeIDs, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealPVFPremiumCatalogCoversNamedContracts(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("DNFBRIDGE_REAL_PVF_SMOKE not set")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open PVF: %v", err)
|
||||
}
|
||||
catalog, err := buildCurrentPremiumCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatalf("build catalog: %v", err)
|
||||
}
|
||||
expect := map[int64]int64{
|
||||
30: 22, // 霸王 1天
|
||||
34: 22, // 霸王 15天
|
||||
43: 27, // 达人 1天
|
||||
46: 27, // 达人 15天
|
||||
2660703: 84, // 成长 [item] 1天
|
||||
10000389: 97, // 晶之契约 7天
|
||||
}
|
||||
for itemID, wantType := range expect {
|
||||
info, ok := catalog.contractsByItem[itemID]
|
||||
if !ok || info.PremiumType != wantType {
|
||||
t.Fatalf("item %d = %+v, want type %d", itemID, info, wantType)
|
||||
}
|
||||
if info.DurationSeconds < 86400 {
|
||||
t.Fatalf("item %d duration = %d, want >= 1 day", itemID, info.DurationSeconds)
|
||||
}
|
||||
}
|
||||
type29Contract, ok := catalog.contractsByItem[193]
|
||||
if !ok || type29Contract.PremiumType != 29 || type29Contract.DurationSeconds < 86400 {
|
||||
t.Fatalf("ordinary premium item 193 = %+v, want type 29 and >= one day", type29Contract)
|
||||
}
|
||||
type8Contract, ok := catalog.contractsByItem[16]
|
||||
if !ok || type8Contract.PremiumType != premium.TypePrivateStore || type8Contract.DurationSeconds != 30*86400 {
|
||||
t.Fatalf("private-store target-item contract 16 = %+v, want type 8 and 30 days", type8Contract)
|
||||
}
|
||||
for itemID, wantType := range map[int64]int64{
|
||||
31: 22, // 霸王之契约 3天
|
||||
44: 27, // 达人之契约 3天
|
||||
2660409: 84, // 成长之契约 3天
|
||||
10000388: 97, // 晶之契约 3天
|
||||
} {
|
||||
info, ok := catalog.contractsByItem[itemID]
|
||||
if !ok || info.PremiumType != wantType || info.DurationSeconds != 3*86400 {
|
||||
t.Fatalf("three-day contract item %d = %+v, want type=%d duration=%d", itemID, info, wantType, 3*86400)
|
||||
}
|
||||
}
|
||||
growthEffect := catalog.effectsByType[84]
|
||||
if growthEffect.BonusExperiencePercent != 20 ||
|
||||
growthEffect.QuestItemDropRatePercent != 20 ||
|
||||
len(growthEffect.IndependentDropRatePercents) != 4 ||
|
||||
growthEffect.independentDropRatePercent(1) != 20 ||
|
||||
growthEffect.independentDropRatePercent(4) != 30 {
|
||||
t.Fatalf("runtime-PVF growth effect = %+v, want exp=20 quest_drop=20 independent=[20 23 27 30]", growthEffect)
|
||||
}
|
||||
for _, premiumType := range []int64{29, 80, 88, 92, 100} {
|
||||
found := false
|
||||
for _, info := range catalog.contractsByItem {
|
||||
if info.PremiumType == premiumType {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("runtime-PVF premium type %d has no activation item", premiumType)
|
||||
}
|
||||
}
|
||||
// A [target item] remains display metadata when the same type has explicit
|
||||
// [item] rows. Only legacy blocks without explicit rows (such as type 8)
|
||||
// use it as their activation item.
|
||||
if _, ok := catalog.contractsByItem[2660408]; ok {
|
||||
t.Fatalf("[target item] 2660408 must not become an activation item")
|
||||
}
|
||||
if len(catalog.devilSlots) != 16 {
|
||||
t.Fatalf("devil perks = %d, want 16 (8 slots x 7/30 days)", len(catalog.devilSlots))
|
||||
}
|
||||
if want := [6]int64{3033, 3034, 3035, 3036, 3037, 3262}; catalog.crystalCubeIDs != want {
|
||||
t.Fatalf("runtime-PVF crystal cube IDs = %v, want %v", catalog.crystalCubeIDs, want)
|
||||
}
|
||||
expectedDevilProducts := map[uint32]struct {
|
||||
slot int64
|
||||
days int64
|
||||
}{
|
||||
100542: {slot: 0, days: 7},
|
||||
100539: {slot: 1, days: 7},
|
||||
100540: {slot: 2, days: 7},
|
||||
100541: {slot: 3, days: 7},
|
||||
100537: {slot: 4, days: 7},
|
||||
100538: {slot: 5, days: 7},
|
||||
100817: {slot: 6, days: 7},
|
||||
100882: {slot: 7, days: 7},
|
||||
100550: {slot: 0, days: 30},
|
||||
100547: {slot: 1, days: 30},
|
||||
100548: {slot: 2, days: 30},
|
||||
100549: {slot: 3, days: 30},
|
||||
100545: {slot: 4, days: 30},
|
||||
100546: {slot: 5, days: 30},
|
||||
100818: {slot: 6, days: 30},
|
||||
100883: {slot: 7, days: 30},
|
||||
}
|
||||
for commodityID, expected := range expectedDevilProducts {
|
||||
info, found := catalog.devilSlots[commodityID]
|
||||
if !found || info.Slot != expected.slot || info.Days != expected.days {
|
||||
t.Fatalf(
|
||||
"devil commodity %d = %+v found=%t, want slot=%d days=%d",
|
||||
commodityID,
|
||||
info,
|
||||
found,
|
||||
expected.slot,
|
||||
expected.days,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user