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

+322
View File
@@ -0,0 +1,322 @@
package main
import (
"embed"
"log"
"log/slog"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/updater"
githubupdater "github.com/wailsapp/wails/v3/pkg/updater/providers/github"
"pvfine/internal/buildmode"
"pvfine/services"
)
// Release builds replace these values with -ldflags. Local builds use a
// synthetic baseline version so manual update checks remain available.
var (
appVersion = "0.0.0"
githubRepository = "dof-dev/pvfine"
)
const (
quitRequestedEvent = "app:quit-requested"
closeRequestedEvent = "app:close-requested"
quitConfirmedEvent = "app:quit-confirmed"
closeConfirmedEvent = "app:close-confirmed"
// mainWindowName identifies the archive editor window. It must be set
// explicitly, otherwise Wails names it "window-N" and it cannot be
// distinguished from the detached script window by name.
mainWindowName = "main"
)
type closeCoordinator struct {
app *application.App
// allowWindowClosing permits the next close of the window that is actually
// being closed. A single flag is not enough once a second window exists:
// app.Window.Current() reports the last interacted window, so approving a
// close would target whichever window the user touched most recently.
allowWindowClosing atomic.Bool
// pendingClose remembers which window asked to close so the confirmation
// closes that window instead of guessing from window focus.
pendingClose atomic.Uint64
// scriptWindow lets the quit path release the detached script window without
// waiting for its own close confirmation.
scriptWindow *services.ScriptWindowService
}
func newCloseCoordinator(app *application.App, scriptWindow *services.ScriptWindowService) *closeCoordinator {
coordinator := &closeCoordinator{app: app, scriptWindow: scriptWindow}
app.Event.On(closeConfirmedEvent, func(*application.CustomEvent) {
coordinator.allowWindowClosing.Store(true)
// Close the window that started this handshake; falling back to the
// focused window would close the wrong one when two windows are open.
id := uint(coordinator.pendingClose.Swap(0))
addressable := false
if id != 0 {
if window, ok := app.Window.GetByID(id); ok && window != nil {
// 主窗口关闭时,独立脚本窗口不能留下:它已经没有回到归档编辑的
// 入口,留下来就是一个无法操作的窗口。
if coordinator.scriptWindow != nil && window.Name() == mainWindowName {
coordinator.scriptWindow.AllowScriptWindowClose()
coordinator.scriptWindow.CloseScriptWindowIfOpen()
}
window.Close()
addressable = true
}
}
if !addressable {
if window := app.Window.Current(); window != nil {
window.Close()
}
}
})
app.Event.On(quitConfirmedEvent, func(*application.CustomEvent) {
// The detached script window keeps its own close confirmation and its own
// Pinia store; release it first so quitting cannot stall on it.
if coordinator.scriptWindow != nil {
coordinator.scriptWindow.AllowScriptWindowClose()
coordinator.scriptWindow.CloseScriptWindowIfOpen()
}
app.Quit()
})
return coordinator
}
func (c *closeCoordinator) requestQuit() {
_ = c.app.Event.Emit(quitRequestedEvent)
}
// handlerFor returns the close handler for one specific window. Binding the
// window keeps the confirmation handshake pointing at the window the user
// actually tried to close.
func (c *closeCoordinator) handlerFor(window application.Window) func(*application.WindowEvent) {
return func(event *application.WindowEvent) {
if c.allowWindowClosing.CompareAndSwap(true, false) {
return
}
if window != nil {
c.pendingClose.Store(uint64(window.ID()))
}
event.Cancel()
_ = c.app.Event.Emit(closeRequestedEvent)
}
}
// Wails uses Go's `embed` package to embed the frontend files into the binary.
// Any files in the frontend/dist folder will be embedded into the binary and
// made available to the frontend.
// See https://pkg.go.dev/embed for more information.
//go:embed all:frontend/dist
var assets embed.FS
//go:embed frontend/src/script-api.d.ts
var scriptAPIDeclaration string
func main() {
logOutput := services.CaptureLogs(log.Writer())
log.SetOutput(logOutput)
application.DefaultServiceOptions.MarshalError = services.LogServiceError
settingsService := services.NewSettingsService()
core := services.NewCore(settingsService)
// The backup service attaches itself to the core so saving or closing the
// workspace can drop a cache that no longer protects anything.
autosaveService := services.NewAutosaveService(core, settingsService)
cacheService := services.NewCacheService(core, settingsService)
// One file set service backs both the sidebar and the script API, so a
// scripted change and a manual save target the same document.
fileSetService := services.NewFileSetService()
archiveService := services.NewArchiveService(core)
editorService := services.NewEditorService(core, settingsService)
scriptService := services.NewScriptService(core, fileSetService)
exportService := services.NewExportService(core)
mcpService := services.NewMCPService(core, archiveService, editorService, scriptService, exportService, scriptAPIDeclaration)
runtimeLogLevel := slog.LevelWarn
if buildmode.Development {
runtimeLogLevel = slog.LevelInfo
}
app := application.New(application.Options{
Name: "pvfine",
Description: "PVF 归档编辑器",
Logger: slog.New(slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: runtimeLogLevel})),
MarshalError: services.LogServiceError,
ErrorHandler: func(err error) { services.LogServiceError(err) },
FileAssociations: []string{".pvf"},
Services: []application.Service{
application.NewService(archiveService),
application.NewService(editorService),
application.NewService(services.NewDropService(core)),
application.NewService(services.NewBatchService(core)),
application.NewService(scriptService),
application.NewService(services.NewVersionService(core)),
application.NewService(exportService),
application.NewService(services.NewModImportService(core)),
application.NewService(services.NewAnnotationService(core)),
application.NewService(services.NewRenderingService(core)),
application.NewService(services.NewRuleService(core)),
application.NewService(services.NewPreviewService(core)),
application.NewService(services.NewImageService(core, settingsService)),
application.NewService(services.NewFileGUIService(core)),
application.NewService(autosaveService),
application.NewService(cacheService),
application.NewService(settingsService),
application.NewService(fileSetService),
application.NewService(services.NewBookmarkService()),
application.NewService(services.NewArchiveDiffService(core)),
application.NewService(mcpService),
application.NewService(services.NewSkillService()),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
services.ObserveLogEvents(app)
updateService := services.NewUpdateService(app, appVersion)
app.RegisterService(application.NewService(updateService))
scriptWindowService := services.NewScriptWindowService(app)
app.RegisterService(application.NewService(scriptWindowService))
closeCoordinator := newCloseCoordinator(app, scriptWindowService)
updaterEnabled := configureUpdater(app)
menu := app.Menu.New()
app.Menu.SetApplicationMenu(menu)
appMenu := menu.AddSubmenu("应用")
if updaterEnabled {
appMenu.Add("检查更新").OnClick(func(*application.Context) {
go func() {
if err := updateService.CheckForUpdates(); err != nil {
log.Printf("检查更新失败: %v", err)
}
}()
})
}
appMenu.AddSeparator()
appMenu.Add("退出").SetAccelerator("CmdOrCtrl+q").OnClick(func(*application.Context) {
closeCoordinator.requestQuit()
})
menu.AddRole(application.EditMenu)
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "pvfine — PVF 归档编辑器",
Name: mainWindowName,
Width: 1440,
Height: 900,
EnableFileDrop: true,
Mac: application.MacWindow{
InvisibleTitleBarHeight: 50,
Backdrop: application.MacBackdropLiquidGlass,
TitleBar: application.MacTitleBarHiddenInset,
},
BackgroundColour: application.NewRGB(24, 26, 32),
URL: "/",
})
var openPathMu sync.Mutex
pendingOpenPath := ""
openPathReady := false
emitPVFOpenPath := func(path string) {
if !strings.HasSuffix(strings.ToLower(path), ".pvf") {
return
}
openPathMu.Lock()
if !openPathReady {
pendingOpenPath = path
openPathMu.Unlock()
return
}
openPathMu.Unlock()
app.Event.Emit("archive:open-path", path)
}
window.OnWindowEvent(events.Common.WindowRuntimeReady, func(*application.WindowEvent) {
openPathMu.Lock()
openPathReady = true
path := pendingOpenPath
pendingOpenPath = ""
openPathMu.Unlock()
if path != "" {
app.Event.Emit("archive:open-path", path)
}
})
window.RegisterHook(events.Common.WindowClosing, closeCoordinator.handlerFor(window))
window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
ctx := event.Context()
if ctx == nil {
return
}
for _, file := range ctx.DroppedFiles() {
if strings.HasSuffix(strings.ToLower(file), ".pvf") {
emitPVFOpenPath(file)
break
}
}
})
app.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(event *application.ApplicationEvent) {
if event == nil || event.Context() == nil {
return
}
path := event.Context().Filename()
emitPVFOpenPath(path)
})
if updaterEnabled {
startBackgroundUpdateCheck(updateService)
}
if err := app.Run(); err != nil {
log.Fatalf("[ERR] %v", err)
}
}
func configureUpdater(app *application.App) bool {
version := strings.TrimPrefix(appVersion, "v")
if githubRepository == "" || version == "" {
return false
}
provider, err := githubupdater.New(githubupdater.Config{
Repository: githubRepository,
ChecksumAsset: "SHA256SUMS",
})
if err != nil {
log.Printf("初始化更新源失败: %v", err)
return false
}
if err := app.Updater.Init(updater.Config{
CurrentVersion: version,
Providers: []updater.Provider{provider},
}); err != nil {
log.Printf("初始化更新器失败: %v", err)
return false
}
return true
}
func startBackgroundUpdateCheck(updateService *services.UpdateService) {
if githubRepository == "" || appVersion == "" || appVersion == "dev" || appVersion == "0.0.0" {
return
}
go func() {
// Give the first window time to enter the event loop before opening the
// update confirmation when a newer release is available.
time.Sleep(3 * time.Second)
if err := services.CheckForUpdatesInBackground(updateService); err != nil {
log.Printf("自动更新失败: %v", err)
}
}()
}