Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

197 lines
6.0 KiB
Go

package onlineevent
import (
"bytes"
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"time"
)
type creditedInterval struct {
Start uint32 `json:"start"`
End uint32 `json:"end"`
}
type claimReceipt struct {
StageID string `json:"stage_id"`
CharacterID string `json:"character_id"`
Items []CommittedItem `json:"items,omitempty"`
GoldBefore int64 `json:"gold_before,omitempty"`
GoldAfter int64 `json:"gold_after,omitempty"`
ClaimedAt time.Time `json:"claimed_at"`
}
type persistedState struct {
Version int `json:"version"`
EventID string `json:"event_id"`
CalendarDate string `json:"calendar_date,omitempty"`
OnlineSeconds uint64 `json:"online_seconds,omitempty"`
Intervals []creditedInterval `json:"intervals,omitempty"`
Claims map[string]claimReceipt `json:"claims,omitempty"`
}
func newState(eventID string) persistedState {
return persistedState{
Version: stateVersion,
EventID: eventID,
Claims: make(map[string]claimReceipt),
}
}
func parseState(raw string, definition Definition) (persistedState, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return newState(definition.ID), nil
}
decoder := json.NewDecoder(bytes.NewBufferString(raw))
decoder.DisallowUnknownFields()
var state persistedState
if err := decoder.Decode(&state); err != nil {
return persistedState{}, fmt.Errorf("%w: decode: %v", ErrStateInvalid, err)
}
if err := rejectTrailingJSON(decoder); err != nil {
return persistedState{}, err
}
if err := validateState(state, definition); err != nil {
return persistedState{}, err
}
if state.Claims == nil {
state.Claims = make(map[string]claimReceipt)
}
state.Intervals = append([]creditedInterval(nil), state.Intervals...)
for stageID, receipt := range state.Claims {
receipt.Items = cloneCommittedItems(receipt.Items)
state.Claims[stageID] = receipt
}
return state, nil
}
func rejectTrailingJSON(decoder *json.Decoder) error {
var trailing any
err := decoder.Decode(&trailing)
if err == io.EOF {
return nil
}
if err == nil {
return fmt.Errorf("%w: trailing json", ErrStateInvalid)
}
return fmt.Errorf("%w: trailing json: %v", ErrStateInvalid, err)
}
func validateState(state persistedState, definition Definition) error {
if state.Version != stateVersion || state.EventID != definition.ID {
return fmt.Errorf(
"%w: version=%d event=%q",
ErrStateInvalid,
state.Version,
state.EventID,
)
}
if state.CalendarDate == "" {
if state.OnlineSeconds != 0 || len(state.Intervals) != 0 || len(state.Claims) != 0 {
return fmt.Errorf("%w: undated state contains progress", ErrStateInvalid)
}
return nil
}
if _, err := time.ParseInLocation(time.DateOnly, state.CalendarDate, definition.calendar()); err != nil {
return fmt.Errorf("%w: calendar_date=%q", ErrStateInvalid, state.CalendarDate)
}
var total uint64
var previousEnd uint32
for index, interval := range state.Intervals {
if interval.Start >= interval.End || uint64(interval.End) > secondsPerDay ||
(index > 0 && interval.Start <= previousEnd) {
return fmt.Errorf("%w: interval[%d]=%+v", ErrStateInvalid, index, interval)
}
total += uint64(interval.End - interval.Start)
previousEnd = interval.End
}
if total != state.OnlineSeconds {
return fmt.Errorf(
"%w: seconds=%d interval_total=%d",
ErrStateInvalid,
state.OnlineSeconds,
total,
)
}
for stageID, receipt := range state.Claims {
stage, found := definition.stage(stageID)
if !found || receipt.StageID != stageID || strings.TrimSpace(receipt.CharacterID) == "" ||
receipt.ClaimedAt.IsZero() || receipt.GoldBefore < 0 || receipt.GoldAfter < receipt.GoldBefore ||
receipt.GoldAfter-receipt.GoldBefore != stage.Gold || len(receipt.Items) != len(stage.Items) {
return fmt.Errorf("%w: claim=%q", ErrStateInvalid, stageID)
}
claimDate, _ := calendarDay(definition.calendar(), definition.boundary(), receipt.ClaimedAt)
if claimDate != state.CalendarDate {
return fmt.Errorf("%w: claim=%q date=%s", ErrStateInvalid, stageID, claimDate)
}
for index, item := range receipt.Items {
if err := validateCommittedItem(item, stage.Items[index]); err != nil {
return fmt.Errorf("%w: claim=%q item=%d", ErrStateInvalid, stageID, index)
}
}
}
return nil
}
func encodeState(state persistedState) (string, error) {
encoded, err := json.Marshal(state)
if err != nil {
return "", fmt.Errorf("%w: encode: %v", ErrStateInvalid, err)
}
return string(encoded), nil
}
func (s persistedState) snapshot() Snapshot {
return Snapshot{
EventID: s.EventID,
CalendarDate: s.CalendarDate,
OnlineSeconds: s.OnlineSeconds,
ClaimedStageIDs: sortedClaimIDs(s.Claims),
}
}
func (s *persistedState) resetTo(calendarDate string) {
s.CalendarDate = calendarDate
s.OnlineSeconds = 0
s.Intervals = nil
s.Claims = make(map[string]claimReceipt)
}
// creditInterval merges one half-open second interval into the day's union.
// Keeping the compact union makes retries, overlapping sessions, and
// out-of-order server ticks idempotent without trusting a client counter.
func (s *persistedState) creditInterval(start uint32, end uint32) uint64 {
if start >= end {
return 0
}
before := s.OnlineSeconds
intervals := append(append([]creditedInterval(nil), s.Intervals...), creditedInterval{Start: start, End: end})
sort.Slice(intervals, func(left, right int) bool {
if intervals[left].Start == intervals[right].Start {
return intervals[left].End < intervals[right].End
}
return intervals[left].Start < intervals[right].Start
})
merged := make([]creditedInterval, 0, len(intervals))
for _, interval := range intervals {
if len(merged) == 0 || interval.Start > merged[len(merged)-1].End {
merged = append(merged, interval)
continue
}
if interval.End > merged[len(merged)-1].End {
merged[len(merged)-1].End = interval.End
}
}
var total uint64
for _, interval := range merged {
total += uint64(interval.End - interval.Start)
}
s.Intervals = merged
s.OnlineSeconds = total
return total - before
}