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,277 @@
|
||||
package onlineevent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/jobmap"
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const LevelAimingPVFPath = "event/levelaimingevent.evt"
|
||||
const LevelAimingMetadataKey = "event_level_aiming_222_v1"
|
||||
const LevelAimingRoleStat = "level_aiming_222_role"
|
||||
|
||||
// These are the ten targets in the current EXE's dword_435A72C.
|
||||
var LevelAimingTargets = [...]uint32{20, 30, 40, 50, 60, 70, 75, 80, 85, 86}
|
||||
|
||||
type LevelAimingCatalog struct {
|
||||
Limit, MinLevel, MaxLevel, Job int
|
||||
Starts, Ends time.Time
|
||||
Main AttendancePVFItemReward
|
||||
Points map[uint32]int64
|
||||
Stages []LegacyLevelReward
|
||||
}
|
||||
|
||||
func LoadLevelAimingCatalog(ctx context.Context, source dnfpvf.Source) (*LevelAimingCatalog, error) {
|
||||
text, err := source.ReadText(LevelAimingPVFPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc, err := dnfpvf.Parse(LevelAimingPVFPath, text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
control, ok := doc.Section("control panel")
|
||||
if !ok || len(control) != 10 {
|
||||
return nil, fmt.Errorf("level aiming control panel requires 10 values")
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
if control[i].Kind != dnfpvf.TokenInt {
|
||||
return nil, fmt.Errorf("level aiming control[%d] is not an integer", i)
|
||||
}
|
||||
}
|
||||
c := &LevelAimingCatalog{Limit: int(control[1].Int), MinLevel: int(control[2].Int), MaxLevel: int(control[3].Int), Job: int(control[4].Int), Points: map[uint32]int64{}}
|
||||
if control[0].Int != 2 || c.Limit < 1 || c.Limit > 5 || c.MinLevel != 1 || c.MaxLevel != 86 || (c.Job != 99 && !jobmap.Valid(c.Job)) {
|
||||
return nil, fmt.Errorf("unsupported level aiming control panel")
|
||||
}
|
||||
loc := time.FixedZone("CST", 8*3600)
|
||||
c.Starts, err = time.ParseInLocation("2006-01-02 15:04:05", control[5].Value, loc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Ends, err = time.ParseInLocation("2006-01-02 15:04:05", control[6].Value, loc)
|
||||
if err != nil || !c.Ends.After(c.Starts) {
|
||||
return nil, fmt.Errorf("invalid level aiming dates")
|
||||
}
|
||||
// The currently deployed new-character reward ratio is exactly one.
|
||||
if control[5].Kind != dnfpvf.TokenString || control[6].Kind != dnfpvf.TokenString || control[9].Kind != dnfpvf.TokenString || control[7].Float != 1 || control[8].Float != 1 {
|
||||
return nil, fmt.Errorf("unsupported level aiming control types or reward ratio")
|
||||
}
|
||||
reward, err := attendanceIntegerSection(doc, "reward", 3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if reward[0] != 0 || reward[1] <= 0 || reward[1] > 0xffffffff || reward[2] != 1 {
|
||||
return nil, fmt.Errorf("invalid level aiming main reward")
|
||||
}
|
||||
c.Main = AttendancePVFItemReward{ItemID: reward[1], Count: reward[2]}
|
||||
points, err := attendanceIntegerSection(doc, "main reward point table", 180)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < len(points); i += 2 {
|
||||
if points[i] != int64(i/2+1) || points[i+1] < 0 || points[i+1] > 0x7fffffff {
|
||||
return nil, fmt.Errorf("invalid level aiming point row")
|
||||
}
|
||||
c.Points[uint32(points[i])] = points[i+1]
|
||||
}
|
||||
stages, err := attendanceIntegerSection(doc, "sub reward lv table", 48)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all := []AttendancePVFItemReward{c.Main}
|
||||
for i := 0; i < len(stages); i += 3 {
|
||||
if stages[i] < 1 || stages[i] > int64(c.MaxLevel) || (i > 0 && stages[i] <= stages[i-3]) || stages[i+1] <= 0 || stages[i+1] > 0xffffffff || stages[i+2] <= 0 || stages[i+2] > 0x7fffffff {
|
||||
return nil, fmt.Errorf("invalid level aiming stage row")
|
||||
}
|
||||
r := AttendancePVFItemReward{ItemID: stages[i+1], Count: stages[i+2]}
|
||||
c.Stages = append(c.Stages, LegacyLevelReward{Level: uint32(stages[i]), Item: r})
|
||||
all = append(all, r)
|
||||
}
|
||||
paths, err := resolveAttendanceRewardDefinitions(ctx, source, all)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Main.DefinitionPath = paths[c.Main.ItemID]
|
||||
for i := range c.Stages {
|
||||
c.Stages[i].Item.DefinitionPath = paths[c.Stages[i].Item.ItemID]
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type LevelAimingState struct {
|
||||
CharacterID string `json:"character_id"`
|
||||
Start, Target, Reservations uint32
|
||||
Stages uint32
|
||||
Awarded bool
|
||||
}
|
||||
|
||||
func ReadLevelAimingState(account dnfrepo.AccountRecord) (LevelAimingState, error) {
|
||||
var state LevelAimingState
|
||||
if raw := account.Metadata[LevelAimingMetadataKey]; raw != "" {
|
||||
if err := json.Unmarshal([]byte(raw), &state); err != nil {
|
||||
return state, err
|
||||
}
|
||||
id, err := strconv.ParseUint(state.CharacterID, 10, 16)
|
||||
if err != nil || id == 0 || state.Start < 1 || state.Start > 86 || state.Reservations > 5 || state.Stages>>16 != 0 || (state.Target == 0 && (state.Start != 1 || state.Reservations != 0 || state.Awarded || state.Stages != 0)) || (state.Target != 0 && (!slices.Contains(LevelAimingTargets[:], state.Target) || state.Reservations == 0 || state.Start > state.Target)) {
|
||||
return state, fmt.Errorf("invalid saved level aiming state")
|
||||
}
|
||||
if (state.Reservations <= 1 && state.Start != 1) || (state.Reservations > 1 && !slices.Contains(LevelAimingTargets[:], state.Start-1)) {
|
||||
return state, fmt.Errorf("invalid saved level aiming interval")
|
||||
}
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
type LevelAimingCommand struct {
|
||||
AccountID, CharacterID string
|
||||
Register bool
|
||||
Target uint32
|
||||
Now time.Time
|
||||
Deliver func(dnfrepo.MailboxRepository, AttendancePVFItemReward, string) (string, error)
|
||||
}
|
||||
|
||||
type LevelAimingResult struct {
|
||||
LevelAimingState
|
||||
Character dnfrepo.CharacterRecord
|
||||
MailIDs []string
|
||||
}
|
||||
|
||||
func SettleLevelAiming(ctx context.Context, repos dnfrepo.Group, c *LevelAimingCatalog, cmd LevelAimingCommand) (LevelAimingResult, error) {
|
||||
if c == nil || repos.MailboxAssets == nil || cmd.AccountID == "" || cmd.CharacterID == "" || cmd.Now.IsZero() {
|
||||
return LevelAimingResult{}, ErrOwnerUnavailable
|
||||
}
|
||||
var result LevelAimingResult
|
||||
err := repos.MailboxAssets.WithinMailboxAssets(ctx, cmd.CharacterID, cmd.CharacterID, func(accounts dnfrepo.AccountRepository, _ dnfrepo.AccountInventoryRepository, characters dnfrepo.CharacterRepository, _ dnfrepo.InventoryRepository, mailboxes dnfrepo.MailboxRepository) error {
|
||||
ch, found, err := characters.Load(ctx, cmd.CharacterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || ch.AccountID != cmd.AccountID {
|
||||
return fmt.Errorf("level aiming character ownership mismatch")
|
||||
}
|
||||
account, found, err := accounts.Load(ctx, cmd.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("level aiming account missing")
|
||||
}
|
||||
state, err := ReadLevelAimingState(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.Character = ch
|
||||
result.LevelAimingState = state
|
||||
active := !cmd.Now.Before(c.Starts) && cmd.Now.Before(c.Ends)
|
||||
if (cmd.Register || cmd.Target != 0) && !active {
|
||||
return fmt.Errorf("level aiming event is outside its dates")
|
||||
}
|
||||
changed := false
|
||||
if cmd.Register && state.CharacterID == "" {
|
||||
job, err := strconv.Atoi(ch.Job)
|
||||
if err != nil || !jobmap.Valid(job) || (c.Job != 99 && c.Job != job) || ch.Level != 1 || ch.CreatedAt.Before(c.Starts) || !ch.CreatedAt.Before(c.Ends) {
|
||||
return fmt.Errorf("level aiming requires a new eligible level-one character")
|
||||
}
|
||||
state = LevelAimingState{CharacterID: ch.CharacterID, Start: 1}
|
||||
changed = true
|
||||
}
|
||||
if (cmd.Register || cmd.Target != 0) && state.CharacterID != ch.CharacterID {
|
||||
return fmt.Errorf("level aiming account already owns another event character")
|
||||
}
|
||||
if state.CharacterID != ch.CharacterID || !active {
|
||||
return nil
|
||||
}
|
||||
deliver := func(reward AttendancePVFItemReward, key string) error {
|
||||
if cmd.Deliver == nil {
|
||||
return ErrOwnerUnavailable
|
||||
}
|
||||
id, err := cmd.Deliver(mailboxes, reward, LevelAimingMetadataKey+":"+ch.CharacterID+":"+key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id == "" {
|
||||
return fmt.Errorf("level aiming reward mail missing")
|
||||
}
|
||||
result.MailIDs = append(result.MailIDs, id)
|
||||
changed = true
|
||||
return nil
|
||||
}
|
||||
// Settle the preceding target before accepting the next reservation.
|
||||
if state.Target != 0 && uint32(ch.Level) >= state.Target && !state.Awarded {
|
||||
var count int64
|
||||
for level := state.Start; level <= state.Target; level++ {
|
||||
count += c.Points[level]
|
||||
}
|
||||
if count > 0 {
|
||||
reward := c.Main
|
||||
if reward.Count <= 0 || count > 0x7fffffff/reward.Count {
|
||||
return fmt.Errorf("level aiming reward count overflow")
|
||||
}
|
||||
reward.Count *= count
|
||||
if err := deliver(reward, "target:"+strconv.Itoa(int(state.Target))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
state.Awarded = true
|
||||
changed = true
|
||||
}
|
||||
if cmd.Target != 0 && cmd.Target != state.Target {
|
||||
if !slices.Contains(LevelAimingTargets[:], cmd.Target) || cmd.Target <= uint32(ch.Level) || cmd.Target <= state.Target || state.Reservations >= uint32(c.Limit) || (state.Target != 0 && !state.Awarded) {
|
||||
return fmt.Errorf("level aiming target is unavailable")
|
||||
}
|
||||
start := uint32(1)
|
||||
if state.Target != 0 {
|
||||
start = state.Target + 1
|
||||
}
|
||||
state.Start, state.Target, state.Awarded = start, cmd.Target, false
|
||||
state.Reservations++
|
||||
changed = true
|
||||
}
|
||||
if state.Target != 0 {
|
||||
for i, row := range c.Stages {
|
||||
if row.Level > uint32(ch.Level) || 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 changed {
|
||||
ch = dnfrepo.CloneCharacter(ch)
|
||||
if ch.Stats == nil {
|
||||
ch.Stats = map[string]int64{}
|
||||
}
|
||||
role := int64(3)
|
||||
if state.Target == 0 {
|
||||
role = 5
|
||||
}
|
||||
ch.Stats[LevelAimingRoleStat] = role
|
||||
ch.UpdatedAt = cmd.Now
|
||||
if err := characters.Save(ctx, ch); err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := json.Marshal(state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dnfrepo.SaveAccountMetadataEntry(ctx, accounts, account, LevelAimingMetadataKey, string(raw), cmd.Now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
result.LevelAimingState, result.Character = state, ch
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return LevelAimingResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in new issue
Block a user