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

@@ -0,0 +1,515 @@
package hotupdate
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"sync"
"time"
"longheng.io/server/internal/platform/eventlog"
)
const (
EventLogIntentStream = "hotupdate.intent"
EventLogProgressStream = "hotupdate.progress"
)
var (
ErrControlLogRequired = errors.New("hot update eventlog control requires eventlog")
ErrControlConflict = errors.New("hot update control idempotency key conflicts with existing intent")
ErrInvalidControlEvent = errors.New("hot update control event payload is invalid")
ErrControlPollInterval = errors.New("hot update control poll interval is invalid")
ErrControlInstanceID = errors.New("hot update control instance id generation failed")
defControlPoll = 2 * time.Second
)
var controlRandRead = rand.Read
type EventLogControlOptions struct {
Log *eventlog.Log
PollInterval time.Duration
Now func() time.Time
}
type EventLogControl struct {
log *eventlog.Log
pollInterval time.Duration
now func() time.Time
ops sync.RWMutex
mu sync.Mutex
intents map[string]Intent
progress map[string][]Progress
watchers map[string]map[chan Intent]struct{}
done chan struct{}
closed bool
instanceID string
progressSequence uint64
}
type intentEnvelope struct {
Target string `json:"target"`
Intent Intent `json:"intent"`
}
type progressEnvelope struct {
Progress Progress `json:"progress"`
}
func NewEventLogControl(options EventLogControlOptions) (*EventLogControl, error) {
if options.Log == nil {
return nil, ErrControlLogRequired
}
pollInterval := options.PollInterval
if pollInterval == 0 {
pollInterval = defControlPoll
}
if pollInterval < 0 {
return nil, ErrControlPollInterval
}
now := options.Now
if now == nil {
now = func() time.Time { return time.Now().UTC() }
}
instanceID, err := newControlInstanceID()
if err != nil {
return nil, err
}
return &EventLogControl{
log: options.Log,
pollInterval: pollInterval,
now: now,
intents: make(map[string]Intent),
progress: make(map[string][]Progress),
watchers: make(map[string]map[chan Intent]struct{}),
done: make(chan struct{}),
instanceID: instanceID,
}, nil
}
func (c *EventLogControl) Publish(ctx context.Context, target string, intent Intent) error {
if err := contextErr(ctx); err != nil {
return err
}
now, err := c.ready()
if err != nil {
return err
}
target, err = normalizeTarget(target)
if err != nil {
return err
}
intent, err = normalizeIntent(intent)
if err != nil {
return err
}
release, err := c.beginWrite()
if err != nil {
return err
}
defer release()
// EventLog 控制面把 intent 持久化为事件,再通知 watcher;watcher 丢通知时可通过轮询补回最新 intent。
key := intentIdempotencyKey(target, intent)
existing, ok, err := c.log.GetByIdempotencyKey(ctx, key)
if err != nil {
return err
}
if ok {
existingIntent, err := decodeIntentEvent(existing)
if err != nil {
return err
}
if !reflect.DeepEqual(existingIntent, intent) {
return fmt.Errorf("%w: target=%s version=%s", ErrControlConflict, target, intent.Version)
}
c.storeIntentAndNotify(ctx, target, existingIntent)
return nil
}
payload, err := json.Marshal(intentEnvelope{Target: target, Intent: intent})
if err != nil {
return err
}
if _, err := c.log.Append(ctx, eventlog.Event{
Stream: EventLogIntentStream,
Type: string(intent.Action),
AggregateID: target,
IdempotencyKey: key,
Payload: payload,
Status: eventlog.StatusPublished,
PublishedAt: now().UTC(),
Headers: map[string]string{
"hotupdate_target": target,
"hotupdate_action": string(intent.Action),
"hotupdate_version": intent.Version,
},
}); err != nil {
if errors.Is(err, eventlog.ErrIdempotencyConflict) {
return fmt.Errorf("%w: target=%s version=%s", ErrControlConflict, target, intent.Version)
}
return err
}
c.storeIntentAndNotify(ctx, target, intent)
return nil
}
func (c *EventLogControl) Watch(ctx context.Context, target string) (<-chan Intent, error) {
if err := contextErr(ctx); err != nil {
return nil, err
}
if ctx == nil {
ctx = context.Background()
}
if _, err := c.ready(); err != nil {
return nil, err
}
target, err := normalizeTarget(target)
if err != nil {
return nil, err
}
// Watch 先注册 channel,再加载最新 intent,避免注册窗口内发布的新 intent 被漏掉。
ch := make(chan Intent, 1)
c.mu.Lock()
if c.closed {
c.mu.Unlock()
close(ch)
return ch, nil
}
if c.watchers[target] == nil {
c.watchers[target] = make(map[chan Intent]struct{})
}
c.watchers[target][ch] = struct{}{}
if intent, ok := c.intents[target]; ok {
ch <- intent
}
c.mu.Unlock()
go c.watchLoop(ctx, target, ch)
return ch, nil
}
func (c *EventLogControl) Report(ctx context.Context, progress Progress) error {
if err := contextErr(ctx); err != nil {
return err
}
now, err := c.ready()
if err != nil {
return err
}
progress, err = normalizeProgress(progress, now)
if err != nil {
return err
}
release, err := c.beginWrite()
if err != nil {
return err
}
defer release()
payload, err := json.Marshal(progressEnvelope{Progress: progress})
if err != nil {
return err
}
instanceID, sequence := c.nextProgressKeyParts()
if _, err := c.log.Append(ctx, eventlog.Event{
Stream: EventLogProgressStream,
Type: string(progress.Stage),
AggregateID: progress.Target,
IdempotencyKey: controlProgressKey(progress, instanceID, sequence),
Payload: payload,
Status: eventlog.StatusPublished,
PublishedAt: now().UTC(),
Headers: map[string]string{
"hotupdate_target": progress.Target,
"hotupdate_node": progress.NodeID,
"hotupdate_version": progress.Version,
"hotupdate_stage": string(progress.Stage),
},
}); err != nil {
return err
}
c.mu.Lock()
if !c.closed {
c.progress[progress.Target] = append(c.progress[progress.Target], progress)
}
c.mu.Unlock()
return nil
}
func (c *EventLogControl) beginWrite() (func(), error) {
c.ops.RLock()
c.mu.Lock()
if c.closed {
c.mu.Unlock()
c.ops.RUnlock()
return nil, ErrControlClosed
}
c.mu.Unlock()
return c.ops.RUnlock, nil
}
func (c *EventLogControl) ready() (func() time.Time, error) {
if c == nil || c.log == nil {
return nil, ErrControlLogRequired
}
c.mu.Lock()
defer c.mu.Unlock()
if c.pollInterval < 0 {
return nil, ErrControlPollInterval
}
if c.pollInterval == 0 {
c.pollInterval = defControlPoll
}
if c.now == nil {
c.now = func() time.Time { return time.Now().UTC() }
}
if c.intents == nil {
c.intents = make(map[string]Intent)
}
if c.progress == nil {
c.progress = make(map[string][]Progress)
}
if c.watchers == nil {
c.watchers = make(map[string]map[chan Intent]struct{})
}
if c.done == nil {
c.done = make(chan struct{})
}
if c.instanceID == "" {
instanceID, err := newControlInstanceID()
if err != nil {
return nil, err
}
c.instanceID = instanceID
}
return c.now, nil
}
func (c *EventLogControl) Snapshot() ControlSnapshot {
if c == nil {
return ControlSnapshot{}
}
c.mu.Lock()
intents := make(map[string]Intent, len(c.intents))
for target, intent := range c.intents {
intents[target] = intent
}
progress := make(map[string][]Progress, len(c.progress))
for target, items := range c.progress {
progress[target] = append([]Progress(nil), items...)
}
watchers := make(map[string]int, len(c.watchers))
for target, list := range c.watchers {
watchers[target] = len(list)
}
snapshot := ControlSnapshot{Intents: intents, Progress: progress, Watchers: watchers, Closed: c.closed}
c.mu.Unlock()
if len(snapshot.Intents) == 0 {
snapshot.Intents = nil
}
if len(snapshot.Progress) == 0 {
snapshot.Progress = nil
}
if len(snapshot.Watchers) == 0 {
snapshot.Watchers = nil
}
return snapshot
}
func (c *EventLogControl) Close() error {
if c == nil {
return nil
}
c.ops.Lock()
defer c.ops.Unlock()
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return nil
}
c.closed = true
// Close 关闭所有 watcher channel;EventLog 里的 intent/progress 仍保留用于审计。
if c.done == nil {
c.done = make(chan struct{})
}
close(c.done)
for target, watchers := range c.watchers {
for ch := range watchers {
close(ch)
}
delete(c.watchers, target)
}
c.mu.Unlock()
return nil
}
func (c *EventLogControl) watchLoop(ctx context.Context, target string, ch chan Intent) {
defer c.removeWatcher(target, ch)
// watchLoop 定期从 EventLog 读取最新 intent,作为内存通知丢失后的补偿。
if c.isDone() {
return
}
_ = c.reloadLatestIntent(ctx, target)
if c.pollInterval <= 0 {
select {
case <-ctx.Done():
case <-c.done:
}
return
}
ticker := time.NewTicker(c.pollInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-c.done:
return
case <-ticker.C:
_ = c.reloadLatestIntent(ctx, target)
}
}
}
func (c *EventLogControl) isDone() bool {
select {
case <-c.done:
return true
default:
return false
}
}
func (c *EventLogControl) removeWatcher(target string, ch chan Intent) {
c.mu.Lock()
if watchers := c.watchers[target]; watchers != nil {
if _, ok := watchers[ch]; ok {
delete(watchers, ch)
close(ch)
}
if len(watchers) == 0 {
delete(c.watchers, target)
}
}
c.mu.Unlock()
}
func (c *EventLogControl) reloadLatestIntent(ctx context.Context, target string) error {
events, err := c.log.ListByStreamAggregate(ctx, EventLogIntentStream, target, 1)
if err != nil {
return err
}
if len(events) == 0 {
return nil
}
intent, err := decodeIntentEvent(events[0])
if err != nil {
return err
}
c.storeIntentAndNotify(ctx, target, intent)
return nil
}
func (c *EventLogControl) storeIntentAndNotify(_ context.Context, target string, intent Intent) {
c.mu.Lock()
// 内存 latest 是读优化,权威记录仍是 EventLog;通知失败不会删除持久化 intent。
if c.closed {
c.mu.Unlock()
return
}
if existing, ok := c.intents[target]; ok && reflect.DeepEqual(existing, intent) {
c.mu.Unlock()
return
}
c.intents[target] = intent
for ch := range c.watchers[target] {
sendLatestIntent(ch, intent)
}
c.mu.Unlock()
}
func decodeIntentEvent(event eventlog.Event) (Intent, error) {
var envelope intentEnvelope
if len(event.Payload) == 0 {
return Intent{}, ErrInvalidControlEvent
}
if err := json.Unmarshal(event.Payload, &envelope); err != nil {
return Intent{}, fmt.Errorf("%w: %w", ErrInvalidControlEvent, err)
}
intent, err := normalizeIntent(envelope.Intent)
if err != nil {
return Intent{}, err
}
return intent, nil
}
func intentIdempotencyKey(target string, intent Intent) string {
if intent.Sequence > 0 {
return fmt.Sprintf("hotupdate:intent:v1:%s:%d", encodeEventKeyPart(target), intent.Sequence)
}
return fmt.Sprintf("hotupdate:intent:v1:%s:%s:%s", encodeEventKeyPart(target), encodeEventKeyPart(string(intent.Action)), encodeEventKeyPart(intent.Version))
}
func progressIdemKey(progress Progress) string {
parts := []string{
"hotupdate:progress",
"v1",
encodeEventKeyPart(progress.Target),
encodeEventKeyPart(progress.NodeID),
encodeEventKeyPart(progress.Version),
encodeEventKeyPart(string(progress.Stage)),
progressFingerprint(progress),
}
return strings.Join(parts, ":")
}
func controlProgressKey(progress Progress, instanceID string, sequence uint64) string {
parts := []string{
progressIdemKey(progress),
encodeEventKeyPart(instanceID),
fmt.Sprint(sequence),
}
return strings.Join(parts, ":")
}
func (c *EventLogControl) nextProgressKeyParts() (string, uint64) {
c.mu.Lock()
defer c.mu.Unlock()
c.progressSequence++
return c.instanceID, c.progressSequence
}
func newControlInstanceID() (string, error) {
var raw [8]byte
if _, err := controlRandRead(raw[:]); err != nil {
return "", fmt.Errorf("%w: %w", ErrControlInstanceID, err)
}
return hex.EncodeToString(raw[:]), nil
}
func encodeEventKeyPart(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "_"
}
return base64.RawURLEncoding.EncodeToString([]byte(value))
}
func progressFingerprint(progress Progress) string {
payload := strings.Join([]string{
strings.TrimSpace(progress.Target),
strings.TrimSpace(progress.NodeID),
strings.TrimSpace(progress.Version),
strings.TrimSpace(string(progress.Stage)),
strings.TrimSpace(progress.Status),
strings.TrimSpace(progress.Detail),
progress.UpdatedAt.UTC().Format(time.RFC3339Nano),
}, "\x00")
sum := sha256.Sum256([]byte(payload))
return hex.EncodeToString(sum[:8])
}