Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

274 lines
8.5 KiB
Go

package app
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"time"
"longheng.io/server/internal/platform/config"
"longheng.io/server/internal/platform/datatable"
"longheng.io/server/internal/platform/discovery"
"longheng.io/server/internal/platform/reload"
rpckit "longheng.io/server/internal/platform/rpc"
)
func (a *Application) applyRuntimeReload(ctx context.Context, trigger reload.Trigger, reason string) (reload.Result, error) {
result := reload.Result{
Trigger: string(trigger),
Reason: reason,
At: time.Now().UTC(),
}
if a == nil {
return result, errors.New("application is nil")
}
if ctx == nil {
ctx = context.Background()
}
oldCfg := a.configSnapshot()
newCfg, err := config.Load(a.configPath, oldCfg.Service.Name)
if err != nil {
result.Error = err.Error()
return result, err
}
result.Changed = !reflect.DeepEqual(oldCfg, newCfg)
if trigger == reload.TriggerWatch && !result.Changed {
result.Skipped = append(result.Skipped, "config unchanged")
return result, nil
}
effective := oldCfg
var applyErr error
apply := func(name string, changed bool, fn func() error) {
if applyErr != nil || (!changed && trigger != reload.TriggerManual) {
return
}
if err := fn(); err != nil {
applyErr = fmt.Errorf("reload %s: %w", name, err)
result.Error = applyErr.Error()
return
}
result.Applied = append(result.Applied, name)
}
apply("rate_limit", !reflect.DeepEqual(oldCfg.RateLimit, newCfg.RateLimit), func() error {
cfg, err := rateLimitConfig(newCfg)
if err != nil {
return err
}
a.rateLimiter.Configure(cfg)
effective.RateLimit = newCfg.RateLimit
return nil
})
apply("discovery", !reflect.DeepEqual(oldCfg.Discovery, newCfg.Discovery), func() error {
a.discovery.Configure(
time.Duration(newCfg.Discovery.CacheTTLSeconds)*time.Second,
time.Duration(newCfg.Discovery.FailureTTLSeconds)*time.Second,
newCfg.Discovery.FailureThreshold,
!newCfg.Discovery.AllowMaintaining,
discovery.Strategy(newCfg.Discovery.Strategy),
)
effective.Discovery = newCfg.Discovery
return nil
})
apply("rpc", !reflect.DeepEqual(oldCfg.RPC, newCfg.RPC), func() error {
a.rpc.SetOptionsExact(rpckit.Options{
CallTimeout: time.Duration(newCfg.RPC.CallTimeoutSeconds) * time.Second,
MaxPending: newCfg.RPC.MaxPending,
MaxPayloadBytes: newCfg.RPC.MaxPayloadBytes,
})
effective.RPC = newCfg.RPC
return nil
})
apply("topology", !reflect.DeepEqual(oldCfg.Topology, newCfg.Topology), func() error {
a.topology.Configure(topologyConfig(newCfg))
if newCfg.Topology.Enabled {
if err := a.topology.Refresh(ctx); err != nil {
return err
}
}
effective.Topology = newCfg.Topology
return nil
})
apply("tracing", !reflect.DeepEqual(oldCfg.Tracing, newCfg.Tracing), func() error {
a.tracer.Configure(newCfg.Tracing.Enabled || oldCfg.OTel.Enabled, newCfg.Tracing.MaxSpans)
effective.Tracing = newCfg.Tracing
return nil
})
apply("data_tables", !reflect.DeepEqual(oldCfg.DataTables, newCfg.DataTables), func() error {
if newCfg.DataTables.Enabled {
options := datatable.ReloadOptions{}
if a.dataTableViews != nil {
options.Validators = append(options.Validators, a.dataTableViews)
}
loadResult, err := a.dataTables.ReloadDir(ctx, newCfg.DataTables.Directory, newCfg.DataTables.Version, options)
if err != nil {
return err
}
effective.DataTables = newCfg.DataTables
if a.dataTableViews != nil {
viewResult, err := a.dataTableViews.Refresh(context.WithoutCancel(ctx), a.dataTables, loadResult)
if err != nil {
return err
}
addReloadDetail(&result, "data_table_views", viewResult)
}
addReloadDetail(&result, "data_tables", loadResult)
} else {
a.dataTables.Configure(newCfg.DataTables.Directory, newCfg.DataTables.Version)
a.dataTables.Clear()
if a.dataTableViews != nil {
addReloadDetail(&result, "data_table_views", a.dataTableViews.Clear())
}
addReloadDetail(&result, "data_tables", map[string]any{
"enabled": false,
"manifest": a.dataTables.Manifest(),
})
}
effective.DataTables = newCfg.DataTables
return nil
})
apply("i18n", !reflect.DeepEqual(oldCfg.I18N, newCfg.I18N), func() error {
if a.i18nCatalog == nil {
effective.I18N = newCfg.I18N
return nil
}
if newCfg.I18N.Enabled {
if err := a.i18nCatalog.LoadDirWithConfig(ctx, newCfg.I18N.Directory, newCfg.I18N.DefaultLanguage, newCfg.I18N.Version); err != nil {
return err
}
if !a.i18nCatalog.HasLanguage(newCfg.I18N.DefaultLanguage) {
return fmt.Errorf("default language %q is not loaded", newCfg.I18N.DefaultLanguage)
}
a.i18nCatalog.Configure(newCfg.I18N.DefaultLanguage, newCfg.I18N.Version)
} else {
a.i18nCatalog.Configure(newCfg.I18N.DefaultLanguage, newCfg.I18N.Version)
a.i18nCatalog.Clear()
}
effective.I18N = newCfg.I18N
return nil
})
apply("reload", !reflect.DeepEqual(oldCfg.Reload, newCfg.Reload), func() error {
if a.reloader != nil {
a.reloader.Configure(reloadConfig(newCfg))
}
effective.Reload = newCfg.Reload
return nil
})
apply("admin_security", adminSecurityChanged(oldCfg.Admin, newCfg.Admin), func() error {
effective.Admin.Token = newCfg.Admin.Token
effective.Admin.Tokens = cloneAdminTokens(newCfg.Admin.Tokens)
effective.Admin.RBACEnabled = newCfg.Admin.RBACEnabled
effective.Admin.DangerousConfirm = newCfg.Admin.DangerousConfirm
return nil
})
apply("metrics_admin_policy", oldCfg.Metrics.RequireAdminToken != newCfg.Metrics.RequireAdminToken, func() error {
effective.Metrics.RequireAdminToken = newCfg.Metrics.RequireAdminToken
return nil
})
result.RestartRequired = restartReqSections(oldCfg, newCfg)
if applyErr != nil {
a.setConfig(effective)
return result, applyErr
}
a.setConfig(effective)
if len(result.Applied) == 0 && len(result.RestartRequired) == 0 {
result.Skipped = append(result.Skipped, "no runtime-applicable changes")
}
return result, nil
}
func adminSecurityChanged(oldAdmin, newAdmin config.AdminSection) bool {
return oldAdmin.Token != newAdmin.Token ||
oldAdmin.RBACEnabled != newAdmin.RBACEnabled ||
oldAdmin.DangerousConfirm != newAdmin.DangerousConfirm ||
!reflect.DeepEqual(oldAdmin.Tokens, newAdmin.Tokens)
}
func cloneAdminTokens(tokens []config.AdminTokenSection) []config.AdminTokenSection {
if len(tokens) == 0 {
return nil
}
out := make([]config.AdminTokenSection, len(tokens))
copy(out, tokens)
for idx := range out {
if len(out[idx].Scopes) > 0 {
out[idx].Scopes = append([]string(nil), out[idx].Scopes...)
}
}
return out
}
func restartReqSections(oldCfg, newCfg config.ServiceConfig) []string {
var out []string
if !reflect.DeepEqual(oldCfg.Service, newCfg.Service) {
out = append(out, "service")
}
if !reflect.DeepEqual(oldCfg.Cluster, newCfg.Cluster) {
out = append(out, "cluster")
}
if oldCfg.Admin.Listen != newCfg.Admin.Listen ||
oldCfg.Admin.ReadTimeoutSeconds != newCfg.Admin.ReadTimeoutSeconds ||
oldCfg.Admin.ReadHeaderTimeoutSeconds != newCfg.Admin.ReadHeaderTimeoutSeconds ||
oldCfg.Admin.WriteTimeoutSeconds != newCfg.Admin.WriteTimeoutSeconds ||
oldCfg.Admin.IdleTimeoutSeconds != newCfg.Admin.IdleTimeoutSeconds {
out = append(out, "admin_http")
}
if oldCfg.Metrics.PrometheusEnabled != newCfg.Metrics.PrometheusEnabled {
out = append(out, "metrics_endpoint")
}
if !reflect.DeepEqual(oldCfg.OTel, newCfg.OTel) {
out = append(out, "otel")
}
if !reflect.DeepEqual(oldCfg.Debug, newCfg.Debug) {
out = append(out, "debug_routes")
}
if !reflect.DeepEqual(oldCfg.PVF, newCfg.PVF) {
out = append(out, "pvf")
}
if !reflect.DeepEqual(oldCfg.Audit, newCfg.Audit) {
out = append(out, "audit_sink")
}
if !reflect.DeepEqual(oldCfg.Registry, newCfg.Registry) {
out = append(out, "registry")
}
if !reflect.DeepEqual(oldCfg.Bus, newCfg.Bus) {
out = append(out, "bus")
}
if !reflect.DeepEqual(oldCfg.Worker, newCfg.Worker) {
out = append(out, "worker_pool")
}
if !reflect.DeepEqual(oldCfg.ProfileStore, newCfg.ProfileStore) {
out = append(out, "profile_store")
}
if !reflect.DeepEqual(oldCfg.Gateway, newCfg.Gateway) {
out = append(out, "gateway")
}
return out
}
func addReloadDetail(result *reload.Result, key string, value any) {
if result == nil {
return
}
key = strings.TrimSpace(key)
if key == "" {
return
}
if result.Details == nil {
result.Details = make(map[string]any)
}
result.Details[key] = value
}
func reloadConfig(cfg config.ServiceConfig) reload.Config {
return reload.Config{
Enabled: cfg.Reload.Enabled,
PollInterval: time.Duration(cfg.Reload.PollIntervalSeconds) * time.Second,
ApplyTimeout: time.Duration(cfg.Reload.ApplyTimeoutSeconds) * time.Second,
}
}