DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,223 @@
package dnfbridge
import (
"context"
"testing"
dnfhellparty "longheng.io/server/internal/modules/dnf/hellparty"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
"longheng.io/server/internal/modules/dnf/worldmap"
appkit "longheng.io/server/internal/platform/app"
)
func TestGMUnlocksAllDungeonDifficultiesAndHellPermissions(t *testing.T) {
t.Parallel()
ctx := context.Background()
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
CharacterID: "77", AccountID: "dnf:gm-dungeon", Name: "Dungeon GM", Job: "0", Level: 90,
}); err != nil {
t.Fatal(err)
}
source := bridgeDungeonPVF(false)
source["dungeon/test.dgn"] = "[name]\n`Synthetic Dungeon`\n" +
"[difficulty level]\n1 2 3 4 5\n" +
"[maze info]\n[size]\n1 1\n[greed]\n`A`\n" +
"[map specification]\n`map` 0 0 100\n[start map]\n0 0\n[boss map]\n0 0\n"
source[worldmap.DefaultWorldMapList] = "39 `test.wdm`\n"
source["worldmap/test.wdm"] = "[name]\n`Synthetic Hell Area`\n" +
"[dungeon]\n700 -1\n[/dungeon]\n" +
"[hell dungeon]\n1\n" +
"[hell quest]\n2900 2901\n[/hell quest]\n"
table, resolver, _ := loadBridgeDungeonStaticData(t, source)
service := NewWithOptions(&appkit.Env{}, ServiceOptions{
RepositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
})
service.worldMapTable = table
service.worldMapResolver = resolver
initial, err := service.GMDungeonAccess(ctx, "77")
if err != nil || initial.DifficultyDungeons != 1 || initial.DifficultyDungeonsUnlocked != 0 ||
initial.HellAreas != 1 || initial.HellPermissionQuests != 2 || initial.HellPermissionQuestsCleared != 0 {
t.Fatalf("initial dungeon access=%+v err=%v", initial, err)
}
difficulties, err := service.GMUnlockAllDungeonDifficulties(ctx, "77")
if err != nil || difficulties.DifficultyDungeonsUnlocked != 1 || difficulties.DifficultyDungeonsUpdated != 1 {
t.Fatalf("difficulty unlock=%+v err=%v", difficulties, err)
}
permission, found, err := repositories.DungeonPermission.Load(ctx, "77")
if err != nil || !found || len(permission.Entries) != 1 || permission.Entries[0].DungeonID != 700 || permission.Entries[0].ClearState != 4 {
t.Fatalf("permission=%+v found=%t err=%v", permission, found, err)
}
hell, err := service.GMUnlockAllHellChallenges(ctx, "77")
if err != nil || hell.HellPermissionQuestsCleared != 2 || hell.HellPermissionQuestsUpdated != 2 {
t.Fatalf("hell unlock=%+v err=%v", hell, err)
}
quests, found, err := repositories.Quest.Load(ctx, "77")
if err != nil || !found {
t.Fatalf("quest record found=%t err=%v", found, err)
}
for _, questID := range []int64{2900, 2901} {
state := quests.States[questID]
if state.Status != "completed" || state.Extra["gm_rewards_granted"] != "false" || state.Extra["gm_all_hell_unlocked"] != "true" {
t.Fatalf("quest %d state=%+v", questID, state)
}
}
hellPermission, found, err := repositories.HellPartyPermission.Load(ctx, "77")
if err != nil || !found || len(hellPermission.Entries) != 1 || hellPermission.Entries[0].AreaID != 39 || hellPermission.Entries[0].QuestID != 2900 {
t.Fatalf("hell permission=%+v found=%t err=%v", hellPermission, found, err)
}
session := &gameSession{selectedCharacterID: 77}
if err := service.authorizeCurrentDungeonHellParty(ctx, session, table, 700); err != nil {
t.Fatalf("hell entry after GM unlock error=%v", err)
}
repeated, err := service.GMUnlockAllHellChallenges(ctx, "77")
if err != nil || repeated.HellPermissionQuestsUpdated != 0 {
t.Fatalf("repeated hell unlock=%+v err=%v", repeated, err)
}
// A character whose quests were already completed by an earlier unlock that
// never wrote the permission row is repaired by running the unlock again.
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
CharacterID: "78", AccountID: "dnf:gm-dungeon", Name: "Legacy Unlock", Job: "0", Level: 90,
}); err != nil {
t.Fatal(err)
}
if err := repositories.Quest.Save(ctx, dnfrepo.QuestRecord{
CharacterID: "78",
States: map[int64]dnfrepo.QuestState{
2900: {Status: "completed"},
2901: {Status: "completed"},
},
}); err != nil {
t.Fatal(err)
}
legacy, err := service.GMUnlockAllHellChallenges(ctx, "78")
if err != nil || legacy.HellPermissionQuestsUpdated != 0 || legacy.HellPermissionQuestsCleared != 2 {
t.Fatalf("legacy hell unlock=%+v err=%v", legacy, err)
}
legacyPermission, found, err := repositories.HellPartyPermission.Load(ctx, "78")
if err != nil || !found || len(legacyPermission.Entries) != 1 || legacyPermission.Entries[0].AreaID != 39 {
t.Fatalf("legacy hell permission=%+v found=%t err=%v", legacyPermission, found, err)
}
}
func TestGMHellEpicDropRatePersistsAndScalesPVFRarityRate(t *testing.T) {
t.Parallel()
ctx := context.Background()
repositories := dnfrepomemory.NewMemoryGroup()
newService := func() *Service {
return NewWithOptions(&appkit.Env{}, ServiceOptions{
RepositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
})
}
service := newService()
settings, err := service.GMGameplaySettings(ctx)
if err != nil || settings.HellEpicDropRatePercent != gmHellEpicDropRateDefaultPercent {
t.Fatalf("default settings=%+v err=%v", settings, err)
}
settings, err = service.GMSetHellEpicDropRate(ctx, 250)
if err != nil || settings.HellEpicDropRatePercent != 250 {
t.Fatalf("saved settings=%+v err=%v", settings, err)
}
persisted, err := newService().GMGameplaySettings(ctx)
if err != nil || persisted.HellEpicDropRatePercent != 250 {
t.Fatalf("persisted settings=%+v err=%v", persisted, err)
}
for _, test := range []struct {
base int64
percent int
want int64
}{
{base: 7_000, percent: 100, want: 7_000},
{base: 7_000, percent: 1_000, want: 70_000},
{base: 20_000, percent: 10_000, want: int64(dnfhellparty.RarityRollDenominator)},
{base: 7_000, percent: 0, want: 0},
{base: 0, percent: 10000, want: 0},
} {
if got := scaleGMHellEpicRarityRate(test.base, test.percent); got != test.want {
t.Fatalf("scale base=%d percent=%d got=%d want=%d", test.base, test.percent, got, test.want)
}
}
if _, err := service.GMSetHellEpicDropRate(ctx, gmHellEpicDropRateMaximumPercent+1); err == nil {
t.Fatal("out-of-range hell epic drop rate was accepted")
}
if maximum, err := service.GMSetHellEpicDropRate(ctx, gmHellEpicDropRateMaximumPercent); err != nil || maximum.HellEpicDropRatePercent != gmHellEpicDropRateMaximumPercent {
t.Fatalf("maximum setting=%+v err=%v", maximum, err)
}
}
func TestGMAdjustHellEpicRarity(t *testing.T) {
const base = int64(10_000)
if rarity, keep := gmAdjustHellEpicRarity(4, base, 0, 0); keep || rarity != 4 {
t.Fatalf("zero-percent epic = rarity %d keep %t", rarity, keep)
}
if rarity, keep := gmAdjustHellEpicRarity(3, base, 200, 0); !keep || rarity != 4 {
t.Fatalf("boosted non-epic = rarity %d keep %t", rarity, keep)
}
if rarity, keep := gmAdjustHellEpicRarity(3, base, 100, 0); !keep || rarity != 3 {
t.Fatalf("default non-epic = rarity %d keep %t", rarity, keep)
}
}
// TestGMEpicPotionTestMultiplierPersistsAndDoesNotDisturbTheHellRate:
// 服主要"先改一下倍率来测药剂是否真生效,后面再改回从 PVF 读取",所以这个旋钮必须
//
// ① 默认 0(= 关闭,按 PVF 表走);
// ② 能存能读(换一个 Service 实例也读得到);
// ③ 与 GM 全局掉落率**互不干扰**(改一个不覆盖另一个);
// ④ 越界要被拒绝。
func TestGMEpicPotionTestMultiplierPersistsAndDoesNotDisturbTheHellRate(t *testing.T) {
t.Parallel()
ctx := context.Background()
repositories := dnfrepomemory.NewMemoryGroup()
newService := func() *Service {
return NewWithOptions(&appkit.Env{}, ServiceOptions{
RepositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
})
}
service := newService()
settings, err := service.GMGameplaySettings(ctx)
if err != nil || settings.EpicDropRatePotionTestMultiplierPercent != gmEpicDropRatePotionTestMultiplierDefaultPercent {
t.Fatalf("default potion test multiplier=%+v err=%v", settings, err)
}
if _, err := service.GMSetHellEpicDropRate(ctx, 250); err != nil {
t.Fatal(err)
}
settings, err = service.GMSetEpicDropRatePotionTestMultiplier(ctx, 2000)
if err != nil {
t.Fatal(err)
}
if settings.EpicDropRatePotionTestMultiplierPercent != 2000 || settings.HellEpicDropRatePercent != 250 {
t.Fatalf("after potion set=%+v want potion 2000 + hell 250 kept", settings)
}
persisted, err := newService().GMGameplaySettings(ctx)
if err != nil || persisted.EpicDropRatePotionTestMultiplierPercent != 2000 ||
persisted.HellEpicDropRatePercent != 250 {
t.Fatalf("persisted=%+v err=%v", persisted, err)
}
// 改回全局掉落率不能把测试倍率冲掉。
if _, err := newService().GMSetHellEpicDropRate(ctx, 300); err != nil {
t.Fatal(err)
}
after, err := newService().GMGameplaySettings(ctx)
if err != nil || after.EpicDropRatePotionTestMultiplierPercent != 2000 || after.HellEpicDropRatePercent != 300 {
t.Fatalf("after hell set=%+v err=%v", after, err)
}
// 关掉旋钮 = 回到 PVF 表。
off, err := newService().GMSetEpicDropRatePotionTestMultiplier(ctx, 0)
if err != nil || off.EpicDropRatePotionTestMultiplierPercent != 0 {
t.Fatalf("disabled=%+v err=%v", off, err)
}
for _, bad := range []int{-1, gmEpicDropRatePotionTestMultiplierMaximumPercent + 1} {
if _, err := newService().GMSetEpicDropRatePotionTestMultiplier(ctx, bad); err == nil {
t.Fatalf("out-of-range potion test multiplier %d was accepted", bad)
}
}
}