服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
204 lines
6.3 KiB
Go
204 lines
6.3 KiB
Go
package onlineevent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
)
|
|
|
|
const LegacyLevelPVFPath = "event/decidelevelupevent.evt"
|
|
const LegacyLevelMetadataKey = "event_legacy_levelup_116_v1"
|
|
|
|
type LegacyLevelReward struct {
|
|
Level uint32
|
|
Item AttendancePVFItemReward
|
|
}
|
|
|
|
type LegacyLevelCatalog struct {
|
|
Main []LegacyLevelReward
|
|
Stage []LegacyLevelReward
|
|
}
|
|
|
|
func LoadLegacyLevelCatalog(ctx context.Context, source dnfpvf.Source) (*LegacyLevelCatalog, error) {
|
|
text, err := source.ReadText(LegacyLevelPVFPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
doc, err := dnfpvf.Parse(LegacyLevelPVFPath, text)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
catalog := &LegacyLevelCatalog{}
|
|
all := []AttendancePVFItemReward{}
|
|
for _, spec := range []struct {
|
|
name string
|
|
count int
|
|
dst *[]LegacyLevelReward
|
|
}{
|
|
{"Main Reward", 6, &catalog.Main}, {"Sub Reward", 8, &catalog.Stage},
|
|
} {
|
|
values, err := attendanceIntegerSection(doc, spec.name, spec.count*4)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var previous int64
|
|
for i := 0; i < len(values); i += 4 {
|
|
level, id, count, enabled := values[i], values[i+1], values[i+2], values[i+3]
|
|
if level <= previous || level > 90 || id <= 0 || id > 0xffffffff || count <= 0 || count > 0x7fffffff || enabled != 1 {
|
|
return nil, fmt.Errorf("legacy level reward [%s] row=%d is invalid", spec.name, i/4)
|
|
}
|
|
reward := AttendancePVFItemReward{ItemID: id, Count: count}
|
|
*spec.dst = append(*spec.dst, LegacyLevelReward{Level: uint32(level), Item: reward})
|
|
all = append(all, reward)
|
|
previous = level
|
|
}
|
|
}
|
|
paths, err := resolveAttendanceRewardDefinitions(ctx, source, all)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, rows := range [][]LegacyLevelReward{catalog.Main, catalog.Stage} {
|
|
for i := range rows {
|
|
rows[i].Item.DefinitionPath = paths[rows[i].Item.ItemID]
|
|
}
|
|
}
|
|
return catalog, nil
|
|
}
|
|
|
|
type LegacyLevelState struct {
|
|
Target uint32 `json:"target"`
|
|
Start uint32 `json:"start"`
|
|
Stages uint32 `json:"stages"`
|
|
Claimed bool `json:"claimed"`
|
|
}
|
|
|
|
type LegacyLevelResult struct {
|
|
LegacyLevelState
|
|
MailIDs []string
|
|
}
|
|
|
|
type LegacyLevelCommand struct {
|
|
AccountID, CharacterID string
|
|
Target uint32
|
|
Claim bool
|
|
Now time.Time
|
|
Deliver func(dnfrepo.MailboxRepository, AttendancePVFItemReward, string) (string, error)
|
|
}
|
|
|
|
// Reservation and mail receipts share the existing mailbox/account transaction.
|
|
func SettleLegacyLevel(ctx context.Context, repos dnfrepo.Group, catalog *LegacyLevelCatalog, cmd LegacyLevelCommand) (LegacyLevelResult, error) {
|
|
if repos.MailboxAssets == nil || catalog == nil || len(catalog.Main) == 0 || cmd.AccountID == "" || cmd.CharacterID == "" || cmd.Now.IsZero() || cmd.Deliver == nil {
|
|
return LegacyLevelResult{}, ErrOwnerUnavailable
|
|
}
|
|
var result LegacyLevelResult
|
|
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 || character.Level < 1 || character.Level > 90 {
|
|
return fmt.Errorf("legacy level character ownership/level mismatch")
|
|
}
|
|
account, found, err := accounts.Load(ctx, cmd.AccountID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("legacy level account missing")
|
|
}
|
|
states := map[string]LegacyLevelState{}
|
|
if raw := account.Metadata[LegacyLevelMetadataKey]; raw != "" {
|
|
if err := json.Unmarshal([]byte(raw), &states); err != nil {
|
|
return err
|
|
}
|
|
if states == nil {
|
|
return fmt.Errorf("legacy level state is null")
|
|
}
|
|
}
|
|
state := states[cmd.CharacterID]
|
|
findMain := func(target uint32) (AttendancePVFItemReward, bool) {
|
|
for _, row := range catalog.Main {
|
|
if row.Level == target {
|
|
return row.Item, true
|
|
}
|
|
}
|
|
return AttendancePVFItemReward{}, false
|
|
}
|
|
main, valid := findMain(state.Target)
|
|
if state.Target == 0 && state != (LegacyLevelState{}) {
|
|
return fmt.Errorf("legacy level unreserved state is invalid")
|
|
}
|
|
if state.Target != 0 && (!valid || state.Start < 1 || state.Start >= state.Target || state.Stages>>len(catalog.Stage) != 0) {
|
|
return fmt.Errorf("legacy level saved state is invalid")
|
|
}
|
|
changed := false
|
|
if cmd.Target != 0 {
|
|
if state.Target != 0 && state.Target != cmd.Target {
|
|
return fmt.Errorf("legacy level already reserved")
|
|
}
|
|
if state.Target == 0 {
|
|
main, valid = findMain(cmd.Target)
|
|
if !valid || cmd.Target <= uint32(character.Level) {
|
|
return fmt.Errorf("legacy level target is unavailable")
|
|
}
|
|
state.Target, state.Start = cmd.Target, uint32(character.Level)
|
|
changed = true
|
|
}
|
|
}
|
|
if cmd.Claim && (state.Target == 0 || uint32(character.Level) < state.Target) {
|
|
return fmt.Errorf("legacy level target is not reached")
|
|
}
|
|
deliver := func(reward AttendancePVFItemReward, suffix string) error {
|
|
key := LegacyLevelMetadataKey + ":" + cmd.CharacterID + ":" + suffix
|
|
id, err := cmd.Deliver(mailboxes, reward, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if id == "" {
|
|
return fmt.Errorf("legacy level reward mail missing")
|
|
}
|
|
result.MailIDs = append(result.MailIDs, id)
|
|
changed = true
|
|
return nil
|
|
}
|
|
if state.Target != 0 {
|
|
for i, row := range catalog.Stage {
|
|
if row.Level <= state.Start || row.Level > uint32(character.Level) || row.Level > state.Target || state.Stages&(1<<i) != 0 {
|
|
continue
|
|
}
|
|
if err := deliver(row.Item, "stage:"+strconv.Itoa(int(row.Level))); err != nil {
|
|
return err
|
|
}
|
|
state.Stages |= 1 << i
|
|
}
|
|
if cmd.Claim && !state.Claimed {
|
|
if err := deliver(main, "main"); err != nil {
|
|
return err
|
|
}
|
|
state.Claimed = true
|
|
}
|
|
}
|
|
result.LegacyLevelState = state
|
|
if !changed {
|
|
return nil
|
|
}
|
|
states[cmd.CharacterID] = state
|
|
raw, err := json.Marshal(states)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return dnfrepo.SaveAccountMetadataEntry(ctx, accounts, account, LegacyLevelMetadataKey, string(raw), cmd.Now)
|
|
})
|
|
if err != nil {
|
|
return LegacyLevelResult{}, err
|
|
}
|
|
return result, nil
|
|
}
|