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,129 @@
package dungeonpermission
import (
"context"
"strings"
"testing"
"longheng.io/server/internal/modules/dnf/repository"
"longheng.io/server/internal/modules/dnf/repository/memory"
)
func TestAccountOwnerKeyIsNamespacedFixedWidthAndDeterministic(t *testing.T) {
first, err := AccountOwnerKey(" account-1 ")
if err != nil {
t.Fatal(err)
}
second, err := AccountOwnerKey("account-1")
if err != nil {
t.Fatal(err)
}
other, err := AccountOwnerKey("account-2")
if err != nil {
t.Fatal(err)
}
if first != second || first == other || !strings.HasPrefix(first, accountOwnerKeyPrefix) || len(first) > 128 {
t.Fatalf("account owner keys first=%q second=%q other=%q", first, second, other)
}
}
func TestLoadAccountMergesLegacyCharacterMaximaAndMigratesOwner(t *testing.T) {
ctx := context.Background()
repositories := memory.NewMemoryGroup()
for _, character := range []repository.CharacterRecord{
{CharacterID: "11", AccountID: "account-1", Slot: 0},
{CharacterID: "12", AccountID: "account-1", Slot: 1},
{CharacterID: "21", AccountID: "account-2", Slot: 0},
} {
if err := repositories.Character.Save(ctx, character); err != nil {
t.Fatal(err)
}
}
for _, seed := range []struct {
owner string
dungeon uint32
state byte
}{
{owner: "11", dungeon: 700, state: 1},
{owner: "11", dungeon: 701, state: 3},
{owner: "12", dungeon: 700, state: 4},
{owner: "21", dungeon: 700, state: 5},
} {
if _, _, err := repositories.DungeonPermission.UpsertMax(ctx, seed.owner, seed.dungeon, seed.state); err != nil {
t.Fatal(err)
}
}
owner, err := NewOwner(repositories)
if err != nil {
t.Fatal(err)
}
snapshot, err := owner.LoadAccount(ctx, "account-1")
if err != nil {
t.Fatal(err)
}
if snapshot.LegacyOwners != 2 || snapshot.MigratedEntries != 2 {
t.Fatalf("migration snapshot=%+v", snapshot)
}
want := map[uint32]byte{700: 4, 701: 3}
if got := entryStates(snapshot.Record); !equalStates(got, want) {
t.Fatalf("account states=%v want=%v", got, want)
}
persisted, found, err := repositories.DungeonPermission.Load(ctx, snapshot.StorageKey)
if err != nil || !found || !equalStates(entryStates(persisted), want) {
t.Fatalf("persisted account record=%+v found=%t err=%v", persisted, found, err)
}
again, err := owner.LoadAccount(ctx, "account-1")
if err != nil || again.MigratedEntries != 0 || !equalStates(entryStates(again.Record), want) {
t.Fatalf("idempotent account snapshot=%+v err=%v", again, err)
}
}
func TestAdvanceIsSharedByEveryCharacterAndIsolatesAccounts(t *testing.T) {
ctx := context.Background()
repositories := memory.NewMemoryGroup()
for _, character := range []repository.CharacterRecord{
{CharacterID: "11", AccountID: "account-1", Slot: 0},
{CharacterID: "12", AccountID: "account-1", Slot: 1},
{CharacterID: "21", AccountID: "account-2", Slot: 0},
} {
if err := repositories.Character.Save(ctx, character); err != nil {
t.Fatal(err)
}
}
owner, err := NewOwner(repositories)
if err != nil {
t.Fatal(err)
}
snapshot, entry, changed, err := owner.Advance(ctx, "account-1", 700, 3)
if err != nil || !changed || entry.ClearState != 3 {
t.Fatalf("advance snapshot=%+v entry=%+v changed=%t err=%v", snapshot, entry, changed, err)
}
shared, err := owner.LoadAccount(ctx, "account-1")
if err != nil || entryStates(shared.Record)[700] != 3 {
t.Fatalf("shared snapshot=%+v err=%v", shared, err)
}
isolated, err := owner.LoadAccount(ctx, "account-2")
if err != nil || entryStates(isolated.Record)[700] != 0 {
t.Fatalf("isolated snapshot=%+v err=%v", isolated, err)
}
}
func entryStates(record repository.DungeonPermissionRecord) map[uint32]byte {
out := make(map[uint32]byte, len(record.Entries))
for _, entry := range record.Entries {
out[entry.DungeonID] = entry.ClearState
}
return out
}
func equalStates(left, right map[uint32]byte) bool {
if len(left) != len(right) {
return false
}
for key, value := range left {
if right[key] != value {
return false
}
}
return true
}