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,262 @@
// 本文件拥有限购计数的读写与消费。计数按“归属者 × 周期 × 道具”一条,落点和
// 本服其它限次系统一致:
//
// - character 维度 → 角色记录的 Stats(int64 map)
// - account 维度 → 账号记录的 Metadata(string map)
//
// 每条计数配一个窗口起点:窗口变了就等于 0,不需要定时清零任务(与
// dungeonadmission/epic_production 的做法一致)。
package purchaselimit
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
)
const (
countKeyPrefix = "purchase_limit_count_"
windowKeyPrefix = "purchase_limit_window_"
)
// CountKey 返回一条计数在 Stats/Metadata 里的键。作用域由容器本身决定,因此
// 键里只带命名空间、道具与周期。
func CountKey(p Policy) string {
return countKeyPrefix + p.KeyTag + strconv.FormatUint(uint64(p.ItemID), 10) + "_" + p.Period.String()
}
// WindowKey 返回该计数窗口起点的键。
func WindowKey(p Policy) string {
return windowKeyPrefix + p.KeyTag + strconv.FormatUint(uint64(p.ItemID), 10) + "_" + p.Period.String()
}
// LimitError 在超出限购时返回,并保留当时的快照供日志使用。
type LimitError struct {
Policy Policy
State State
Requested uint32
}
func (e *LimitError) Error() string {
if e == nil {
return ErrLimitReached.Error()
}
return fmt.Sprintf("%s: item=%d scope=%s period=%s used=%d requested=%d limit=%d",
ErrLimitReached, e.Policy.ItemID, e.Policy.Scope, e.Policy.Period, e.State.Used, e.Requested, e.Policy.Limit)
}
func (e *LimitError) Unwrap() error { return ErrLimitReached }
// State 是一条计数当前的权威投影。
type State struct {
Used uint32
WindowStart int64
Limit uint32
Remaining uint32
}
func stateFromUsed(p Policy, used uint32) State {
state := State{Used: used, Limit: p.Limit}
if used < p.Limit {
state.Remaining = p.Limit - used
}
return state
}
func windowStartUnix(window Window) int64 {
if window.Start.IsZero() {
return 0
}
return window.Start.Unix()
}
// ReadCharacter 读取角色维度的计数。
func ReadCharacter(stats map[string]int64, p Policy, now time.Time) State {
if err := p.Valid(); err != nil || p.Scope != ScopeCharacter {
return stateFromUsed(p, 0)
}
used, _ := stats[CountKey(p)]
if used < 0 {
used = 0
}
stored := int64(0)
if window := p.Window(now); !window.Start.IsZero() {
stored, _ = stats[WindowKey(p)]
if stored != windowStartUnix(window) {
used = 0
}
}
if used > int64(p.Limit) {
used = int64(p.Limit)
}
return stateFromUsed(p, uint32(used))
}
// ReadAccount 读取账号维度的计数。
func ReadAccount(metadata map[string]string, p Policy, now time.Time) State {
if err := p.Valid(); err != nil || p.Scope != ScopeAccount {
return stateFromUsed(p, 0)
}
used := int64(0)
if raw, ok := metadata[CountKey(p)]; ok {
if parsed, err := strconv.ParseInt(raw, 10, 64); err == nil && parsed > 0 {
used = parsed
}
}
if window := p.Window(now); !window.Start.IsZero() {
stored := int64(0)
if raw, ok := metadata[WindowKey(p)]; ok {
if parsed, err := strconv.ParseInt(raw, 10, 64); err == nil {
stored = parsed
}
}
if stored != windowStartUnix(window) {
used = 0
}
}
if used > int64(p.Limit) {
used = int64(p.Limit)
}
return stateFromUsed(p, uint32(used))
}
// Read 按策略作用域选择容器读取。
func Read(characterStats map[string]int64, accountMetadata map[string]string, p Policy, now time.Time) State {
if p.Scope == ScopeAccount {
return ReadAccount(accountMetadata, p, now)
}
return ReadCharacter(characterStats, p, now)
}
// ConsumeCharacter 在角色容器里消费 count 次;超出限购返回 *LimitError。
func ConsumeCharacter(stats map[string]int64, p Policy, count uint32, now time.Time) error {
if err := p.Valid(); err != nil {
return err
}
if p.Scope != ScopeCharacter {
return fmt.Errorf("%w: consume character with scope=%s", ErrPolicyInvalid, p.Scope)
}
if count == 0 {
count = 1
}
state := ReadCharacter(stats, p, now)
if uint64(state.Used)+uint64(count) > uint64(p.Limit) {
return &LimitError{Policy: p, State: state, Requested: count}
}
if stats == nil {
return fmt.Errorf("%w: character stats map is nil", ErrPolicyInvalid)
}
stats[CountKey(p)] = int64(state.Used) + int64(count)
if window := p.Window(now); !window.Start.IsZero() {
stats[WindowKey(p)] = windowStartUnix(window)
}
return nil
}
// ConsumeAccount 在账号容器里消费 count 次;超出限购返回 *LimitError。
func ConsumeAccount(metadata map[string]string, p Policy, count uint32, now time.Time) error {
if err := p.Valid(); err != nil {
return err
}
if p.Scope != ScopeAccount {
return fmt.Errorf("%w: consume account with scope=%s", ErrPolicyInvalid, p.Scope)
}
if count == 0 {
count = 1
}
state := ReadAccount(metadata, p, now)
if uint64(state.Used)+uint64(count) > uint64(p.Limit) {
return &LimitError{Policy: p, State: state, Requested: count}
}
if metadata == nil {
return fmt.Errorf("%w: account metadata map is nil", ErrPolicyInvalid)
}
metadata[CountKey(p)] = strconv.FormatInt(int64(state.Used)+int64(count), 10)
if window := p.Window(now); !window.Start.IsZero() {
metadata[WindowKey(p)] = strconv.FormatInt(windowStartUnix(window), 10)
}
return nil
}
// Consume 按策略作用域选择容器消费。
func Consume(characterStats map[string]int64, accountMetadata map[string]string, p Policy, count uint32, now time.Time) error {
if p.Scope == ScopeAccount {
return ConsumeAccount(accountMetadata, p, count, now)
}
return ConsumeCharacter(characterStats, p, count, now)
}
// ParseCountKey 解析一条计数键,返回道具与周期。
//
// 带 KeyTag 命名空间的键在这里解析失败(道具段不是纯数字):这是**刻意**的——
// UsedCounts 只服务 NPC 商店的 op715“4 张表”,商城商品维度的计数不能混进那 4 张
// 表里,否则那个窗口会把商品号当道具号显示。
func ParseCountKey(key string) (uint32, Period, bool) {
rest, ok := strings.CutPrefix(strings.TrimSpace(key), countKeyPrefix)
if !ok {
return 0, 0, false
}
index := strings.LastIndex(rest, "_")
if index <= 0 || index+1 >= len(rest) {
return 0, 0, false
}
itemID, err := strconv.ParseUint(rest[:index], 10, 32)
if err != nil {
return 0, 0, false
}
period, ok := ParsePeriod(rest[index+1:])
if !ok {
return 0, 0, false
}
return uint32(itemID), period, true
}
// Entry 是容器里的一条限购计数投影。
type Entry struct {
ItemID uint32
Scope Scope
Period Period
Used uint32
}
// UsedCounts 枚举容器里所有限购计数,并按窗口判定过期(过期的按 0 处理,跳过)。
// 用于 op715 回包:把权威的已购次数下发给客户端。
func UsedCounts(characterStats map[string]int64, accountMetadata map[string]string, now time.Time) []Entry {
entries := make([]Entry, 0, len(characterStats)+len(accountMetadata))
for key, value := range characterStats {
itemID, period, ok := ParseCountKey(key)
if !ok || value <= 0 {
continue
}
policy := Policy{ItemID: itemID, Scope: ScopeCharacter, Period: period, Limit: ^uint32(0)}
if used := ReadCharacter(characterStats, policy, now).Used; used > 0 {
entries = append(entries, Entry{ItemID: itemID, Scope: ScopeCharacter, Period: period, Used: used})
}
}
for key, raw := range accountMetadata {
itemID, period, ok := ParseCountKey(key)
if !ok {
continue
}
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err != nil || value <= 0 {
continue
}
policy := Policy{ItemID: itemID, Scope: ScopeAccount, Period: period, Limit: ^uint32(0)}
if used := ReadAccount(accountMetadata, policy, now).Used; used > 0 {
entries = append(entries, Entry{ItemID: itemID, Scope: ScopeAccount, Period: period, Used: used})
}
}
sort.Slice(entries, func(left, right int) bool {
if entries[left].Scope != entries[right].Scope {
return entries[left].Scope < entries[right].Scope
}
if entries[left].Period != entries[right].Period {
return entries[left].Period < entries[right].Period
}
return entries[left].ItemID < entries[right].ItemID
})
return entries
}