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

+167
View File
@@ -0,0 +1,167 @@
package services
import (
"os"
"path/filepath"
"testing"
)
func TestSetTextEncodingRefreshesArchiveAndCacheIdentity(t *testing.T) {
c := NewCore()
t.Cleanup(c.closeArchive)
svc := NewArchiveService(c)
if _, err := svc.SetTextEncoding("GBK"); err != ErrNoArchive {
t.Fatalf("without archive: %v", err)
}
if _, err := svc.Open(writeSearchFixture(t, "encoding.pvf")); err != nil {
t.Fatal(err)
}
waitForSearchIndex(t, c)
original := c.archive
before, err := searchIndexCacheIdentityForArchive(original)
if err != nil {
t.Fatal(err)
}
sqliteBefore, _, err := archiveIndexIdentity(original)
if err != nil {
t.Fatal(err)
}
c.editorText[0] = "stale cache"
info, err := svc.SetTextEncoding("GBK")
if err != nil {
t.Fatal(err)
}
waitForSearchIndex(t, c)
if info.TextEncoding != "GBK" || info.ModifiedCount != 0 {
t.Fatalf("unexpected state: %#v", info)
}
c.mu.RLock()
current := c.archive
cached := len(c.editorText)
c.mu.RUnlock()
if cached != 0 {
t.Fatal("editor text cache retained old decoding")
}
after, err := searchIndexCacheIdentityForArchive(current)
if err != nil || before == after {
t.Fatal("search cache identity unchanged", err)
}
sqliteAfter, _, err := archiveIndexIdentity(current)
if err != nil || sqliteBefore == sqliteAfter {
t.Fatal("SQLite identity unchanged", err)
}
if _, err := svc.SetTextEncoding("invalid"); err == nil {
t.Fatal("accepted invalid encoding")
}
if svc.Info().TextEncoding != "GBK" {
t.Fatal("failed switch changed archive")
}
c.mu.Lock()
if err := current.SetText(0, "1008 `changed.equ`"); err != nil {
c.mu.Unlock()
t.Fatal(err)
}
c.mu.Unlock()
if _, err := svc.SetTextEncoding("auto"); err == nil {
t.Fatal("discarded pending edits")
}
}
func TestArchiveTextEncodingPersistsAcrossRestart(t *testing.T) {
settingsPath := filepath.Join(t.TempDir(), "settings.json")
path := writeSearchFixture(t, "remember-encoding.pvf")
otherPath := writeSearchFixture(t, "other-encoding.pvf")
c := NewCore(newSettingsService(settingsPath))
svc := NewArchiveService(c)
t.Cleanup(c.closeArchive)
if _, err := svc.Open(path); err != nil {
t.Fatal(err)
}
if _, err := svc.SetTextEncoding("UTF-16LE"); err != nil {
t.Fatal(err)
}
svc.Close()
// A fresh core and settings service model an application restart.
next := NewCore(newSettingsService(settingsPath))
t.Cleanup(next.closeArchive)
reopened := NewArchiveService(next)
info, err := reopened.Open(path)
if err != nil {
t.Fatal(err)
}
if info.TextEncoding != "UTF-16LE" {
t.Fatalf("reopened encoding = %q", info.TextEncoding)
}
info, err = reopened.Open(otherPath)
if err != nil {
t.Fatal(err)
}
if info.TextEncoding != "auto" {
t.Fatal("encoding leaked to another archive")
}
if _, err := reopened.Open(path); err != nil {
t.Fatal(err)
}
if _, err := reopened.SetTextEncoding("invalid"); err == nil {
t.Fatal("accepted invalid codec")
}
if value, err := next.settings.archiveTextEncoding(path); err != nil || value != "UTF-16LE" {
t.Fatal("failed switch changed remembered codec", value, err)
}
if _, err := reopened.SetTextEncoding("auto"); err != nil {
t.Fatal(err)
}
reopened.Close()
info, err = svc.Open(path)
if err != nil {
t.Fatal(err)
}
if info.TextEncoding != "auto" {
t.Fatal("automatic selection was not remembered")
}
}
func TestArchiveTextEncodingCanonicalPathAndSettingsIsolation(t *testing.T) {
directory := t.TempDir()
settings := newSettingsService(filepath.Join(directory, "settings.json"))
path := writeSearchFixture(t, "encoding-source.pvf")
alias := filepath.Join(t.TempDir(), "alias.pvf")
if err := os.Symlink(path, alias); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
if err := settings.rememberArchiveTextEncoding(path, "Big5"); err != nil {
t.Fatal(err)
}
if value, err := settings.archiveTextEncoding(alias); err != nil || value != "Big5" {
t.Fatal("symlink did not share preference", value, err)
}
if err := settings.SaveSettings(DefaultAppSettings()); err != nil {
t.Fatal(err)
}
if value, err := newSettingsService(settings.path).archiveTextEncoding(path); err != nil || value != "Big5" {
t.Fatal("general settings erased preference", value, err)
}
}
func TestArchiveTextEncodingPersistenceFailureKeepsCurrentArchive(t *testing.T) {
directory := t.TempDir()
settings := newSettingsService(filepath.Join(directory, "settings.json"))
c := NewCore(settings)
t.Cleanup(c.closeArchive)
svc := NewArchiveService(c)
if _, err := svc.Open(writeSearchFixture(t, "encoding-failure.pvf")); err != nil {
t.Fatal(err)
}
// A directory cannot be read or replaced as the preference file.
if err := os.Mkdir(settings.archiveEncodingPath(), 0o755); err != nil {
t.Fatal(err)
}
original := c.archive
if _, err := svc.SetTextEncoding("GBK"); err == nil {
t.Fatal("persistence failure was ignored")
}
if c.archive != original || svc.Info().TextEncoding != "auto" {
t.Fatal("failed switch changed active archive")
}
}