Files
90CNServer/server/internal/modules/dnf/onlineevent/weekly.go
T
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

286 lines
8.8 KiB
Go

package onlineevent
import (
"context"
"encoding/json"
"fmt"
"math/bits"
"time"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const WeeklyPVFPath = "event/chn_event/chn_totalattendance.evt"
const WeeklyMetadataKey = "event_weekly_attendance_20260910_v1"
// The local twelve-week edition starts on Thursday at the DNF reset hour.
func WeeklyStarts() time.Time { return time.Date(2026, 9, 10, 6, 0, 0, 0, ChinaCalendar) }
func WeeklyEnds() time.Time { return WeeklyStarts().AddDate(0, 0, 12*7) }
func WeeklyWeek(now time.Time) int {
if now.Before(WeeklyStarts()) || !now.Before(WeeklyEnds()) {
return -1
}
return int(now.Sub(WeeklyStarts()) / (7 * 24 * time.Hour))
}
type WeeklyCatalog struct {
RequiredClears, CatchupClears, CatchupLimit uint32
Rewards [12]AttendancePVFItemReward
Bonus [3]AttendancePVFItemReward
Thresholds [3]uint32
}
func LoadWeeklyCatalog(ctx context.Context, source dnfpvf.Source) (*WeeklyCatalog, error) {
text, err := source.ReadText(WeeklyPVFPath)
if err != nil {
return nil, err
}
doc, err := dnfpvf.Parse(WeeklyPVFPath, text)
if err != nil {
return nil, err
}
c := &WeeklyCatalog{}
for _, spec := range []struct {
name string
count int
}{
{"event duration", 1}, {"attendance condition", 2}, {"bonus attendance count", 1}, {"reward by the week", 1},
{"attendance week", 36}, {"total attendance week", 9},
} {
v, err := attendanceIntegerSection(doc, spec.name, spec.count)
if err != nil {
return nil, err
}
switch spec.name {
case "event duration":
if v[0] != 12 {
return nil, fmt.Errorf("weekly duration must match twelve native slots")
}
case "attendance condition":
if v[0] < 1 || v[0] > 10000 || v[1] < 1 || v[1] > 10000 {
return nil, fmt.Errorf("weekly clear thresholds invalid")
}
c.RequiredClears, c.CatchupClears = uint32(v[0]), uint32(v[1])
case "bonus attendance count":
if v[0] < 0 || v[0] > 12 {
return nil, fmt.Errorf("weekly catchup limit invalid")
}
c.CatchupLimit = uint32(v[0])
case "reward by the week":
if v[0] != 0 {
return nil, fmt.Errorf("weekly reward mode is not sequential")
}
default:
for i := 0; i < len(v)/3; i++ {
key, id, count := v[i*3], v[i*3+1], v[i*3+2]
if id <= 0 || id > 0xffffffff || count <= 0 || count > 0xffffffff {
return nil, fmt.Errorf("weekly reward invalid")
}
r := AttendancePVFItemReward{ItemID: id, Count: count}
if spec.name == "attendance week" {
if key != int64(i+1) {
return nil, fmt.Errorf("weekly reward index invalid")
}
c.Rewards[i] = r
} else {
if key != []int64{4, 8, 11}[i] {
return nil, fmt.Errorf("weekly bonus threshold differs from native slots")
}
c.Bonus[i], c.Thresholds[i] = r, uint32(key)
}
}
}
}
all := append(append([]AttendancePVFItemReward{}, c.Rewards[:]...), c.Bonus[:]...)
paths, err := resolveAttendanceRewardDefinitions(ctx, source, all)
if err != nil {
return nil, err
}
attachAttendanceDefinitionPaths(c.Rewards[:], paths)
attachAttendanceDefinitionPaths(c.Bonus[:], paths)
return c, nil
}
type weeklyState struct {
Clears [12]uint32 `json:"clears"`
Signed uint16 `json:"signed"`
Catchups uint32 `json:"catchups"`
Bonus uint8 `json:"bonus"`
}
func loadWeeklyState(account dnfrepo.AccountRecord, c *WeeklyCatalog) (weeklyState, error) {
var state weeklyState
if raw := account.Metadata[WeeklyMetadataKey]; raw != "" {
if err := json.Unmarshal([]byte(raw), &state); err != nil {
return state, fmt.Errorf("weekly state invalid: %w", err)
}
}
if state.Signed>>12 != 0 || state.Bonus > 7 || state.Catchups > c.CatchupLimit || state.Catchups > uint32(bits.OnesCount16(state.Signed)) {
return state, fmt.Errorf("weekly state exceeds catalog limits")
}
for _, count := range state.Clears {
if count > c.RequiredClears+c.CatchupClears*c.CatchupLimit {
return state, fmt.Errorf("weekly clear count invalid")
}
}
for i, threshold := range c.Thresholds {
if state.Bonus&(1<<i) != 0 && uint32(bits.OnesCount16(state.Signed)) < threshold {
return state, fmt.Errorf("weekly bonus state invalid")
}
}
return state, nil
}
// Called inside the dungeon settlement receipt transaction, never from a client count.
func RecordWeeklyClear(account *dnfrepo.AccountRecord, c *WeeklyCatalog, now time.Time) error {
week := WeeklyWeek(now)
if week < 0 || c == nil {
return nil
}
state, err := loadWeeklyState(*account, c)
if err != nil {
return err
}
if state.Clears[week] >= c.RequiredClears+c.CatchupClears*c.CatchupLimit {
return nil
}
state.Clears[week]++
raw, err := json.Marshal(state)
if err != nil {
return err
}
if account.Metadata == nil {
account.Metadata = map[string]string{}
}
account.Metadata[WeeklyMetadataKey] = string(raw)
return nil
}
type WeeklySnapshot struct {
Week, Total, Clears, Catchups, BonusMask uint32
SignedThisWeek, CanSign, CanCatchup, CanBonus bool
SignedWeeks [20]byte
MailIDs []string
}
func weeklySnapshot(state weeklyState, c *WeeklyCatalog, week int) WeeklySnapshot {
s := WeeklySnapshot{Total: uint32(bits.OnesCount16(state.Signed)), Catchups: state.Catchups}
for i, threshold := range c.Thresholds {
if state.Bonus&(1<<i) != 0 {
s.BonusMask |= 1 << threshold
} else if s.Total >= threshold && week >= 0 {
s.CanBonus = true
}
}
index := 0
for i := 0; i < 12; i++ {
if state.Signed&(1<<i) != 0 {
s.SignedWeeks[index] = byte(i + 1)
index++
}
}
if week < 0 {
s.Week = 13
return s
}
s.Week = uint32(week + 1)
s.Clears = state.Clears[week]
s.SignedThisWeek = state.Signed&(1<<week) != 0
// Each earlier missing week can be restored once. The current week is signed normally first.
s.CanCatchup = s.SignedThisWeek && s.Total < uint32(week+1) && state.Catchups < c.CatchupLimit
s.CanSign = (!s.SignedThisWeek && s.Clears >= c.RequiredClears) || (s.CanCatchup && s.Clears >= c.CatchupClears)
return s
}
type WeeklyCommand struct {
AccountID, CharacterID string
Now time.Time
Claim, ClaimBonus bool
Deliver func(dnfrepo.MailboxRepository, AttendancePVFItemReward, string) (string, error)
}
func SettleWeeklyAttendance(ctx context.Context, repos dnfrepo.Group, c *WeeklyCatalog, cmd WeeklyCommand) (WeeklySnapshot, error) {
if c == nil || repos.MailboxAssets == nil || cmd.AccountID == "" || cmd.CharacterID == "" || cmd.Now.IsZero() || cmd.Deliver == nil {
return WeeklySnapshot{}, ErrOwnerUnavailable
}
var result WeeklySnapshot
err := repos.MailboxAssets.WithinMailboxAssets(ctx, cmd.CharacterID, cmd.CharacterID, func(accounts dnfrepo.AccountRepository, _ dnfrepo.AccountInventoryRepository, characters dnfrepo.CharacterRepository, _ dnfrepo.InventoryRepository, mailboxes dnfrepo.MailboxRepository) error {
character, found, err := characters.Load(ctx, cmd.CharacterID)
if err != nil {
return err
}
if !found || character.AccountID != cmd.AccountID {
return fmt.Errorf("weekly character ownership mismatch")
}
account, found, err := accounts.Load(ctx, cmd.AccountID)
if err != nil {
return err
}
if !found {
return fmt.Errorf("weekly account missing")
}
state, err := loadWeeklyState(account, c)
if err != nil {
return err
}
week := WeeklyWeek(cmd.Now)
before := weeklySnapshot(state, c, week)
var mails []string
deliver := func(r AttendancePVFItemReward, key string) error {
id, err := cmd.Deliver(mailboxes, r, WeeklyMetadataKey+":"+key)
if err != nil {
return err
}
if id == "" {
return fmt.Errorf("weekly mail missing")
}
mails = append(mails, id)
return nil
}
if cmd.Claim && before.CanSign {
target := week
cost := c.RequiredClears
if before.SignedThisWeek {
for target = 0; target < week; target++ {
if state.Signed&(1<<target) == 0 {
break
}
}
cost = c.CatchupClears
state.Catchups++
}
if err := deliver(c.Rewards[before.Total], fmt.Sprintf("week:%d", target+1)); err != nil {
return err
}
state.Signed |= 1 << target
state.Clears[week] -= cost
}
if cmd.ClaimBonus && week >= 0 {
for i, threshold := range c.Thresholds {
if uint32(bits.OnesCount16(state.Signed)) >= threshold && state.Bonus&(1<<i) == 0 {
if err := deliver(c.Bonus[i], fmt.Sprintf("bonus:%d", threshold)); err != nil {
return err
}
state.Bonus |= 1 << i
}
}
}
result = weeklySnapshot(state, c, week)
result.MailIDs = mails
if len(mails) == 0 {
return nil
}
raw, err := json.Marshal(state)
if err != nil {
return err
}
return dnfrepo.SaveAccountMetadataEntry(ctx, accounts, account, WeeklyMetadataKey, string(raw), cmd.Now)
})
if err != nil {
return WeeklySnapshot{}, err
}
return result, nil
}