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,190 @@
// 本文件定义 PVF `[purchase limit]` / `[daily purchase limit]` 的限购策略、
// 服务窗口与作用域/周期枚举。
//
// PVF 现场(全库 Script.pvf 扫描:420 个文件、41 种组合):
//
// [purchase limit]
// `account` `weekly` 10 ← 新格式:作用域 周期 次数
//
// 旧格式是另一套字段,只有作用域+次数,周期固定为“每天”:
//
// [daily purchase limit]
// `character` 1
//
// 作用域 2 种(account/character)× 周期 4 种(accumulate/weekly/daily/
// agitwarseason)。客户端 UI 上的“账号永久/账号每周/角色永久/角色每周”4 行是
// 按 (作用域, 是否周期) 分组的展示,不是 PVF 的取值。
package purchaselimit
import (
"errors"
"fmt"
"strings"
"time"
)
var (
ErrPolicyInvalid = errors.New("dnf purchase limit policy is invalid")
ErrLimitReached = errors.New("dnf purchase limit reached")
)
// Scope 是限购计数的持久化归属者。account 维度的计数落在账号记录上,character
// 维度落在角色记录上;两者可以同时存在于同一件道具(PVF 只写一条,所以实际
// 只会命中一条)。
type Scope byte
const (
ScopeCharacter Scope = iota + 1
ScopeAccount
)
func (s Scope) String() string {
switch s {
case ScopeCharacter:
return "character"
case ScopeAccount:
return "account"
default:
return "unknown"
}
}
// Period 是一条计数使用的服务时间窗口。
type Period byte
const (
// PeriodAccumulate 是 PVF 的 `accumulate`:永不清零(永久限购)。
PeriodAccumulate Period = iota + 1
// PeriodDaily 是 PVF 的 `daily` 与旧字段 `[daily purchase limit]`。
PeriodDaily
// PeriodWeekly 是 PVF 的 `weekly`。
PeriodWeekly
// PeriodSeason 是 PVF 的 `agitwarseason`(公会争夺战赛季)。
PeriodSeason
)
func (p Period) String() string {
switch p {
case PeriodAccumulate:
return "accumulate"
case PeriodDaily:
return "daily"
case PeriodWeekly:
return "weekly"
case PeriodSeason:
return "season"
default:
return "unknown"
}
}
// Periodic 报告该周期是否会按窗口重置。
func (p Period) Periodic() bool {
return p == PeriodDaily || p == PeriodWeekly || p == PeriodSeason
}
// Policy 是一件道具的限购策略。
type Policy struct {
ItemID uint32
Scope Scope
Period Period
Limit uint32
// Section 记录来源字段,便于日志区分新字段与旧字段。
Section string
// KeyTag 是计数键里的命名空间。道具维度(PVF `[purchase limit]`)留空;商城
// 商品维度必须带前缀,因为商城是按**商品号**限购的,而商品号与道具号共用同
// 一个数值空间(例如商品 104412 与某件道具 104412 会撞同一条计数)。
KeyTag string
}
func (p Policy) Valid() error {
if p.ItemID == 0 || p.Limit == 0 ||
(p.Scope != ScopeCharacter && p.Scope != ScopeAccount) ||
(p.Period != PeriodAccumulate && p.Period != PeriodDaily && p.Period != PeriodWeekly && p.Period != PeriodSeason) {
return fmt.Errorf("%w: item=%d scope=%d period=%d limit=%d", ErrPolicyInvalid, p.ItemID, p.Scope, p.Period, p.Limit)
}
return nil
}
// ParseScope 解析 PVF 的作用域文本(反引号内的内容)。
//
// 真实 PVF 里除标准写法外还有作者笔误,全库扫描(2026-09-14)实测到的:
// - `charac` 截断,出现在 40+ 个文件(如 100060533.equ、chn_490003157.stk)
// - `chraracter` 错拼,出现在 chn_490002849.stk
//
// 这两种只可能是 character(另一取值 account 无法由它们拼出),因此按 character
// 接受。其它取值(例如 `trade`,见 chn_thief_chnagejob_10006348.stk)语义不明,
// 一律判为“不认识的字段”→ 不限购,绝不猜。
func ParseScope(raw string) (Scope, bool) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "account":
return ScopeAccount, true
case "character", "charac", "chraracter":
return ScopeCharacter, true
default:
return 0, false
}
}
// ParsePeriod 解析 PVF 的周期文本。
//
// 必须同时接受 Period.String() 的输出("season")与 PVF 的原始写法
// ("agitwarseason"):计数键是用 Period.String() 拼的,ParseCountKey 再把它读回来。
// 只认 PVF 写法会让赛季计数键读不回来(op715 下发时被判成"没有这条计数")。
func ParsePeriod(raw string) (Period, bool) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "accumulate":
return PeriodAccumulate, true
case "daily":
return PeriodDaily, true
case "weekly":
return PeriodWeekly, true
case "agitwarseason", "season":
return PeriodSeason, true
default:
return 0, false
}
}
// serviceCalendar 与 dungeonadmission 使用同一个北京服务时区:DNF 的服务日从
// 06:00 开始,周重置同样落在 06:00。
var serviceCalendar = time.FixedZone("Asia/Shanghai", 8*60*60)
// DefaultWeeklyResetWeekday 是商店限购的周重置日。DNF 的周重置统一在周四
// 06:00(与 raid 周重置一致)。PVF 不声明商店限购的重置日,所以这里是本服
// 择定的常量,改这里即可整体调整。
const DefaultWeeklyResetWeekday = time.Thursday
// Window 是一条计数当前所属的服务时间区间。Accumulate/Season 这类不按窗口
// 重置的策略返回零值窗口。
type Window struct {
Start time.Time
End time.Time
}
// Window 计算 now 落在的服务窗口。零值 now 取当前时间。
func (p Policy) Window(now time.Time) Window {
if !p.Period.Periodic() {
return Window{}
}
if now.IsZero() {
now = time.Now()
}
local := now.In(serviceCalendar)
serviceDate := local.Add(-6 * time.Hour)
start := time.Date(serviceDate.Year(), serviceDate.Month(), serviceDate.Day(), 6, 0, 0, 0, serviceCalendar)
switch p.Period {
case PeriodDaily:
return Window{Start: start, End: start.AddDate(0, 0, 1)}
case PeriodWeekly:
days := (int(serviceDate.Weekday()) - int(DefaultWeeklyResetWeekday) + 7) % 7
start = start.AddDate(0, 0, -days)
return Window{Start: start, End: start.AddDate(0, 0, 7)}
case PeriodSeason:
// 本服没有区域占领战赛季日历,赛季窗口按“永不清零”处理(见
// PeriodSeason 的说明);这里返回零值窗口,计数不会因为换周被清掉。
return Window{}
default:
return Window{}
}
}