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,161 @@
// Package itemcooldown owns the durable per-character item cooltime window.
//
// PVF contract. A stackable that declares `[cooltime maintenance]` (with a
// non-zero value) keeps its cooldown **across sessions**: the 90CN client's own
// gate is process-local memory only, so without a durable copy a 小退/重上 wipes
// it and the item becomes usable again. `[cool time]` is the window in
// milliseconds; `[cooltime group]` makes items of the same group share one
// window (0/absent = the item only blocks itself).
//
// Live report 2026-09-17: 首日度假礼盒 (490702504, [cool time] 86400000,
// [cooltime group] 66, [cooltime maintenance] 1 1 1) could be used again right
// after a relog. The 86 reference server implements exactly this contract
// (_86ref .../InventoryItemLifecycleService.cs: HasCooltimeMaintenance +
// CoolTime + ResolveCooltimeGroup, status CooltimeActive) and restores the
// client list through the item-state notification built from the persisted
// "cooltime" rows.
//
// Storage. One character-overflow stat row per item,
// `item_cooltime_<itemID>` = the expiry in unix seconds. Expired rows are
// pruned lazily on read, the same way the daily-use counters age.
package itemcooldown
import (
"sort"
"strconv"
"strings"
)
// StatPrefix keys one durable cooldown window per item id.
const StatPrefix = "item_cooltime_"
// Entry is one stored cooldown window.
type Entry struct {
ItemID int64
ExpireAt int64
}
// StatKey returns the character stat key that holds one item's expiry.
func StatKey(itemID int64) string {
return StatPrefix + strconv.FormatInt(itemID, 10)
}
// Entries lists every stored window, sorted by item id. Expired rows are
// included: callers that want the live set use ActiveEntries.
func Entries(stats map[string]int64) []Entry {
if len(stats) == 0 {
return nil
}
entries := make([]Entry, 0, 4)
for key, expireAt := range stats {
if !strings.HasPrefix(key, StatPrefix) {
continue
}
itemID, err := strconv.ParseInt(strings.TrimPrefix(key, StatPrefix), 10, 64)
if err != nil || itemID <= 0 || expireAt <= 0 {
continue
}
entries = append(entries, Entry{ItemID: itemID, ExpireAt: expireAt})
}
sort.Slice(entries, func(i, j int) bool { return entries[i].ItemID < entries[j].ItemID })
return entries
}
// ActiveEntries lists the windows that are still running at `now` (unix
// seconds).
func ActiveEntries(stats map[string]int64, now int64) []Entry {
all := Entries(stats)
if len(all) == 0 {
return nil
}
active := make([]Entry, 0, len(all))
for _, entry := range all {
if entry.ExpireAt > now {
active = append(active, entry)
}
}
if len(active) == 0 {
return nil
}
return active
}
// Prune drops the windows that have expired, reporting whether anything
// changed so the caller can persist.
func Prune(stats map[string]int64, now int64) bool {
changed := false
for _, entry := range Entries(stats) {
if entry.ExpireAt > now {
continue
}
delete(stats, StatKey(entry.ItemID))
changed = true
}
return changed
}
// Set stores one item's new expiry, replacing any previous window.
func Set(stats map[string]int64, itemID int64, expireAt int64) {
if stats == nil || itemID <= 0 || expireAt <= 0 {
return
}
stats[StatKey(itemID)] = expireAt
}
// Clear removes one item's window.
func Clear(stats map[string]int64, itemID int64) {
if stats == nil || itemID <= 0 {
return
}
delete(stats, StatKey(itemID))
}
// Blocks reports whether using `itemID` (whose PVF group is `group`) is still
// on cooldown for this character: a running window on the same item, or on any
// item that resolves to the same non-zero cooltime group.
//
// groupOf resolves the PVF cooltime group of a **stored** item id; it may be
// nil when the caller knows no two items share a group.
func Blocks(
stats map[string]int64,
now int64,
itemID int64,
group int64,
groupOf func(itemID int64) int64,
) (Entry, bool) {
for _, entry := range ActiveEntries(stats, now) {
if entry.ItemID == itemID {
return entry, true
}
if group > 0 && groupOf != nil && groupOf(entry.ItemID) == group {
return entry, true
}
}
return Entry{}, false
}
// ExpireAt returns the stored expiry of one item, if any.
func ExpireAt(stats map[string]int64, itemID int64) (int64, bool) {
if len(stats) == 0 || itemID <= 0 {
return 0, false
}
expireAt, found := stats[StatKey(itemID)]
if !found || expireAt <= 0 {
return 0, false
}
return expireAt, true
}
// MillisecondsToSeconds converts a PVF [cool time] (milliseconds) into the
// whole seconds the durable window and the client list both use. A positive
// sub-second window counts as one second so it can never become "no cooldown".
func MillisecondsToSeconds(milliseconds int64) int64 {
if milliseconds <= 0 {
return 0
}
seconds := milliseconds / 1000
if milliseconds%1000 != 0 {
seconds++
}
return seconds
}