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,213 @@
package channelinfo
import (
"strconv"
"strings"
"testing"
)
func TestBuild90CNOnlineScriptExpandsPackedSourceServer(t *testing.T) {
data := []byte("[dungeon]\n`[sky_catle]` `sky` 11 12\n[/dungeon]\n[server]\n1 1 `tower` 2 `[none]` 0 0 0 6 `trade` 3 `[none]` 0 0 0 10 `forest` 0 `[granfloris]` 5 0 0 19 `crack` 1 `[crack]` 5 0 0 1 0 0 0\n[/server]\n[server]\n2 19 `other` 0 `[sky_catle]` 3 0 0\n[/server]\n")
script, err := Build90CNOnlineScript(data, 1, 0, 19)
if err != nil {
t.Fatal(err)
}
text := string(script)
for _, want := range []string{
"[dungeon]\n`[sky_catle]` `sky` 11 12\n[/dungeon]",
"[server]\n0\n",
"1 `tower` 11 `[none]` 0 0 0 ``\n",
"6 `trade` 3 `[none]` 0 0 0 ``\n",
"10 `forest` 22 `[granfloris]` 5 0 0 ``\n",
"19 `crack` 22 `[crack]` 5 0 0 1 0 0 0 ``\n",
} {
if !strings.Contains(text, want) {
t.Fatalf("online script is missing %q:\n%s", want, text)
}
}
if strings.Contains(text, "2 19 `other`") {
t.Fatalf("online script contains another server:\n%s", text)
}
index, err := Parse(script)
if err != nil {
t.Fatal(err)
}
channels := index.ChannelsForServer(0)
if len(channels) != 4 {
t.Fatalf("online channels = %d, want 4: %+v", len(channels), channels)
}
if channels[0].Type != 11 || channels[1].Type != 3 || channels[2].Type != 22 || channels[3].Type != 22 {
t.Fatalf("online channel types = %d/%d/%d/%d", channels[0].Type, channels[1].Type, channels[2].Type, channels[3].Type)
}
}
func TestBuild90CNOnlineScriptAcceptsInlineServerID(t *testing.T) {
data := []byte("[dungeon]\n`[granfloris]`\n<4::channel_info_dname_1>\n3\n[/dungeon]\n[server] 1\n10 `<chn_channel_info_028>` 11 `[granfloris]` 5 0.0 0.0 ``\n19 `<chn_channel_info_028>` 11 `[granfloris]` 5 0 0 ``\n[/server]\n")
script, err := Build90CNOnlineScript(data, 1, 0, 19)
if err != nil {
t.Fatal(err)
}
index, err := Parse(script)
if err != nil {
t.Fatal(err)
}
channels := index.ChannelsForServer(0)
if len(channels) != 2 || channels[0].ID != 10 || channels[1].ID != 19 {
t.Fatalf("online inline channels mismatch: %+v", channels)
}
if channels[0].NameKey != "chn_channel_info_028" ||
channels[1].NameKey != "chn_channel_info_028" ||
channels[0].AreaKey != "granfloris" ||
channels[1].AreaKey != "granfloris" {
t.Fatalf("online inline source mapping was not preserved: %+v", channels)
}
}
func TestBuild90CNOnlineScriptPreservesSourceMappingForChannel38(t *testing.T) {
data := []byte("[server]\n1\n19 `crack` 1 `[crack]` 5 0 0\n38 `trade` 3 `[trade]` 0 0 0\n[/server]\n")
script, err := Build90CNOnlineScript(data, 1, 0, 19)
if err != nil {
t.Fatal(err)
}
text := string(script)
if !strings.Contains(text, "38 `trade` 3 `[trade]`") {
t.Fatalf("online script did not preserve channel 38 source mapping:\n%s", text)
}
}
func TestBuild90CNOnlineScriptPreservesOriginPvpChannels(t *testing.T) {
data := []byte("[dungeon]\n`[pvp]`\n<4::chn_channel_info_044>\n[/dungeon]\n[server] 1\n" +
"19 `<chn_channel_info_028>` 11 `[granfloris]` 5 0 0 ``\n" +
"501 `<chn_channel_info_039>` 24 `[pvp]` 0 0 0 ``\n" +
"502 `<chn_channel_info_040>` 24 `[pvp]` 0 0 0 ``\n" +
"503 `<chn_channel_info_041>` 24 `[pvp]` 0 0 0 ``\n" +
"504 `<chn_channel_info_042>` 24 `[pvp]` 0 0 0 ``\n" +
"505 `<chn_channel_info_043>` 24 `[pvp]` 0 0 0 ``\n" +
"506 `<chn_channel_info_044>` 24 `[pvp]` 0 0 0 ``\n" +
"507 `<chn_channel_info_045>` 24 `[pvp]` 0 0 0 ``\n" +
"[/server]\n")
script, err := Build90CNOnlineScript(data, 1, 0, 19)
if err != nil {
t.Fatal(err)
}
index, err := Parse(script)
if err != nil {
t.Fatal(err)
}
channels := index.ChannelsForServer(0)
if len(channels) != 8 {
t.Fatalf("online channels = %d, want 8: %+v", len(channels), channels)
}
for i, channel := range channels[1:] {
wantID := 501 + i
wantNameKey := "chn_channel_info_0" + strconv.Itoa(39+i)
if channel.ID != wantID || channel.Type != 24 ||
channel.AreaKey != "pvp" || channel.NameKey != wantNameKey {
t.Fatalf("pvp channel %d mismatch: %+v", wantID, channel)
}
}
}
func TestBuild90CNOnlineScriptNormalizesExistingRecordTerminators(t *testing.T) {
data := []byte("[server]\r\n1\r\n10 `forest` 1 `[granfloris]` 5 0 0 ``\r\n19 `crack` 1 `[crack]` 5 0 0 ``\r\n[/server]\r\n")
script, err := Build90CNOnlineScript(data, 1, 0, 19)
if err != nil {
t.Fatal(err)
}
text := string(script)
if strings.Contains(text, " `` ``\n") {
t.Fatalf("online script contains duplicate record terminators:\n%s", text)
}
if got := strings.Count(text, " ``\n"); got != 2 {
t.Fatalf("online script terminators = %d, want 2:\n%s", got, text)
}
index, err := Parse(script)
if err != nil {
t.Fatal(err)
}
if got := len(index.ChannelsForServer(0)); got != 2 {
t.Fatalf("online channels = %d, want 2", got)
}
}
func TestOnlineTypeFor90CNSeparatesDirectoryTypeFromRawType(t *testing.T) {
tests := []struct {
channelID int
channelType int
want int
}{
{channelID: 10, channelType: 0, want: 22},
{channelID: 31, channelType: 1, want: 22},
{channelID: 1, channelType: 2, want: 11},
{channelID: 6, channelType: 3, want: 3},
{channelID: 51, channelType: 4, want: 4},
}
for _, test := range tests {
if got := OnlineTypeFor90CN(test.channelID, test.channelType); got != test.want {
t.Errorf("OnlineTypeFor90CN(%d, %d) = %d, want %d", test.channelID, test.channelType, got, test.want)
}
}
}
func TestBuild90CNOnlineScriptRequiresBootstrapChannel(t *testing.T) {
_, err := Build90CNOnlineScript([]byte("[server]\n1 10 `forest` 1 `[granfloris]` 0 0\n[/server]\n"), 1, 1, 19)
if err == nil || !strings.Contains(err.Error(), "missing bootstrap channel 19") {
t.Fatalf("error = %v, want missing bootstrap channel", err)
}
}
// TestBuild90CNOnlineScriptAllowingMissingBootstrap 宽松版必须照常产出脚本,
// 并用第二个返回值把"源表没有引导频道"这件事告诉调用方(由它决定告警还是拒绝)。
func TestBuild90CNOnlineScriptAllowingMissingBootstrap(t *testing.T) {
script, bootstrapPresent, err := Build90CNOnlineScriptAllowingMissingBootstrap(
[]byte("[server]\n1 171 `chn_channel_info_014` 22 `[deathtower]` 0 0\n[/server]\n"),
1,
1,
19,
)
if err != nil {
t.Fatalf("宽松模式下不该失败: %v", err)
}
if bootstrapPresent {
t.Fatal("源表里没有 19,bootstrapPresent 必须是 false")
}
online, err := Parse(script)
if err != nil {
t.Fatalf("宽松模式产出的脚本必须仍可解析: %v", err)
}
if _, ok := online.ServerChannel(1, 171); !ok {
t.Fatalf("宽松模式脚本里没有 171:\n%s", script)
}
// 引导频道在表里时必须报告 true,且严格版与宽松版产出一致。
withBootstrap := []byte("[server]\n1 19 `chn_channel_info_028` 11 `[granfloris]` 0 0\n[/server]\n")
relaxedScript, present, err := Build90CNOnlineScriptAllowingMissingBootstrap(withBootstrap, 1, 1, 19)
if err != nil || !present {
t.Fatalf("含引导频道时 present=%v err=%v, want true/nil", present, err)
}
strictScript, err := Build90CNOnlineScript(withBootstrap, 1, 1, 19)
if err != nil {
t.Fatal(err)
}
if string(relaxedScript) != string(strictScript) {
t.Fatalf("宽松版与严格版产出不一致:\n宽松:\n%s\n严格:\n%s", relaxedScript, strictScript)
}
}
func TestBuild90CNOnlineScriptRejectsUnquotedChannelFields(t *testing.T) {
_, err := Build90CNOnlineScript(
[]byte("[server]\n1\n19 crack 1 [crack] 5 0 0\n[/server]\n"),
1,
0,
19,
)
if err == nil || !strings.Contains(err.Error(), "invalid packed channel row") {
t.Fatalf("error = %v, want invalid packed channel row", err)
}
}