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

426 lines
10 KiB
Go

package hotupdate
import (
"context"
"errors"
"log/slog"
"strings"
"sync"
"time"
)
type ManagerOptions struct {
Target string
NodeID string
Workspace string
ApplyTimeout time.Duration
ReportTimeout time.Duration
Control Control
Source Source
Applier Applier
Logger *slog.Logger
Now func() time.Time
}
type Manager struct {
target string
nodeID string
workspace string
applyTimeout time.Duration
reportTimeout time.Duration
control Control
source Source
applier Applier
logger *slog.Logger
now func() time.Time
mu sync.RWMutex
started bool
cancel context.CancelFunc
done chan struct{}
snapshot Snapshot
lifecycleCtx context.Context
lifecycleCancel context.CancelFunc
autoRestoreTimer *time.Timer
}
type patchWindowApplier interface {
PatchLiveDuration() time.Duration
}
func NewManager(options ManagerOptions) (*Manager, error) {
target, err := normalizeTarget(options.Target)
if err != nil {
return nil, err
}
if options.Control == nil {
options.Control = NewMemoryControl()
}
if options.Source == nil {
options.Source = LocalSource{}
}
if options.Applier == nil {
return nil, ErrApplyRequired
}
now := options.Now
if now == nil {
now = func() time.Time { return time.Now().UTC() }
}
nodeID := strings.TrimSpace(options.NodeID)
if nodeID == "" {
nodeID = "local"
}
reportTimeout := options.ReportTimeout
if reportTimeout <= 0 {
reportTimeout = 30 * time.Second
}
lifecycleCtx, lifecycleCancel := context.WithCancel(context.Background())
return &Manager{
target: target,
nodeID: nodeID,
workspace: strings.TrimSpace(options.Workspace),
applyTimeout: options.ApplyTimeout,
reportTimeout: reportTimeout,
control: options.Control,
source: options.Source,
applier: options.Applier,
logger: options.Logger,
now: now,
lifecycleCtx: lifecycleCtx,
lifecycleCancel: lifecycleCancel,
snapshot: Snapshot{
Target: target,
NodeID: nodeID,
},
}, nil
}
func (m *Manager) Name() string {
if m == nil {
return "hot-update"
}
return "hot-update-" + m.target
}
func (m *Manager) Start(ctx context.Context) error {
if m == nil {
return nil
}
if ctx == nil {
ctx = context.Background()
}
m.mu.Lock()
if m.started {
m.mu.Unlock()
return nil
}
m.ensureLifeLocked()
runCtx, cancel := context.WithCancel(ctx)
done := make(chan struct{})
m.started = true
m.cancel = cancel
m.done = done
m.snapshot.Started = true
m.mu.Unlock()
// Manager 只监听控制面 intent;真正下载、校验、替换由 source/applier 执行,便于替换成 etcd/SQL 控制面。
go m.loop(runCtx, done)
return nil
}
func (m *Manager) Stop(ctx context.Context) error {
if m == nil {
return nil
}
if ctx == nil {
ctx = context.Background()
}
m.mu.Lock()
cancel := m.cancel
done := m.done
autoRestoreTimer := m.autoRestoreTimer
lifecycleCancel := m.lifecycleCancel
m.cancel = nil
m.done = nil
m.autoRestoreTimer = nil
m.lifecycleCtx = nil
m.lifecycleCancel = nil
m.started = false
m.snapshot.Started = false
m.snapshot.AutoRestoreAt = time.Time{}
m.mu.Unlock()
if lifecycleCancel != nil {
lifecycleCancel()
}
if autoRestoreTimer != nil {
autoRestoreTimer.Stop()
}
// Stop 取消 watcher 和自动恢复计时器,并等待 loop 退出,避免停机后继续应用热更。
if cancel != nil {
cancel()
}
if done == nil {
return nil
}
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (m *Manager) Process(ctx context.Context, intent Intent) (ApplyResult, error) {
if m == nil {
return ApplyResult{}, errors.New("hot update manager is nil")
}
if ctx == nil {
ctx = context.Background()
}
intent, err := normalizeIntent(intent)
if err != nil {
return ApplyResult{}, err
}
// Process 是单个 intent 的状态机入口:restore 走回滚,其余动作按下载、校验、应用、报告推进。
m.setIntent(intent)
m.report(ctx, intent, StageAccepted, "ok", "")
if intent.Action == ActionRestore {
return m.restore(ctx, intent)
}
m.report(ctx, intent, StageDownloading, "ok", "")
pkg, err := m.source.Fetch(ctx, intent, m.workspace)
if err != nil {
m.fail(ctx, intent, err)
return ApplyResult{}, err
}
m.setPackage(pkg)
if wait := time.Until(intent.AvailableAt); !intent.AvailableAt.IsZero() && wait > 0 {
m.report(ctx, intent, StageScheduled, "ok", intent.AvailableAt.Format(time.RFC3339))
timer := time.NewTimer(wait)
select {
case <-timer.C:
case <-ctx.Done():
timer.Stop()
m.fail(ctx, intent, ctx.Err())
return ApplyResult{}, ctx.Err()
}
}
m.report(ctx, intent, StageApplying, "ok", "")
applyCtx := ctx
cancel := func() {}
if m.applyTimeout > 0 {
applyCtx, cancel = context.WithTimeout(ctx, m.applyTimeout)
}
defer cancel()
result, err := m.applier.Apply(applyCtx, pkg)
if err != nil {
m.fail(ctx, intent, err)
return ApplyResult{}, err
}
m.mu.Lock()
m.snapshot.LastApplied = m.now()
m.mu.Unlock()
m.report(ctx, intent, StageApplied, "ok", "")
m.scheduleAutoRestore(intent)
return result, nil
}
func (m *Manager) restore(ctx context.Context, intent Intent) (ApplyResult, error) {
restorer, ok := m.applier.(Restorer)
if !ok {
err := ErrRestoreRequired
m.fail(ctx, intent, err)
return ApplyResult{}, err
}
m.cancelAutoRestore()
// restore 会取消自动恢复计时器,避免手动回滚和定时回滚并发执行。
m.report(ctx, intent, StageRestoring, "ok", "")
applyCtx := ctx
cancel := func() {}
if m.applyTimeout > 0 {
applyCtx, cancel = context.WithTimeout(ctx, m.applyTimeout)
}
defer cancel()
result, err := restorer.Restore(applyCtx, intent)
if err != nil {
m.fail(ctx, intent, err)
return ApplyResult{}, err
}
m.mu.Lock()
m.snapshot.LastApplied = m.now()
m.mu.Unlock()
m.report(ctx, intent, StageRestored, "ok", "")
return result, nil
}
func (m *Manager) NativePatchEnabled() bool {
if m == nil {
return false
}
_, ok := m.applier.(NativePatchApplier)
return ok
}
func (m *Manager) Snapshot() Snapshot {
if m == nil {
return Snapshot{}
}
m.mu.RLock()
snapshot := m.snapshot
m.mu.RUnlock()
return snapshot
}
func (m *Manager) Control() Control {
if m == nil {
return nil
}
return m.control
}
func (m *Manager) scheduleAutoRestore(intent Intent) {
if m == nil {
return
}
duration := time.Duration(0)
if applier, ok := m.applier.(patchWindowApplier); ok {
duration = applier.PatchLiveDuration()
}
if duration <= 0 {
m.cancelAutoRestore()
return
}
// 原生补丁必须有存活窗口,到期自动 restore,防止紧急 patch 长期遗留在线上。
restoreAt := m.now().UTC().Add(duration)
restoreIntent := Intent{
Action: ActionRestore,
Version: intent.Version,
Reason: "native patch auto restore after live window",
RequestedBy: "system",
}
m.mu.Lock()
m.ensureLifeLocked()
lifecycleCtx := m.lifecycleCtx
if m.autoRestoreTimer != nil {
m.autoRestoreTimer.Stop()
}
m.snapshot.AutoRestoreAt = restoreAt
m.autoRestoreTimer = time.AfterFunc(duration, func() {
if lifecycleCtx != nil && lifecycleCtx.Err() != nil {
return
}
if _, err := m.restore(lifecycleCtx, restoreIntent); err != nil && m.logger != nil && !errors.Is(err, context.Canceled) {
m.logger.Warn("native patch auto restore failed", "target", m.target, "version", restoreIntent.Version, "error", err)
}
})
m.mu.Unlock()
}
func (m *Manager) cancelAutoRestore() {
if m == nil {
return
}
m.mu.Lock()
if m.autoRestoreTimer != nil {
m.autoRestoreTimer.Stop()
m.autoRestoreTimer = nil
}
m.snapshot.AutoRestoreAt = time.Time{}
m.mu.Unlock()
}
func (m *Manager) ensureLifeLocked() {
if m.lifecycleCtx != nil && m.lifecycleCtx.Err() == nil && m.lifecycleCancel != nil {
return
}
m.lifecycleCtx, m.lifecycleCancel = context.WithCancel(context.Background())
}
func (m *Manager) loop(ctx context.Context, done chan struct{}) {
defer close(done)
// Watch 返回的是控制面变更流;同一版本重复 intent 由 Process/Progress 负责幂等记录。
changes, err := m.control.Watch(ctx, m.target)
if err != nil {
if m.logger != nil {
m.logger.Warn("hot update watch failed", "target", m.target, "error", err)
}
return
}
for {
select {
case <-ctx.Done():
return
case intent, ok := <-changes:
if !ok {
return
}
if _, err := m.Process(ctx, intent); err != nil && m.logger != nil {
m.logger.Warn("hot update apply failed", "target", m.target, "version", intent.Version, "error", err)
}
}
}
}
func (m *Manager) report(ctx context.Context, intent Intent, stage Stage, status, detail string) {
// progress 写失败只记录日志,不阻断 apply;因此 release gate 必须检查报告完整性。
progress := Progress{
Target: m.target,
NodeID: m.nodeID,
Version: intent.Version,
Stage: stage,
Status: strings.TrimSpace(status),
Detail: strings.TrimSpace(detail),
UpdatedAt: m.now(),
}
m.mu.Lock()
m.snapshot.LastProgress = progress
m.mu.Unlock()
reportCtx, cancel := m.reportContext(ctx)
defer cancel()
if err := m.control.Report(reportCtx, progress); err != nil {
m.mu.Lock()
m.snapshot.Failures++
m.mu.Unlock()
if m.logger != nil && !errors.Is(err, context.Canceled) {
m.logger.Warn("hot update progress report failed", "target", m.target, "version", intent.Version, "stage", stage, "error", err)
}
}
}
func (m *Manager) reportContext(ctx context.Context) (context.Context, context.CancelFunc) {
if ctx == nil {
ctx = context.Background()
}
if _, ok := ctx.Deadline(); ok {
return ctx, func() {}
}
timeout := 30 * time.Second
if m != nil && m.reportTimeout > 0 {
timeout = m.reportTimeout
}
return context.WithTimeout(ctx, timeout)
}
func (m *Manager) fail(ctx context.Context, intent Intent, err error) {
detail := ""
if err != nil {
detail = err.Error()
}
m.mu.Lock()
m.snapshot.Failures++
m.mu.Unlock()
m.report(ctx, intent, StageFailed, "failed", detail)
}
func (m *Manager) setIntent(intent Intent) {
m.mu.Lock()
m.snapshot.LastIntent = intent
m.mu.Unlock()
}
func (m *Manager) setPackage(pkg Package) {
m.mu.Lock()
m.snapshot.LastPackage = pkg
m.mu.Unlock()
}