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
@@ -0,0 +1,595 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/platform/db"
|
||||
)
|
||||
|
||||
const rtRecordLockShards = 64
|
||||
|
||||
type SaveFieldsFunc[T any, F comparable] func(context.Context, db.Store[T], T, ...F) error
|
||||
|
||||
type PrepareFunc[T any, F comparable] func(T, string, time.Time, bool) (T, []F, error)
|
||||
|
||||
type SavePrepareFunc[T any, F comparable] func(T, time.Time) (T, []F, error)
|
||||
|
||||
type TouchFunc[T any, F comparable] func(T, time.Time) (T, []F, error)
|
||||
|
||||
type SameFunc[T any] func(current, snapshot T) bool
|
||||
|
||||
var ErrCloneRequired = errors.New("profile runtime clone function is required")
|
||||
|
||||
type Options[T any, F comparable] struct {
|
||||
Store db.Store[T]
|
||||
Fields db.FieldRegistry[F]
|
||||
Clone func(T) T
|
||||
NewRecord func(string, time.Time) T
|
||||
PrepareLoaded PrepareFunc[T, F]
|
||||
PrepareSaved SavePrepareFunc[T, F]
|
||||
Touch TouchFunc[T, F]
|
||||
SaveFields SaveFieldsFunc[T, F]
|
||||
SameActive SameFunc[T]
|
||||
NormalizeKey func(string) string
|
||||
}
|
||||
|
||||
type Runtime[T any, F comparable] struct {
|
||||
store db.Store[T]
|
||||
fields db.FieldRegistry[F]
|
||||
clone func(T) T
|
||||
newRecord func(string, time.Time) T
|
||||
prepareLoaded PrepareFunc[T, F]
|
||||
prepareSaved SavePrepareFunc[T, F]
|
||||
touch TouchFunc[T, F]
|
||||
saveFields SaveFieldsFunc[T, F]
|
||||
sameActive SameFunc[T]
|
||||
normalizeKey func(string) string
|
||||
|
||||
mu sync.RWMutex
|
||||
active map[string]T
|
||||
dirty map[string]map[F]uint64
|
||||
rev uint64
|
||||
|
||||
recordLocks [rtRecordLockShards]sync.Mutex
|
||||
}
|
||||
|
||||
func NewRuntime[T any, F comparable](options Options[T, F]) (*Runtime[T, F], error) {
|
||||
if options.Store == nil {
|
||||
return nil, fmt.Errorf("profile runtime store is required")
|
||||
}
|
||||
if options.NewRecord == nil {
|
||||
return nil, fmt.Errorf("profile runtime new record function is required")
|
||||
}
|
||||
if options.SaveFields == nil {
|
||||
return nil, fmt.Errorf("profile runtime save fields function is required")
|
||||
}
|
||||
if options.Clone == nil && hasRefFields[T]() {
|
||||
return nil, fmt.Errorf("%w: %s", ErrCloneRequired, recordTypeName[T]())
|
||||
}
|
||||
if options.Clone == nil {
|
||||
options.Clone = db.IdentityClone[T]
|
||||
}
|
||||
return &Runtime[T, F]{
|
||||
store: options.Store,
|
||||
fields: options.Fields,
|
||||
clone: options.Clone,
|
||||
newRecord: options.NewRecord,
|
||||
prepareLoaded: options.PrepareLoaded,
|
||||
prepareSaved: options.PrepareSaved,
|
||||
touch: options.Touch,
|
||||
saveFields: options.SaveFields,
|
||||
sameActive: options.SameActive,
|
||||
normalizeKey: options.NormalizeKey,
|
||||
active: make(map[string]T),
|
||||
dirty: make(map[string]map[F]uint64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func hasRefFields[T any]() bool {
|
||||
var zero T
|
||||
t := reflect.TypeOf(zero)
|
||||
if t == nil {
|
||||
return false
|
||||
}
|
||||
return typeHasRefFields(t)
|
||||
}
|
||||
|
||||
func typeHasRefFields(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Pointer {
|
||||
return true
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface:
|
||||
return true
|
||||
case reflect.Array:
|
||||
return typeHasRefFields(t.Elem())
|
||||
case reflect.Struct:
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if directFieldHasRef(t.Field(i).Type) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func directFieldHasRef(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Pointer {
|
||||
return true
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface:
|
||||
return true
|
||||
case reflect.Array:
|
||||
return directFieldHasRef(t.Elem())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func recordTypeName[T any]() string {
|
||||
var zero T
|
||||
t := reflect.TypeOf(zero)
|
||||
if t == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return t.String()
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) LoadOrCreate(ctx context.Context, key string) (T, bool, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
var zero T
|
||||
key = r.normalizeRecordKey(key)
|
||||
if key == "" {
|
||||
return zero, false, fmt.Errorf("profile key is required")
|
||||
}
|
||||
|
||||
unlock := r.lockRecord(key)
|
||||
defer unlock()
|
||||
return r.loadOrCreateLocked(ctx, key)
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) loadOrCreateLocked(ctx context.Context, key string) (T, bool, error) {
|
||||
var zero T
|
||||
r.mu.RLock()
|
||||
if record, ok := r.active[key]; ok {
|
||||
r.mu.RUnlock()
|
||||
return r.clone(record), false, nil
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
now := time.Now().UTC()
|
||||
record, existed, err := r.store.Load(ctx, key)
|
||||
if err != nil {
|
||||
return zero, false, err
|
||||
}
|
||||
dirty := r.fields.NewSet()
|
||||
if !existed {
|
||||
record = r.newRecord(key, now)
|
||||
dirty.AddAll()
|
||||
} else {
|
||||
record = r.clone(record)
|
||||
}
|
||||
if r.prepareLoaded != nil {
|
||||
record, err = r.applyPrepareLoaded(record, key, now, existed, dirty)
|
||||
if err != nil {
|
||||
return zero, false, err
|
||||
}
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
if existing, ok := r.active[key]; ok {
|
||||
r.mu.Unlock()
|
||||
return r.clone(existing), false, nil
|
||||
}
|
||||
record = r.clone(record)
|
||||
r.active[key] = record
|
||||
r.markDirtyLocked(key, dirty.List()...)
|
||||
r.mu.Unlock()
|
||||
return r.clone(record), true, nil
|
||||
}
|
||||
|
||||
// SaveAndUnload 保存当前在线快照,并在记录未被并发修改时卸载。
|
||||
// 保存链路只操作克隆快照,避免 PrepareSaved 或底层存储污染 active 状态。
|
||||
func (r *Runtime[T, F]) SaveAndUnload(ctx context.Context, key string) (T, T, bool, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
var zero T
|
||||
key = r.normalizeRecordKey(key)
|
||||
if key == "" {
|
||||
return zero, zero, false, fmt.Errorf("profile key is required")
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
r.mu.Lock()
|
||||
record, ok := r.active[key]
|
||||
if !ok {
|
||||
r.mu.Unlock()
|
||||
return zero, zero, false, nil
|
||||
}
|
||||
recordSnapshot := r.clone(record)
|
||||
saveRecord := r.clone(recordSnapshot)
|
||||
dirtyFields, dirtyRevs := r.dirtySnapshotLocked(key)
|
||||
if r.prepareSaved != nil {
|
||||
var fields []F
|
||||
var err error
|
||||
saveRecord, fields, err = r.prepareSaved(saveRecord, now)
|
||||
if err != nil {
|
||||
r.mu.Unlock()
|
||||
return zero, zero, false, err
|
||||
}
|
||||
dirtyFields = r.fields.Merge(dirtyFields, fields)
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
if err := r.saveFields(ctx, r.store, saveRecord, dirtyFields...); err != nil {
|
||||
return zero, zero, false, err
|
||||
}
|
||||
|
||||
summaryRecord := saveRecord
|
||||
r.mu.Lock()
|
||||
if current, exists := r.active[key]; exists {
|
||||
r.clearDirtyLocked(key, dirtyRevs)
|
||||
if r.same(current, recordSnapshot) && len(r.dirty[key]) == 0 {
|
||||
delete(r.active, key)
|
||||
delete(r.dirty, key)
|
||||
} else {
|
||||
summaryRecord = current
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return r.clone(saveRecord), r.clone(summaryRecord), true, nil
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) Upsert(key string, mutate func(T, bool, time.Time) (T, []F, error)) (T, error) {
|
||||
var zero T
|
||||
key = r.normalizeRecordKey(key)
|
||||
if key == "" {
|
||||
return zero, fmt.Errorf("profile key is required")
|
||||
}
|
||||
if mutate == nil {
|
||||
return zero, fmt.Errorf("profile upsert mutation is required")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
|
||||
unlock := r.lockRecord(key)
|
||||
defer unlock()
|
||||
|
||||
r.mu.RLock()
|
||||
record, existed := r.active[key]
|
||||
if !existed {
|
||||
record = r.newRecord(key, now)
|
||||
} else {
|
||||
record = r.clone(record)
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
record, fields, err := mutate(record, existed, now)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
record = r.clone(record)
|
||||
r.mu.Lock()
|
||||
r.active[key] = record
|
||||
if !existed {
|
||||
all := r.fields.All()
|
||||
fields = append(all, fields...)
|
||||
}
|
||||
r.markDirtyLocked(key, fields...)
|
||||
r.mu.Unlock()
|
||||
return r.clone(record), nil
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) MutateLoaded(ctx context.Context, key string, mutate func(*T, time.Time) ([]F, error)) (T, T, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
var zero T
|
||||
key = r.normalizeRecordKey(key)
|
||||
if key == "" {
|
||||
return zero, zero, fmt.Errorf("profile key is required")
|
||||
}
|
||||
if mutate == nil {
|
||||
return zero, zero, fmt.Errorf("profile mutation is required")
|
||||
}
|
||||
const attempts = 2
|
||||
for attempt := 0; attempt < attempts; attempt++ {
|
||||
unlock := r.lockRecord(key)
|
||||
if _, _, err := r.loadOrCreateLocked(ctx, key); err != nil {
|
||||
unlock()
|
||||
return zero, zero, err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
r.mu.RLock()
|
||||
record, ok := r.active[key]
|
||||
if !ok {
|
||||
r.mu.RUnlock()
|
||||
unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return zero, zero, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
before := r.clone(record)
|
||||
record = r.clone(record)
|
||||
r.mu.RUnlock()
|
||||
fields, err := mutate(&record, now)
|
||||
if err != nil {
|
||||
unlock()
|
||||
return zero, zero, err
|
||||
}
|
||||
if r.touch != nil {
|
||||
var touchFields []F
|
||||
record, touchFields, err = r.touch(record, now)
|
||||
if err != nil {
|
||||
unlock()
|
||||
return zero, zero, err
|
||||
}
|
||||
fields = append(fields, touchFields...)
|
||||
}
|
||||
record = r.clone(record)
|
||||
r.mu.Lock()
|
||||
r.active[key] = record
|
||||
r.markDirtyLocked(key, fields...)
|
||||
r.mu.Unlock()
|
||||
unlock()
|
||||
return before, r.clone(record), nil
|
||||
}
|
||||
return zero, zero, fmt.Errorf("profile %s was unloaded during mutation", key)
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) MutateActive(key string, mutate func(*T, time.Time) ([]F, error)) (T, T, bool, error) {
|
||||
var zero T
|
||||
key = r.normalizeRecordKey(key)
|
||||
if key == "" {
|
||||
return zero, zero, false, fmt.Errorf("profile key is required")
|
||||
}
|
||||
if mutate == nil {
|
||||
return zero, zero, false, fmt.Errorf("profile mutation is required")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
unlock := r.lockRecord(key)
|
||||
defer unlock()
|
||||
|
||||
r.mu.RLock()
|
||||
record, ok := r.active[key]
|
||||
if !ok {
|
||||
r.mu.RUnlock()
|
||||
return zero, zero, false, nil
|
||||
}
|
||||
before := r.clone(record)
|
||||
record = r.clone(record)
|
||||
r.mu.RUnlock()
|
||||
fields, err := mutate(&record, now)
|
||||
if err != nil {
|
||||
return zero, zero, true, err
|
||||
}
|
||||
if r.touch != nil && len(fields) > 0 {
|
||||
var touchFields []F
|
||||
record, touchFields, err = r.touch(record, now)
|
||||
if err != nil {
|
||||
return zero, zero, true, err
|
||||
}
|
||||
fields = append(fields, touchFields...)
|
||||
}
|
||||
record = r.clone(record)
|
||||
r.mu.Lock()
|
||||
r.active[key] = record
|
||||
r.markDirtyLocked(key, fields...)
|
||||
r.mu.Unlock()
|
||||
return before, r.clone(record), true, nil
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) LoadDetached(ctx context.Context, key string) (T, bool, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
key = r.normalizeRecordKey(key)
|
||||
if key == "" {
|
||||
var zero T
|
||||
return zero, false, fmt.Errorf("profile key is required")
|
||||
}
|
||||
record, ok, err := r.store.Load(ctx, key)
|
||||
if err != nil || !ok {
|
||||
return record, ok, err
|
||||
}
|
||||
return r.clone(record), true, nil
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) NewDetached(key string, now time.Time) T {
|
||||
key = r.normalizeRecordKey(key)
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
return r.newRecord(key, now)
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) AttachIfAbsent(key string, record T, fields ...F) (T, bool) {
|
||||
key = r.normalizeRecordKey(key)
|
||||
if key == "" {
|
||||
return r.clone(record), false
|
||||
}
|
||||
unlock := r.lockRecord(key)
|
||||
defer unlock()
|
||||
|
||||
r.mu.Lock()
|
||||
if current, ok := r.active[key]; ok {
|
||||
r.mu.Unlock()
|
||||
return r.clone(current), false
|
||||
}
|
||||
record = r.clone(record)
|
||||
r.active[key] = record
|
||||
r.markDirtyLocked(key, fields...)
|
||||
r.mu.Unlock()
|
||||
return r.clone(record), true
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) Get(key string) (T, bool) {
|
||||
key = r.normalizeRecordKey(key)
|
||||
r.mu.RLock()
|
||||
record, ok := r.active[key]
|
||||
r.mu.RUnlock()
|
||||
return r.clone(record), ok
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) Snapshot() []T {
|
||||
r.mu.RLock()
|
||||
keys := make([]string, 0, len(r.active))
|
||||
for key := range r.active {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
records := make([]T, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
records = append(records, r.active[key])
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
out := make([]T, 0, len(records))
|
||||
for _, record := range records {
|
||||
out = append(out, r.clone(record))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) ActiveKeys() []string {
|
||||
r.mu.RLock()
|
||||
keys := make([]string, 0, len(r.active))
|
||||
for key := range r.active {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) ActiveByPredicate(match func(T) bool) (T, bool) {
|
||||
var zero T
|
||||
if match == nil {
|
||||
return zero, false
|
||||
}
|
||||
r.mu.RLock()
|
||||
records := make([]T, 0, len(r.active))
|
||||
for _, record := range r.active {
|
||||
records = append(records, record)
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
for _, record := range records {
|
||||
cloned := r.clone(record)
|
||||
if match(cloned) {
|
||||
return cloned, true
|
||||
}
|
||||
}
|
||||
return zero, false
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) applyPrepareLoaded(record T, key string, now time.Time, existed bool, dirty db.FieldSet[F]) (T, error) {
|
||||
record, fields, err := r.prepareLoaded(record, key, now, existed)
|
||||
if err != nil {
|
||||
return record, err
|
||||
}
|
||||
dirty.Add(fields...)
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) markDirtyLocked(key string, fields ...F) {
|
||||
fields = r.fields.Normalize(fields)
|
||||
if len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
revisions := r.dirty[key]
|
||||
if revisions == nil {
|
||||
revisions = make(map[F]uint64, len(fields))
|
||||
}
|
||||
for _, field := range fields {
|
||||
r.rev++
|
||||
revisions[field] = r.rev
|
||||
}
|
||||
r.dirty[key] = revisions
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) dirtySnapshotLocked(key string) ([]F, map[F]uint64) {
|
||||
revisions := r.dirty[key]
|
||||
fields := r.dirtyFieldsLocked(revisions)
|
||||
if len(fields) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
snapshot := make(map[F]uint64, len(fields))
|
||||
for _, field := range fields {
|
||||
snapshot[field] = revisions[field]
|
||||
}
|
||||
return fields, snapshot
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) clearDirtyLocked(key string, saved map[F]uint64) {
|
||||
if len(saved) == 0 {
|
||||
return
|
||||
}
|
||||
revisions := r.dirty[key]
|
||||
for field, savedRev := range saved {
|
||||
if currentRev, ok := revisions[field]; ok && currentRev == savedRev {
|
||||
delete(revisions, field)
|
||||
}
|
||||
}
|
||||
if len(revisions) == 0 {
|
||||
delete(r.dirty, key)
|
||||
return
|
||||
}
|
||||
r.dirty[key] = revisions
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) dirtyFieldsLocked(revisions map[F]uint64) []F {
|
||||
if len(revisions) == 0 {
|
||||
return nil
|
||||
}
|
||||
fields := make([]F, 0, len(revisions))
|
||||
for _, field := range r.fields.All() {
|
||||
if _, ok := revisions[field]; ok {
|
||||
fields = append(fields, field)
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) normalizeRecordKey(key string) string {
|
||||
if r.normalizeKey != nil {
|
||||
return r.normalizeKey(key)
|
||||
}
|
||||
return strings.TrimSpace(key)
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) lockRecord(key string) func() {
|
||||
shard := rtRecordShard(key)
|
||||
r.recordLocks[shard].Lock()
|
||||
return func() {
|
||||
r.recordLocks[shard].Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func rtRecordShard(key string) int {
|
||||
var hash uint32 = 2166136261
|
||||
for i := 0; i < len(key); i++ {
|
||||
hash ^= uint32(key[i])
|
||||
hash *= 16777619
|
||||
}
|
||||
// 分片数固定为 64,索引一定落在 int 安全范围内。
|
||||
return int(hash & (rtRecordLockShards - 1))
|
||||
}
|
||||
|
||||
func (r *Runtime[T, F]) same(current, snapshot T) bool {
|
||||
if r.sameActive == nil {
|
||||
return false
|
||||
}
|
||||
return r.sameActive(current, snapshot)
|
||||
}
|
||||
Reference in new issue
Block a user