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,318 @@
package moderation
import (
"context"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"time"
)
var (
ErrInvalidSanction = errors.New("moderation sanction is invalid")
ErrSanctionMissing = errors.New("moderation sanction not found")
)
type SanctionKind string
const (
SanctionMute SanctionKind = "mute"
SanctionBan SanctionKind = "ban"
)
type Sanction struct {
ID string `json:"id"`
Subject string `json:"subject"`
Scope string `json:"scope,omitempty"`
Kind SanctionKind `json:"kind"`
Reason string `json:"reason,omitempty"`
Source string `json:"source,omitempty"`
Until time.Time `json:"until,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
type SanctionQuery struct {
Subject string `json:"subject"`
Scope string `json:"scope,omitempty"`
Kinds []SanctionKind `json:"kinds,omitempty"`
Now time.Time `json:"now,omitempty"`
}
type SanctionSnapshot struct {
Items []Sanction `json:"items,omitempty"`
}
type SanctionStore interface {
Upsert(context.Context, Sanction) (Sanction, error)
Remove(context.Context, string) error
Active(context.Context, SanctionQuery) ([]Sanction, error)
Snapshot(context.Context) (SanctionSnapshot, error)
}
type MemorySanctionStore struct {
mu sync.RWMutex
items map[string]Sanction
bySubject map[string]map[string]struct{}
now func() time.Time
}
func NewMemorySanctionStore() *MemorySanctionStore {
return &MemorySanctionStore{
items: make(map[string]Sanction),
bySubject: make(map[string]map[string]struct{}),
now: time.Now,
}
}
func (s *MemorySanctionStore) SetNow(now func() time.Time) {
if s == nil {
return
}
s.mu.Lock()
s.now = now
s.mu.Unlock()
}
func (s *MemorySanctionStore) Upsert(ctx context.Context, sanction Sanction) (Sanction, error) {
if err := ctxErr(ctx); err != nil {
return Sanction{}, err
}
if s == nil {
return Sanction{}, ErrSanctionMissing
}
item, err := normalizeSanction(sanction, s.nowUTC())
if err != nil {
return Sanction{}, err
}
s.mu.Lock()
if s.items == nil {
s.items = make(map[string]Sanction)
}
if s.bySubject == nil {
s.bySubject = make(map[string]map[string]struct{})
}
if existing, ok := s.items[item.ID]; ok && existing.Subject != item.Subject {
s.removeSubjectIdx(existing.Subject, item.ID)
}
s.items[item.ID] = cloneSanction(item)
s.addSubjectIndex(item.Subject, item.ID)
s.mu.Unlock()
return item, nil
}
func (s *MemorySanctionStore) Remove(ctx context.Context, id string) error {
if err := ctxErr(ctx); err != nil {
return err
}
if s == nil {
return ErrSanctionMissing
}
id = normalizeToken(id)
if id == "" {
return fmt.Errorf("%w: id is required", ErrInvalidSanction)
}
s.mu.Lock()
if _, ok := s.items[id]; !ok {
s.mu.Unlock()
return ErrSanctionMissing
}
s.removeSubjectIdx(s.items[id].Subject, id)
delete(s.items, id)
s.mu.Unlock()
return nil
}
func (s *MemorySanctionStore) Active(ctx context.Context, query SanctionQuery) ([]Sanction, error) {
if err := ctxErr(ctx); err != nil {
return nil, err
}
if s == nil {
return nil, nil
}
query.Subject = normalizeToken(query.Subject)
query.Scope = normalizeToken(query.Scope)
if query.Now.IsZero() {
query.Now = s.nowUTC()
} else {
query.Now = query.Now.UTC()
}
kindSet := sanctionKindSet(query.Kinds)
s.mu.RLock()
out := make([]Sanction, 0)
ids := make([]string, 0, len(s.bySubject[query.Subject]))
for id := range s.bySubject[query.Subject] {
ids = append(ids, id)
}
sort.Strings(ids)
for _, id := range ids {
item, ok := s.items[id]
if !ok {
continue
}
if !sanctionScopeApplies(item.Scope, query.Scope) {
continue
}
if len(kindSet) > 0 {
if _, ok := kindSet[item.Kind]; !ok {
continue
}
}
if !item.ActiveAt(query.Now) {
continue
}
out = append(out, cloneSanction(item))
}
s.mu.RUnlock()
sortSanctions(out)
return out, nil
}
func (s *MemorySanctionStore) Snapshot(ctx context.Context) (SanctionSnapshot, error) {
if err := ctxErr(ctx); err != nil {
return SanctionSnapshot{}, err
}
if s == nil {
return SanctionSnapshot{}, nil
}
s.mu.RLock()
items := make([]Sanction, 0, len(s.items))
for _, item := range s.items {
items = append(items, cloneSanction(item))
}
s.mu.RUnlock()
sortSanctions(items)
return SanctionSnapshot{Items: items}, nil
}
func (s *MemorySanctionStore) addSubjectIndex(subject, id string) {
if s.bySubject[subject] == nil {
s.bySubject[subject] = make(map[string]struct{})
}
s.bySubject[subject][id] = struct{}{}
}
func (s *MemorySanctionStore) removeSubjectIdx(subject, id string) {
if ids := s.bySubject[subject]; ids != nil {
delete(ids, id)
if len(ids) == 0 {
delete(s.bySubject, subject)
}
}
}
func (s Sanction) ActiveAt(now time.Time) bool {
if now.IsZero() {
now = time.Now().UTC()
} else {
now = now.UTC()
}
return s.Until.IsZero() || s.Until.After(now)
}
func normalizeSanction(sanction Sanction, now time.Time) (Sanction, error) {
sanction.ID = normalizeToken(sanction.ID)
sanction.Subject = normalizeToken(sanction.Subject)
sanction.Scope = normalizeToken(sanction.Scope)
sanction.Reason = strings.TrimSpace(sanction.Reason)
sanction.Source = strings.TrimSpace(sanction.Source)
sanction.Meta = cloneStringMap(sanction.Meta)
if sanction.Subject == "" {
return Sanction{}, fmt.Errorf("%w: subject is required", ErrInvalidSanction)
}
if sanction.Kind != SanctionMute && sanction.Kind != SanctionBan {
return Sanction{}, fmt.Errorf("%w: invalid kind %q", ErrInvalidSanction, sanction.Kind)
}
if sanction.ID == "" {
if hasDefaultIDDelim(sanction.Subject) || hasDefaultIDDelim(sanction.Scope) {
return Sanction{}, fmt.Errorf("%w: subject or scope contains reserved delimiter", ErrInvalidSanction)
}
sanction.ID = sanction.Subject + ":" + string(sanction.Kind) + ":" + sanction.Scope
}
if sanction.CreatedAt.IsZero() {
sanction.CreatedAt = now.UTC()
} else {
sanction.CreatedAt = sanction.CreatedAt.UTC()
}
if !sanction.Until.IsZero() {
sanction.Until = sanction.Until.UTC()
}
return sanction, nil
}
func sanctionKindSet(kinds []SanctionKind) map[SanctionKind]struct{} {
if len(kinds) == 0 {
return nil
}
out := make(map[SanctionKind]struct{}, len(kinds))
for _, kind := range kinds {
if kind == "" {
continue
}
out[kind] = struct{}{}
}
return out
}
func sanctionScopeApplies(sanctionScope, requestScope string) bool {
sanctionScope = normalizeToken(sanctionScope)
requestScope = normalizeToken(requestScope)
return sanctionScope == "" || sanctionScope == "global" || sanctionScope == requestScope
}
// hasDefaultIDDelim 拦截会让默认处罚 ID 产生歧义的保留分隔符。
func hasDefaultIDDelim(value string) bool {
return strings.Contains(value, ":") || strings.ContainsRune(value, '\x00')
}
// encodeModerationKey 用长度前缀拼接多段键,避免字段内容自带分隔符时发生碰撞。
func encodeModerationKey(parts ...string) string {
var builder strings.Builder
for _, part := range parts {
builder.WriteString(strconv.Itoa(len(part)))
builder.WriteByte(':')
builder.WriteString(part)
}
return builder.String()
}
func sortSanctions(items []Sanction) {
sort.Slice(items, func(i, j int) bool {
if items[i].Subject != items[j].Subject {
return items[i].Subject < items[j].Subject
}
if items[i].Scope != items[j].Scope {
return items[i].Scope < items[j].Scope
}
if items[i].Kind != items[j].Kind {
return items[i].Kind < items[j].Kind
}
return items[i].ID < items[j].ID
})
}
func cloneSanction(sanction Sanction) Sanction {
sanction.Meta = cloneStringMap(sanction.Meta)
return sanction
}
func (s *MemorySanctionStore) nowUTC() time.Time {
s.mu.RLock()
now := s.now
s.mu.RUnlock()
if now == nil {
return time.Now().UTC()
}
return now().UTC()
}
func ctxErr(ctx context.Context) error {
if ctx == nil {
return nil
}
return ctx.Err()
}