服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
160 lines
5.2 KiB
Go
160 lines
5.2 KiB
Go
package pvp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"time"
|
|
|
|
repo "longheng.io/server/internal/modules/dnf/repository"
|
|
)
|
|
|
|
var ErrTotalRewardInventoryFull = errors.New("total mission reward inventory full")
|
|
|
|
type TotalRewardPlacement func(repo.InventoryRecord, int64, int64, time.Time) (repo.InventoryRecord, error)
|
|
|
|
type TotalRewards struct {
|
|
Character repo.CharacterRecord
|
|
Items map[int16]repo.ItemStack
|
|
Granted, Pending []uint16
|
|
}
|
|
|
|
func totalRewardKey(id uint16) string { return "pvp_total_target_" + strconv.Itoa(int(id)) + "_paid" }
|
|
|
|
// GrantTotalTargets consumes committed account progress. Progress survives an
|
|
// interrupted delivery; the next result or login resumes it. Items, emblems and
|
|
// the account-wide paid marker commit together, so changing characters or
|
|
// replaying a result cannot pay a target twice. No speculative mail is created.
|
|
func GrantTotalTargets(ctx context.Context, repos repo.Group, c *Catalog, accountID, selected string, now time.Time, place TotalRewardPlacement) (TotalRewards, error) {
|
|
var result TotalRewards
|
|
if c == nil || repos.RentalAssets == nil || now.IsZero() {
|
|
return result, fmt.Errorf("total reward dependencies missing")
|
|
}
|
|
err := repo.WithinRentalAssetsScoped(ctx, repos.RentalAssets, accountID, selected,
|
|
func(accounts repo.AccountRepository, characters repo.CharacterRepository, inventories repo.InventoryRepository, _ repo.EquipmentRepository) error {
|
|
account, found, err := accounts.Load(ctx, accountID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found || account.AccountID != accountID {
|
|
return fmt.Errorf("total reward account missing")
|
|
}
|
|
character, found, err := characters.Load(ctx, selected)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found || character.AccountID != accountID || character.Stats["delete_flag"] != 0 {
|
|
return fmt.Errorf("total reward character owner mismatch")
|
|
}
|
|
character = repo.CloneCharacter(character)
|
|
if character.Stats == nil {
|
|
character.Stats = map[string]int64{}
|
|
}
|
|
p, err := ReadTotalProgress(account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, err := c.normalRows(totalMissionCharacter(accountID, p.Stats))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
complete := map[uint16]bool{}
|
|
for _, row := range rows {
|
|
complete[row.ID] = row.Progress == row.Target
|
|
}
|
|
rewardIDs := map[int64]bool{}
|
|
var inventory repo.InventoryRecord
|
|
loaded, dirty := false, false
|
|
for _, m := range c.Normal {
|
|
paid := p.Stats[totalRewardKey(m.ID)]
|
|
if paid != 0 && paid != 1 || paid == 1 && !complete[m.ID] {
|
|
return fmt.Errorf("invalid total reward marker %d", m.ID)
|
|
}
|
|
if m.RewardItemID > 0 {
|
|
rewardIDs[m.RewardItemID] = true
|
|
}
|
|
if paid == 1 || !complete[m.ID] || m.PreRequired != 0 && !complete[m.PreRequired] {
|
|
continue
|
|
}
|
|
balance := character.Stats[EmblemsKey]
|
|
if m.Emblems < 0 || m.Emblems > math.MaxInt32 || balance < 0 || balance > math.MaxInt32-m.Emblems {
|
|
return fmt.Errorf("total reward emblem overflow")
|
|
}
|
|
if m.RewardItemID > 0 {
|
|
if place == nil {
|
|
return fmt.Errorf("total mission item placement missing")
|
|
}
|
|
if !loaded {
|
|
inventory, found, err = inventories.Load(ctx, selected)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found || inventory.CharacterID != selected {
|
|
return fmt.Errorf("total reward inventory missing")
|
|
}
|
|
loaded = true
|
|
}
|
|
next, err := place(repo.CloneInventory(inventory), m.RewardItemID, m.RewardItemCount, now)
|
|
if errors.Is(err, ErrTotalRewardInventoryFull) {
|
|
result.Pending = append(result.Pending, m.ID)
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if next.CharacterID != selected {
|
|
return fmt.Errorf("total reward inventory owner changed")
|
|
}
|
|
inventory, dirty = next, true
|
|
}
|
|
character.Stats[EmblemsKey] = balance + m.Emblems
|
|
p.Stats[totalRewardKey(m.ID)] = 1
|
|
result.Granted = append(result.Granted, m.ID)
|
|
}
|
|
if len(result.Granted) > 0 {
|
|
if dirty {
|
|
if err := repo.SaveInventoryFields(ctx, inventories, inventory, repo.InventoryFieldSlots); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := repo.SaveCharacterFields(ctx, characters, character, repo.CharacterFieldStats); err != nil {
|
|
return err
|
|
}
|
|
raw, err := json.Marshal(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := repo.SaveAccountMetadataEntry(ctx, accounts, account, TotalProgressMetadataKey, string(raw), now); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Publish current absolute values also on an idempotent retry, including
|
|
// an indeterminate commit that completed before the connection failed.
|
|
if len(rewardIDs) > 0 && !loaded {
|
|
inventory, _, err = inventories.Load(ctx, selected)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
result.Items = map[int16]repo.ItemStack{}
|
|
for key, stack := range inventory.Slots {
|
|
if !rewardIDs[stack.ItemID] {
|
|
continue
|
|
}
|
|
var list, slot int
|
|
if _, err := fmt.Sscanf(key, "%d:%d", &list, &slot); err == nil && list == 0 && slot >= 0 && slot <= math.MaxInt16 {
|
|
result.Items[int16(slot)] = stack
|
|
}
|
|
}
|
|
result.Character = character
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return TotalRewards{}, err
|
|
}
|
|
return result, nil
|
|
}
|