DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,268 @@
|
||||
package onlineevent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const PCRoomTimePointPVFPath = "etc/pcroomtimepoint.etc"
|
||||
const PCRoomTimePointMetadataKey = "online_pcroom_timepoint_228_v1"
|
||||
|
||||
// Current EXE 0x1BD4670/0x3885270 loads these rewards independently of
|
||||
// chn_AttendanceEvent.evt. Durations and period requirements are incremental.
|
||||
type PCRoomTimePointCatalog struct {
|
||||
Daily [4]AttendancePVFItemReward
|
||||
Period [4]AttendancePVFItemReward
|
||||
Seconds [4]uint32
|
||||
Counts [4]uint32
|
||||
UIPath string
|
||||
}
|
||||
|
||||
func LoadPCRoomTimePointCatalog(ctx context.Context, source dnfpvf.Source) (*PCRoomTimePointCatalog, error) {
|
||||
if source == nil {
|
||||
return nil, dnfpvf.ErrSourceRequired
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text, err := source.ReadText(PCRoomTimePointPVFPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc, err := dnfpvf.Parse(PCRoomTimePointPVFPath, text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &PCRoomTimePointCatalog{}
|
||||
for name, want := range map[string]string{"daily reward autoget": "false", "daily reward loop": "1", "period reward loop": "4"} {
|
||||
tokens, ok := doc.Section(name)
|
||||
if !ok || len(tokens) != 1 || tokens[0].Value != want {
|
||||
return nil, fmt.Errorf("pcroom [%s] differs from current manual four-stage edition", name)
|
||||
}
|
||||
}
|
||||
for _, spec := range []struct {
|
||||
name string
|
||||
width int
|
||||
}{{"daily reward items", 6}, {"period reward item", 5}} {
|
||||
tokens, ok := doc.Section(spec.name)
|
||||
if !ok || len(tokens) != 4*spec.width {
|
||||
return nil, fmt.Errorf("pcroom [%s] has invalid shape", spec.name)
|
||||
}
|
||||
var cumulative uint64
|
||||
for i := 0; i < 4; i++ {
|
||||
row := tokens[i*spec.width : (i+1)*spec.width]
|
||||
for j := 0; j < 4; j++ {
|
||||
if row[j].Kind != dnfpvf.TokenInt || row[j].Int <= 0 || row[j].Int > math.MaxUint32 {
|
||||
return nil, fmt.Errorf("pcroom [%s] row %d integer %d invalid", spec.name, i, j)
|
||||
}
|
||||
}
|
||||
for j := 4; j < spec.width; j++ {
|
||||
if row[j].Value != "true" {
|
||||
return nil, fmt.Errorf("pcroom [%s] row %d unsupported flag", spec.name, i)
|
||||
}
|
||||
}
|
||||
r := AttendancePVFItemReward{ItemID: row[1].Int, Count: row[2].Int}
|
||||
if spec.width == 6 {
|
||||
if row[0].Int != 1 || row[3].Int%1000 != 0 {
|
||||
return nil, fmt.Errorf("pcroom daily group/duration invalid")
|
||||
}
|
||||
cumulative += uint64(row[3].Int / 1000)
|
||||
if cumulative > secondsPerDay {
|
||||
return nil, fmt.Errorf("pcroom daily duration exceeds service day")
|
||||
}
|
||||
c.Daily[i], c.Seconds[i] = r, uint32(cumulative)
|
||||
} else {
|
||||
if row[0].Int != int64(i+1) {
|
||||
return nil, fmt.Errorf("pcroom period slot invalid")
|
||||
}
|
||||
cumulative += uint64(row[3].Int)
|
||||
if cumulative > math.MaxInt32 {
|
||||
return nil, fmt.Errorf("pcroom period count overflows client")
|
||||
}
|
||||
c.Period[i], c.Counts[i] = r, uint32(cumulative)
|
||||
}
|
||||
}
|
||||
}
|
||||
ui, ok := doc.Section("ui file")
|
||||
if !ok || len(ui) != 1 || strings.ToLower(ui[0].Value) != "ui/event/pcroomtimepoint/pcroomtimepointrewarditeminfowindow_chn.xui" {
|
||||
return nil, fmt.Errorf("pcroom native UI path differs from verified current client")
|
||||
}
|
||||
c.UIPath = ui[0].Value
|
||||
if _, err := source.ReadText(c.UIPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths, err := resolveAttendanceRewardDefinitions(ctx, source, append(append([]AttendancePVFItemReward{}, c.Daily[:]...), c.Period[:]...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attachAttendanceDefinitionPaths(c.Daily[:], paths)
|
||||
attachAttendanceDefinitionPaths(c.Period[:], paths)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type pcroomTimePointState struct {
|
||||
Version int `json:"version"`
|
||||
Date string `json:"date"`
|
||||
DailyMask byte `json:"daily_mask"`
|
||||
DayComplete bool `json:"day_complete"`
|
||||
Total uint32 `json:"total"`
|
||||
PeriodMask byte `json:"period_mask"`
|
||||
}
|
||||
|
||||
type PCRoomTimePointSnapshot struct {
|
||||
Seconds uint32
|
||||
Total uint32
|
||||
DailyMask byte
|
||||
PeriodMask uint32
|
||||
MailIDs []string
|
||||
}
|
||||
|
||||
type PCRoomTimePointCommand struct {
|
||||
AccountID, CharacterID string
|
||||
Now time.Time
|
||||
// Reuse the existing authoritative interval-union clock, including its
|
||||
// 06:00 boundary, cross-character accounting and already persisted time.
|
||||
ClockDefinition Definition
|
||||
DailyClaim byte
|
||||
PeriodClaim int // -1 means no period claim; 0..3 are native button indices.
|
||||
Deliver func(dnfrepo.MailboxRepository, AttendancePVFItemReward, string) (string, error)
|
||||
}
|
||||
|
||||
// SettlePCRoomTimePoint counts one fully completed service day, independently
|
||||
// of clicking the fourth reward. Mail and masks commit in the same transaction.
|
||||
// The current PVF edition is kept open locally; counts stop at its final tier.
|
||||
func SettlePCRoomTimePoint(ctx context.Context, repos dnfrepo.Group, c *PCRoomTimePointCatalog, cmd PCRoomTimePointCommand) (PCRoomTimePointSnapshot, error) {
|
||||
if c == nil || repos.MailboxAssets == nil || cmd.AccountID == "" || cmd.CharacterID == "" || cmd.Now.IsZero() || cmd.Deliver == nil {
|
||||
return PCRoomTimePointSnapshot{}, ErrOwnerUnavailable
|
||||
}
|
||||
if cmd.DailyClaim&^byte(15) != 0 || cmd.PeriodClaim < -1 || cmd.PeriodClaim > 3 || (cmd.DailyClaim != 0 && cmd.PeriodClaim >= 0) {
|
||||
return PCRoomTimePointSnapshot{}, ErrRewardInvalid
|
||||
}
|
||||
if err := cmd.ClockDefinition.Validate(); err != nil {
|
||||
return PCRoomTimePointSnapshot{}, err
|
||||
}
|
||||
var result PCRoomTimePointSnapshot
|
||||
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 ErrAccountMismatch
|
||||
}
|
||||
account, found, err := accounts.Load(ctx, cmd.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return ErrAccountNotFound
|
||||
}
|
||||
clock, err := parseState(account.Metadata[metadataKey(cmd.ClockDefinition.ID)], cmd.ClockDefinition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
date, _ := calendarDay(cmd.ClockDefinition.calendar(), cmd.ClockDefinition.boundary(), cmd.Now)
|
||||
if clock.CalendarDate > date {
|
||||
return ErrObservationStale
|
||||
}
|
||||
var seconds uint32
|
||||
if clock.CalendarDate == date {
|
||||
seconds = uint32(clock.OnlineSeconds)
|
||||
}
|
||||
state := pcroomTimePointState{Version: 1, Date: date}
|
||||
raw := account.Metadata[PCRoomTimePointMetadataKey]
|
||||
if raw != "" {
|
||||
decoder := json.NewDecoder(strings.NewReader(raw))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&state); err != nil {
|
||||
return fmt.Errorf("%w: pcroom %v", ErrStateInvalid, err)
|
||||
}
|
||||
if err := rejectTrailingJSON(decoder); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := time.Parse(time.DateOnly, state.Date); err != nil {
|
||||
return ErrStateInvalid
|
||||
}
|
||||
if state.Version != 1 || state.DailyMask > 15 || state.PeriodMask > 15 || state.Total > c.Counts[3] {
|
||||
return ErrStateInvalid
|
||||
}
|
||||
for i, n := range c.Counts {
|
||||
if state.PeriodMask&(1<<i) != 0 && state.Total < n {
|
||||
return ErrStateInvalid
|
||||
}
|
||||
}
|
||||
if state.Date > date {
|
||||
return ErrObservationStale
|
||||
}
|
||||
if state.Date != date {
|
||||
state.Date = date
|
||||
state.DailyMask = 0
|
||||
state.DayComplete = false
|
||||
}
|
||||
} else if clock.CalendarDate == date {
|
||||
// One-time cutover: retain today's already delivered legacy rewards as
|
||||
// claimed, so switching the UI does not grant a second set that day.
|
||||
for i, stage := range cmd.ClockDefinition.Stages {
|
||||
if i < 4 {
|
||||
if _, ok := clock.Claims[stage.ID]; ok {
|
||||
state.DailyMask |= 1 << i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !state.DayComplete && seconds >= c.Seconds[3] {
|
||||
state.DayComplete = true
|
||||
if state.Total < c.Counts[3] {
|
||||
state.Total++
|
||||
}
|
||||
}
|
||||
result = PCRoomTimePointSnapshot{Seconds: seconds, Total: state.Total, DailyMask: state.DailyMask, PeriodMask: uint32(state.PeriodMask)}
|
||||
deliver := func(reward AttendancePVFItemReward, key string) error {
|
||||
id, err := cmd.Deliver(mailboxes, reward, PCRoomTimePointMetadataKey+":"+key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id == "" {
|
||||
return fmt.Errorf("pcroom reward mail missing")
|
||||
}
|
||||
result.MailIDs = append(result.MailIDs, id)
|
||||
return nil
|
||||
}
|
||||
for i, reward := range c.Daily {
|
||||
bit := byte(1 << i)
|
||||
if cmd.DailyClaim&bit == 0 || state.DailyMask&bit != 0 || seconds < c.Seconds[i] {
|
||||
continue
|
||||
}
|
||||
if err := deliver(reward, fmt.Sprintf("daily:%s:%d", date, i)); err != nil {
|
||||
return err
|
||||
}
|
||||
state.DailyMask |= bit
|
||||
}
|
||||
if i := cmd.PeriodClaim; i >= 0 && state.Total >= c.Counts[i] && state.PeriodMask&(1<<i) == 0 {
|
||||
if err := deliver(c.Period[i], fmt.Sprintf("period:%d", i)); err != nil {
|
||||
return err
|
||||
}
|
||||
state.PeriodMask |= 1 << i
|
||||
}
|
||||
result.DailyMask, result.PeriodMask = state.DailyMask, uint32(state.PeriodMask)
|
||||
encoded, err := json.Marshal(state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(encoded) == raw {
|
||||
return nil
|
||||
}
|
||||
return dnfrepo.SaveAccountMetadataEntry(ctx, accounts, account, PCRoomTimePointMetadataKey, string(encoded), cmd.Now)
|
||||
})
|
||||
if err != nil {
|
||||
return PCRoomTimePointSnapshot{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in new issue
Block a user