DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
+322
@@ -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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
Reference in new issue
Block a user