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

532 lines
13 KiB
Go

package player
import (
"context"
"errors"
"sort"
"strings"
"sync"
"time"
"longheng.io/server/internal/platform/db"
)
const (
sumQueueMax = 4096
sumFlushTimeout = 3 * time.Second
sumRetryInterval = 200 * time.Millisecond
)
var (
errSumStoreNil = errors.New("player summary store is nil")
errSumClosed = errors.New("player summary queue is closed")
errSumFull = errors.New("player summary queue is full")
)
// AsyncSummaryStore 把玩家摘要写入合并到后台,避免远端读模型拖慢玩家热路径。
type AsyncSummaryStore struct {
base SummaryStore
max int
timeout time.Duration
mu sync.RWMutex
pending map[string]sumPending
roles map[string]string
seq uint64
closed bool
wake chan struct{}
stop chan struct{}
done chan struct{}
once sync.Once
}
// NewAsyncSummaryStore 创建异步玩家摘要读模型存储。
func NewAsyncSummaryStore(base SummaryStore) *AsyncSummaryStore {
store := &AsyncSummaryStore{
base: base,
max: sumQueueMax,
timeout: sumFlushTimeout,
pending: make(map[string]sumPending),
roles: make(map[string]string),
wake: make(chan struct{}, 1),
stop: make(chan struct{}),
done: make(chan struct{}),
}
go store.run()
return store
}
// SavePlayerSummary 合并玩家摘要并唤醒后台刷新。
func (s *AsyncSummaryStore) SavePlayerSummary(ctx context.Context, summary PlayerSummary) error {
if s == nil || s.base == nil {
return errSumStoreNil
}
ctx = summaryCtx(ctx)
if err := ctx.Err(); err != nil {
return err
}
summary = normPlayerSummaryID(summary)
if summary.AccountID == "" {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return errSumClosed
}
if _, ok := s.pending[summary.AccountID]; !ok && len(s.pending) >= s.max {
return errSumFull
}
s.putLocked(summary)
s.wakeLocked()
return nil
}
// GetPlayerSummary 优先读取尚未落远端的最新摘要。
func (s *AsyncSummaryStore) GetPlayerSummary(ctx context.Context, accountID string) (PlayerSummary, bool, error) {
if s == nil || s.base == nil {
return PlayerSummary{}, false, errSumStoreNil
}
ctx = summaryCtx(ctx)
if err := ctx.Err(); err != nil {
return PlayerSummary{}, false, err
}
accountID = strings.TrimSpace(accountID)
if accountID == "" {
return PlayerSummary{}, false, nil
}
if summary, ok := s.pendingByAcct(accountID); ok {
return summary, true, nil
}
return s.base.GetPlayerSummary(ctx, accountID)
}
// GetPlayerSummaryByRoleID 优先读取尚未落远端的角色索引。
func (s *AsyncSummaryStore) GetPlayerSummaryByRoleID(ctx context.Context, roleID string) (PlayerSummary, bool, error) {
if s == nil || s.base == nil {
return PlayerSummary{}, false, errSumStoreNil
}
ctx = summaryCtx(ctx)
if err := ctx.Err(); err != nil {
return PlayerSummary{}, false, err
}
roleID = strings.TrimSpace(roleID)
if roleID == "" {
return PlayerSummary{}, false, nil
}
if summary, ok := s.pendingByRole(roleID); ok {
return summary, true, nil
}
summary, ok, err := s.base.GetPlayerSummaryByRoleID(ctx, roleID)
if err != nil || !ok {
return summary, ok, err
}
if pending, pendingOK := s.pendingByAcct(summary.AccountID); pendingOK && pending.RoleID != roleID {
return PlayerSummary{}, false, nil
}
return summary, true, nil
}
// ListPlayerSummariesByAccountIDs 批量读取时合并 pending 摘要。
func (s *AsyncSummaryStore) ListPlayerSummariesByAccountIDs(ctx context.Context, accountIDs []string) ([]PlayerSummary, error) {
if s == nil || s.base == nil {
return nil, errSumStoreNil
}
ctx = summaryCtx(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
base, err := s.base.ListPlayerSummariesByAccountIDs(ctx, accountIDs)
if err != nil {
return nil, err
}
ids := normSummaryIDs(accountIDs)
byAcct := make(map[string]PlayerSummary, len(base)+len(ids))
for _, summary := range base {
summary = normPlayerSummaryID(summary)
if summary.AccountID != "" {
byAcct[summary.AccountID] = clonePlayerSummary(summary)
}
}
for _, summary := range s.pendingByAccts(ids) {
byAcct[summary.AccountID] = summary
}
out := make([]PlayerSummary, 0, len(ids))
for _, accountID := range ids {
if summary, ok := byAcct[accountID]; ok {
out = append(out, clonePlayerSummary(summary))
}
}
sortPlayerSummaries(out)
return out, nil
}
// ListByRoleIDs 批量读取角色索引时合并 pending 摘要并屏蔽旧索引。
func (s *AsyncSummaryStore) ListByRoleIDs(ctx context.Context, roleIDs []string) ([]PlayerSummary, error) {
if s == nil || s.base == nil {
return nil, errSumStoreNil
}
ctx = summaryCtx(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
base, err := s.base.ListByRoleIDs(ctx, roleIDs)
if err != nil {
return nil, err
}
ids := normSummaryIDs(roleIDs)
byRole := make(map[string]PlayerSummary, len(base)+len(ids))
for _, summary := range base {
summary = normPlayerSummaryID(summary)
if summary.RoleID != "" {
byRole[summary.RoleID] = clonePlayerSummary(summary)
}
}
pending := s.pendingAll()
pendingByAcct := make(map[string]PlayerSummary, len(pending))
for _, summary := range pending {
pendingByAcct[summary.AccountID] = summary
}
for roleID, summary := range byRole {
if pending, ok := pendingByAcct[summary.AccountID]; ok && pending.RoleID != roleID {
delete(byRole, roleID)
}
}
for _, pending := range pending {
if pending.RoleID != "" {
byRole[pending.RoleID] = pending
}
}
out := make([]PlayerSummary, 0, len(ids))
for _, roleID := range ids {
if summary, ok := byRole[roleID]; ok {
out = append(out, clonePlayerSummary(summary))
}
}
sortPlayerSummaries(out)
return out, nil
}
// SearchPlayerSummaries 搜索时合并 pending 摘要,保证读到最新公开状态。
func (s *AsyncSummaryStore) SearchPlayerSummaries(ctx context.Context, query PlayerSummaryQuery) ([]PlayerSummary, error) {
if s == nil || s.base == nil {
return nil, errSumStoreNil
}
if len(query.AccountIDs) > 0 {
summaries, err := s.ListPlayerSummariesByAccountIDs(ctx, query.AccountIDs)
return filterSummaries(summaries, query), err
}
if len(query.RoleIDs) > 0 {
summaries, err := s.ListByRoleIDs(ctx, query.RoleIDs)
return filterSummaries(summaries, query), err
}
ctx = summaryCtx(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
base, err := s.base.SearchPlayerSummaries(ctx, query)
if err != nil {
return nil, err
}
byAcct := make(map[string]PlayerSummary, len(base))
for _, summary := range base {
summary = normPlayerSummaryID(summary)
if summary.AccountID != "" {
byAcct[summary.AccountID] = clonePlayerSummary(summary)
}
}
for _, pending := range s.pendingAll() {
if summaryMatches(pending, query) {
byAcct[pending.AccountID] = pending
continue
}
delete(byAcct, pending.AccountID)
}
out := make([]PlayerSummary, 0, len(byAcct))
for _, summary := range byAcct {
out = append(out, clonePlayerSummary(summary))
}
sortPlayerSummaries(out)
if query.Limit > 0 && len(out) > query.Limit {
out = out[:query.Limit]
}
return out, nil
}
// Flush 同步刷新所有待写摘要。
func (s *AsyncSummaryStore) Flush(ctx context.Context) error {
if s == nil || s.base == nil {
return errSumStoreNil
}
ctx = summaryCtx(ctx)
for {
batch := s.takePending()
if len(batch) == 0 {
return nil
}
for _, summary := range batch {
if err := ctx.Err(); err != nil {
return err
}
if err := s.base.SavePlayerSummary(ctx, summary.summary); err != nil {
return err
}
s.removeSaved(summary)
}
}
}
// Close 停止后台刷新,排空 pending 后关闭底层存储。
func (s *AsyncSummaryStore) Close(ctx context.Context) error {
if s == nil {
return nil
}
closeCtx, cancel := s.closeContext(ctx)
defer cancel()
s.once.Do(func() {
s.mu.Lock()
s.closed = true
s.mu.Unlock()
close(s.stop)
})
select {
case <-s.done:
case <-closeCtx.Done():
return closeCtx.Err()
}
return errors.Join(s.Flush(closeCtx), db.CloseOrFlush(closeCtx, s.base))
}
func (s *AsyncSummaryStore) closeContext(ctx context.Context) (context.Context, context.CancelFunc) {
ctx = summaryCtx(ctx)
if _, ok := ctx.Deadline(); ok {
return ctx, func() {}
}
timeout := sumFlushTimeout
if s.timeout > 0 {
timeout = s.timeout
}
return context.WithTimeout(ctx, timeout)
}
// Check 透传底层摘要存储预检。
func (s *AsyncSummaryStore) Check(ctx context.Context) error {
if s == nil || s.base == nil {
return errSumStoreNil
}
return db.Check(ctx, s.base)
}
func (s *AsyncSummaryStore) run() {
defer close(s.done)
retry := time.NewTimer(time.Hour)
stopSumTimer(retry)
var retryC <-chan time.Time
defer stopSumTimer(retry)
for {
select {
case <-s.wake:
if err := s.flushBackground(); err != nil {
resetSumTimer(retry, sumRetryInterval)
retryC = retry.C
} else {
retryC = nil
}
case <-retryC:
retryC = nil
if err := s.flushBackground(); err != nil {
resetSumTimer(retry, sumRetryInterval)
retryC = retry.C
}
case <-s.stop:
return
}
}
}
func (s *AsyncSummaryStore) flushBackground() error {
timeout := s.timeout
if timeout <= 0 {
timeout = sumFlushTimeout
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
err := s.Flush(ctx)
cancel()
return err
}
func (s *AsyncSummaryStore) putLocked(summary PlayerSummary) {
if old, ok := s.pending[summary.AccountID]; ok && old.summary.RoleID != "" && old.summary.RoleID != summary.RoleID {
delete(s.roles, old.summary.RoleID)
}
s.seq++
s.pending[summary.AccountID] = sumPending{
summary: clonePlayerSummary(summary),
seq: s.seq,
}
if summary.RoleID != "" {
s.roles[summary.RoleID] = summary.AccountID
}
}
func (s *AsyncSummaryStore) wakeLocked() {
select {
case s.wake <- struct{}{}:
default:
}
}
func (s *AsyncSummaryStore) pendingByAcct(accountID string) (PlayerSummary, bool) {
s.mu.RLock()
pending, ok := s.pending[accountID]
s.mu.RUnlock()
return clonePlayerSummary(pending.summary), ok
}
func (s *AsyncSummaryStore) pendingByRole(roleID string) (PlayerSummary, bool) {
s.mu.RLock()
accountID := s.roles[roleID]
pending, ok := s.pending[accountID]
s.mu.RUnlock()
if !ok || pending.summary.RoleID != roleID {
return PlayerSummary{}, false
}
return clonePlayerSummary(pending.summary), true
}
func (s *AsyncSummaryStore) pendingByAccts(accountIDs []string) []PlayerSummary {
if len(accountIDs) == 0 {
return nil
}
out := make([]PlayerSummary, 0, len(accountIDs))
s.mu.RLock()
for _, accountID := range accountIDs {
if pending, ok := s.pending[accountID]; ok {
out = append(out, clonePlayerSummary(pending.summary))
}
}
s.mu.RUnlock()
sortPlayerSummaries(out)
return out
}
func (s *AsyncSummaryStore) pendingAll() []PlayerSummary {
s.mu.RLock()
out := make([]PlayerSummary, 0, len(s.pending))
for _, pending := range s.pending {
out = append(out, clonePlayerSummary(pending.summary))
}
s.mu.RUnlock()
sortPlayerSummaries(out)
return out
}
func (s *AsyncSummaryStore) takePending() []sumWrite {
s.mu.RLock()
defer s.mu.RUnlock()
if len(s.pending) == 0 {
return nil
}
out := make([]sumWrite, 0, len(s.pending))
for _, pending := range s.pending {
out = append(out, sumWrite{
summary: clonePlayerSummary(pending.summary),
seq: pending.seq,
})
}
sort.Slice(out, func(i, j int) bool {
if out[i].summary.AccountID == out[j].summary.AccountID {
return out[i].summary.RoleID < out[j].summary.RoleID
}
return out[i].summary.AccountID < out[j].summary.AccountID
})
return out
}
func (s *AsyncSummaryStore) removeSaved(saved sumWrite) {
s.mu.Lock()
defer s.mu.Unlock()
pending, ok := s.pending[saved.summary.AccountID]
if !ok || pending.seq != saved.seq {
return
}
delete(s.pending, saved.summary.AccountID)
if saved.summary.RoleID != "" && s.roles[saved.summary.RoleID] == saved.summary.AccountID {
delete(s.roles, saved.summary.RoleID)
}
}
func wrapSummaryStore(store SummaryStore) SummaryStore {
switch store.(type) {
case nil, *MemorySummaryStore, *AsyncSummaryStore:
return store
default:
return NewAsyncSummaryStore(store)
}
}
func normSummaryIDs(ids []string) []string {
if len(ids) == 0 {
return nil
}
seen := make(map[string]struct{}, len(ids))
out := make([]string, 0, len(ids))
for _, id := range ids {
id = strings.TrimSpace(id)
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
sort.Strings(out)
return out
}
func summaryCtx(ctx context.Context) context.Context {
if ctx == nil {
return context.Background()
}
return ctx
}
func resetSumTimer(timer *time.Timer, delay time.Duration) {
if timer == nil {
return
}
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset(delay)
}
func stopSumTimer(timer *time.Timer) {
if timer == nil {
return
}
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
}
type sumPending struct {
summary PlayerSummary
seq uint64
}
type sumWrite struct {
summary PlayerSummary
seq uint64
}