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,576 @@
|
||||
package dungeon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/adventuregroup"
|
||||
"longheng.io/server/internal/modules/dnf/honor"
|
||||
"longheng.io/server/internal/modules/dnf/onlineevent"
|
||||
"longheng.io/server/internal/modules/dnf/progression"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/wedding"
|
||||
)
|
||||
|
||||
const (
|
||||
settlementReceiptPrefix = "dungeon_settlement_receipt_"
|
||||
settlementReceiptVersion = int64(1)
|
||||
)
|
||||
|
||||
var ErrSettlementStateInvalid = errors.New("dungeon settlement persisted state is invalid")
|
||||
|
||||
type SettlementCommand struct {
|
||||
AccountID string
|
||||
CharacterID string
|
||||
CompletionKey string
|
||||
Tables *progression.Tables
|
||||
Experience uint32
|
||||
RecommendedDungeonClear bool
|
||||
// WeddingCapsule 是婚姻成长胶囊的 PVF 规则;仅当"已婚角色通关推荐副本"时
|
||||
// 由 bridge 解析并传入,其它情况为 nil,本 owner 会整段跳过。
|
||||
WeddingCapsule *wedding.CapsuleCatalog
|
||||
// 活动(onlineevent)进度闸门。这三项都必须在本事务内推进,否则"通关即签到"
|
||||
// 这条链路就断了——签到类活动的进度只在副本结算里累加,没有第二个写入方。
|
||||
//
|
||||
// MonthlyAttendance 月度签到(活动 2370):推荐副本通关计数,每日 2 次
|
||||
// WeeklyAttendance 周周签到(活动 2208):需要目录来判定当周与补签
|
||||
// AttendanceMarble 签到弹珠:通关清次数决定额外骰子
|
||||
//
|
||||
// 三者都只在 RecommendedDungeonClear 为真时生效。
|
||||
MonthlyAttendance bool
|
||||
WeeklyAttendance *onlineevent.WeeklyCatalog
|
||||
AttendanceMarble *onlineevent.MarbleCatalog
|
||||
AdventureRuntime *adventuregroup.RuntimeConfig
|
||||
HonorExpertTables *honor.Tables
|
||||
MaximumCharacterLevel int
|
||||
// RentalPointAward and RentalPointLimit are resolved from the current PVF
|
||||
// rental catalog and committed with the dungeon completion receipt.
|
||||
RentalPointAward uint32
|
||||
RentalPointLimit uint32
|
||||
// AllowZeroExperience opts a completion that legitimately pays no clear
|
||||
// experience into the ledger. The zero-experience guard below exists to catch
|
||||
// a caller whose clear-exp computation silently produced nothing; a raid gate
|
||||
// is the one real case (its reward is the raid phase settlement, not character
|
||||
// growth), so it has to ask explicitly instead of the guard being relaxed for
|
||||
// everyone.
|
||||
AllowZeroExperience bool
|
||||
// Tower carries a Tower of Despair/Grief clear: its ten frozen reward slots
|
||||
// and the entry pool it consumes. Nil for every ordinary dungeon.
|
||||
Tower *TowerClear
|
||||
// GuildContribution carries the 公会贡献度 events of this clear. Nil for
|
||||
// every non-member and for clears that never took a roster snapshot.
|
||||
GuildContribution *GuildContributionClear
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SettlementResult struct {
|
||||
Character dnfrepo.CharacterRecord
|
||||
Skill dnfrepo.SkillRecord
|
||||
Inventory dnfrepo.InventoryRecord
|
||||
ExperienceGain uint32
|
||||
HonorExpertGain uint32
|
||||
SPGain int
|
||||
TPGain int
|
||||
LuckyStarBefore uint32
|
||||
LuckyStarApplied uint32
|
||||
LuckyStarAfter uint32
|
||||
LuckyStarAttempted bool
|
||||
Replayed bool
|
||||
}
|
||||
|
||||
// CommitSettlement atomically advances experience and the SP/TP ledger and
|
||||
// writes an idempotency receipt tied to the accepted dungeon completion.
|
||||
func (o *Owner) CommitSettlement(ctx context.Context, cmd SettlementCommand) (SettlementResult, error) {
|
||||
if o == nil || o.settlements == nil || strings.TrimSpace(cmd.CharacterID) == "" ||
|
||||
strings.TrimSpace(cmd.AccountID) == "" || strings.TrimSpace(cmd.CompletionKey) == "" ||
|
||||
cmd.Tables == nil {
|
||||
return SettlementResult{}, ErrOwnerUnavailable
|
||||
}
|
||||
// A tower clear pays its reward through the tower window rather than clear
|
||||
// experience, so a zero-experience tower settlement is legitimate.
|
||||
// 同理:签到类活动即使这一笔不带经验(例如免经验的活动副本),只要它会推进
|
||||
// 活动进度,也必须放行——否则玩家通关了却什么都没记上。
|
||||
if cmd.Experience == 0 && !cmd.AllowZeroExperience && cmd.Tower == nil &&
|
||||
!(cmd.RecommendedDungeonClear && (cmd.RentalPointAward > 0 || cmd.WeddingCapsule != nil ||
|
||||
cmd.MonthlyAttendance || cmd.WeeklyAttendance != nil || cmd.AttendanceMarble != nil)) {
|
||||
return SettlementResult{}, ErrSettlementStateInvalid
|
||||
}
|
||||
ctx = contextOrBackground(ctx)
|
||||
wantHash := sha256.Sum256([]byte(cmd.CompletionKey))
|
||||
now := updatedAtOrNow(cmd.UpdatedAt)
|
||||
|
||||
var result SettlementResult
|
||||
err := o.settlements.WithinCharacterSettlement(ctx, cmd.CharacterID, func(tx dnfrepo.Group) error {
|
||||
character, found, err := tx.Character.Load(ctx, cmd.CharacterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || character.CharacterID != cmd.CharacterID ||
|
||||
strings.TrimSpace(character.AccountID) != strings.TrimSpace(cmd.AccountID) {
|
||||
return ErrSettlementStateInvalid
|
||||
}
|
||||
skill, found, err := tx.Skill.Load(ctx, cmd.CharacterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || skill.CharacterID != cmd.CharacterID {
|
||||
return ErrSettlementStateInvalid
|
||||
}
|
||||
inventory, found, err := tx.Inventory.Load(ctx, cmd.CharacterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || inventory.CharacterID != cmd.CharacterID {
|
||||
return ErrSettlementStateInvalid
|
||||
}
|
||||
character = dnfrepo.CloneCharacter(character)
|
||||
skill = dnfrepo.CloneSkill(skill)
|
||||
inventory = dnfrepo.CloneInventory(inventory)
|
||||
if settlementReceiptMatches(character.Stats, wantHash) {
|
||||
receipt, err := settlementReceiptValues(character, skill)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result = SettlementResult{
|
||||
Character: character, Skill: skill, Inventory: inventory,
|
||||
ExperienceGain: receipt.ExperienceGain,
|
||||
SPGain: receipt.SPGain,
|
||||
LuckyStarBefore: receipt.LuckyStarBefore,
|
||||
LuckyStarApplied: receipt.LuckyStarApplied,
|
||||
LuckyStarAfter: receipt.LuckyStarAfter,
|
||||
LuckyStarAttempted: receipt.LuckyStarAttempted,
|
||||
Replayed: true,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
experience, present := character.Stats["exp"]
|
||||
if !present || experience < 0 || uint64(experience) > math.MaxUint32 ||
|
||||
character.Level <= 0 || skill.Points.SyncedLevel != character.Level {
|
||||
return ErrSettlementStateInvalid
|
||||
}
|
||||
maximumLevel := cmd.MaximumCharacterLevel
|
||||
if maximumLevel <= 0 {
|
||||
maximumLevel = progression.MaximumCharacterLevel
|
||||
}
|
||||
planned, err := PlanSettlementProgressionAtCap(
|
||||
cmd.Tables,
|
||||
character.Level,
|
||||
uint32(experience),
|
||||
cmd.Experience,
|
||||
skill.Points,
|
||||
maximumLevel,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
maximumLevelExperience := maximumLevelExperienceGain(
|
||||
character.Level,
|
||||
uint32(experience),
|
||||
cmd.Experience,
|
||||
cmd.MaximumCharacterLevel,
|
||||
cmd.Tables,
|
||||
)
|
||||
character.Level = planned.Experience.NewLevel
|
||||
character.Stats["exp"] = int64(planned.Experience.NewExperience)
|
||||
appliedHonorExpertGain := uint32(0)
|
||||
if maximumLevelExperience > 0 && cmd.HonorExpertTables != nil {
|
||||
currentExpert, err := honor.ExpertProgressFromStats(character.Stats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextExpert, err := cmd.HonorExpertTables.AdvanceExpert(currentExpert, uint64(maximumLevelExperience))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expertStats, err := honor.ExpertStats(nextExpert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range expertStats {
|
||||
character.Stats[key] = value
|
||||
}
|
||||
appliedHonorExpertGain = maximumLevelExperience
|
||||
}
|
||||
if cmd.RecommendedDungeonClear {
|
||||
// 签到弹珠:通关清次数决定额外骰子(4/8/12 次档)。必须在推荐次数
|
||||
// 自增之前跑,它读的是"这一局之前"的累计清次数。
|
||||
if err := onlineevent.RecordMarbleClear(&character, cmd.AttendanceMarble, now); err != nil {
|
||||
return err
|
||||
}
|
||||
count := character.Stats[adventuregroup.RecommendedDungeonClearStatKey]
|
||||
if count < 0 {
|
||||
return ErrSettlementStateInvalid
|
||||
}
|
||||
if count < math.MaxUint16 {
|
||||
character.Stats[adventuregroup.RecommendedDungeonClearStatKey] = count + 1
|
||||
}
|
||||
// 已婚角色的推荐副本通关同时累加婚姻成长胶囊进度。与推荐次数同事务,
|
||||
// 所以重放同一 CompletionKey 不会重复计入(回执幂等门在上层)。
|
||||
if cmd.WeddingCapsule != nil {
|
||||
if err := wedding.AccumulateCapsuleClear(&character, cmd.WeddingCapsule); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// 月度/周签到的进度落在**账号**上,所以有它们就必须把账号行读进同一事务。
|
||||
monthlyClear := cmd.MonthlyAttendance && cmd.RecommendedDungeonClear
|
||||
weeklyClear := cmd.WeeklyAttendance != nil && cmd.RecommendedDungeonClear
|
||||
// 骁勇结晶从 2026-09-24 起对**任意等级**的经验记账(不再只收满级溢出),
|
||||
// 所以只要这笔结算带了经验、又给了冒险团配置,就必须把账号记录读进来
|
||||
// 同事务提交;否则未满级角色通关会被 ErrSettlementStateInvalid 卡住。
|
||||
needsAccount := monthlyClear || weeklyClear || cmd.RentalPointAward > 0 || cmd.Tower != nil ||
|
||||
(cmd.AdventureRuntime != nil && (cmd.Experience > 0 || maximumLevelExperience > 0))
|
||||
var account dnfrepo.AccountRecord
|
||||
accountLoaded := false
|
||||
if needsAccount {
|
||||
if tx.Account == nil {
|
||||
return ErrSettlementStateInvalid
|
||||
}
|
||||
loaded, accountFound, err := tx.Account.Load(ctx, cmd.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !accountFound || strings.TrimSpace(loaded.AccountID) != strings.TrimSpace(cmd.AccountID) {
|
||||
// 账号行缺失时的取舍与怪物击杀一致:这笔结算里只要有**满级部分**
|
||||
// (进胶囊槽/名誉经验)或者**租赁点券**要落,就整笔失败 —— 那两项
|
||||
// 是原版规则,静默丢掉比报错更糟;只有"未满级经验镜像进骁勇结晶"
|
||||
// 时跳过记账、继续照常发角色经验与奖励,不让一行账本缺失把玩家的
|
||||
// 通关结算整个卡死("改了A把B弄坏"正是要避免的)。
|
||||
//
|
||||
// 签到同理:账号行不在就没法记签到进度,但也不能因此把整笔通关
|
||||
// 卡死——角色经验/技能/背包的写入还在下面。
|
||||
if cmd.RentalPointAward > 0 || maximumLevelExperience > 0 || cmd.Tower != nil {
|
||||
return ErrSettlementStateInvalid
|
||||
}
|
||||
// 注意:这里不能 return —— 角色经验/技能/背包的写入还在下面。
|
||||
needsAccount = false
|
||||
} else {
|
||||
account = dnfrepo.CloneAccount(loaded)
|
||||
accountLoaded = true
|
||||
}
|
||||
}
|
||||
if accountLoaded {
|
||||
if monthlyClear {
|
||||
if err := onlineevent.RecordMonthlyClear(&account, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if weeklyClear {
|
||||
if err := onlineevent.RecordWeeklyClear(&account, cmd.WeeklyAttendance, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if cmd.RentalPointAward > 0 {
|
||||
if cmd.RentalPointLimit == 0 || cmd.RentalPointAward > cmd.RentalPointLimit {
|
||||
return ErrSettlementStateInvalid
|
||||
}
|
||||
current, err := parseLuckyStars(account.Metadata[LuckyStarMetadataKey])
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: invalid %s metadata: %v", ErrSettlementStateInvalid, LuckyStarMetadataKey, err)
|
||||
}
|
||||
result.LuckyStarBefore = current
|
||||
result.LuckyStarAfter = current
|
||||
result.LuckyStarAttempted = true
|
||||
if current < cmd.RentalPointLimit {
|
||||
applied := cmd.RentalPointAward
|
||||
available := cmd.RentalPointLimit - current
|
||||
if applied > available {
|
||||
applied = available
|
||||
}
|
||||
result.LuckyStarApplied = applied
|
||||
result.LuckyStarAfter = current + applied
|
||||
if applied > 0 {
|
||||
if account.Metadata == nil {
|
||||
account.Metadata = make(map[string]string)
|
||||
}
|
||||
account.Metadata[LuckyStarMetadataKey] = strconv.FormatUint(uint64(result.LuckyStarAfter), 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
writeSettlementReceipt(
|
||||
character.Stats,
|
||||
wantHash,
|
||||
cmd.Experience,
|
||||
planned.SkillPoints.SPGain,
|
||||
planned.Experience.NewLevel,
|
||||
planned.Experience.NewExperience,
|
||||
result.LuckyStarAttempted,
|
||||
result.LuckyStarBefore,
|
||||
result.LuckyStarApplied,
|
||||
result.LuckyStarAfter,
|
||||
)
|
||||
character.UpdatedAt = now
|
||||
skill.Points = planned.SkillPoints.New
|
||||
skill.UpdatedAt = now
|
||||
if err := dnfrepo.SaveCharacterFields(ctx, tx.Character, character, dnfrepo.CharacterFieldBase, dnfrepo.CharacterFieldStats); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dnfrepo.SaveSkillFields(ctx, tx.Skill, skill, dnfrepo.SkillFieldPoints); err != nil {
|
||||
return err
|
||||
}
|
||||
if accountLoaded && cmd.AdventureRuntime != nil && (cmd.Experience > 0 || maximumLevelExperience > 0) {
|
||||
// 骁勇结晶 / 成长胶囊槽 / 账号名誉经验的口径只在 adventuregroup 里
|
||||
// 定义一次:怪物击杀与任务完成两条路也用同一个函数记入,否则同一笔
|
||||
// 经验会在不同路径上算出不同结果。
|
||||
// AwardedExperience = 这次通关经验全额 → 骁勇结晶(任意等级)
|
||||
// MaximumLevelExperience = 其中的满级溢出 → 胶囊槽 + 名誉经验
|
||||
if _, err := adventuregroup.ApplyExperienceCredit(
|
||||
&account,
|
||||
*cmd.AdventureRuntime,
|
||||
adventuregroup.ExperienceCredit{
|
||||
AwardedExperience: uint64(cmd.Experience),
|
||||
MaximumLevelExperience: uint64(maximumLevelExperience),
|
||||
},
|
||||
now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if accountLoaded {
|
||||
account.UpdatedAt = now
|
||||
if err := tx.Account.Save(ctx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if cmd.Tower != nil {
|
||||
// Consume the entry pool (character entry first, then the account
|
||||
// pool), then apply the frozen ten reward slots. Both live in this
|
||||
// transaction so a failed grant leaves the floor and the pools
|
||||
// untouched.
|
||||
if !accountLoaded {
|
||||
return ErrSettlementStateInvalid
|
||||
}
|
||||
if err := recordTowerEntryClear(&account, &character, cmd.Tower.Kind, now); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := applyTowerClear(&character, &inventory, cmd.Tower, now); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := RecordTower100FloorClear(&account, cmd.Tower.Kind, cmd.Tower.Floor); err != nil {
|
||||
return err
|
||||
}
|
||||
character.UpdatedAt = now
|
||||
if err := dnfrepo.SaveCharacterFields(ctx, tx.Character, character, dnfrepo.CharacterFieldStats); err != nil {
|
||||
return err
|
||||
}
|
||||
account.UpdatedAt = now
|
||||
if err := tx.Account.Save(ctx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
inventory.UpdatedAt = now
|
||||
if err := dnfrepo.SaveInventoryFields(ctx, tx.Inventory, inventory, dnfrepo.InventoryFieldSlots); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
result = SettlementResult{
|
||||
Character: character, Skill: skill, Inventory: inventory,
|
||||
ExperienceGain: cmd.Experience,
|
||||
HonorExpertGain: appliedHonorExpertGain,
|
||||
SPGain: planned.SkillPoints.SPGain,
|
||||
TPGain: planned.SkillPoints.TPGain,
|
||||
LuckyStarBefore: result.LuckyStarBefore,
|
||||
LuckyStarApplied: result.LuckyStarApplied,
|
||||
LuckyStarAfter: result.LuckyStarAfter,
|
||||
LuckyStarAttempted: result.LuckyStarAttempted,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return SettlementResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func maximumLevelExperienceGain(
|
||||
previousLevel int,
|
||||
previousExperience uint32,
|
||||
gain uint32,
|
||||
maximumLevel int,
|
||||
tables *progression.Tables,
|
||||
) uint32 {
|
||||
if maximumLevel <= 0 || previousLevel <= 0 || previousLevel > maximumLevel || gain == 0 || tables == nil {
|
||||
return 0
|
||||
}
|
||||
if previousLevel >= maximumLevel {
|
||||
return gain
|
||||
}
|
||||
entryExperience, err := tables.ThresholdToNext(maximumLevel - 1)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
total := uint64(previousExperience) + uint64(gain)
|
||||
if total <= uint64(entryExperience) {
|
||||
return 0
|
||||
}
|
||||
excess := total - uint64(entryExperience)
|
||||
if excess > math.MaxUint32 {
|
||||
return math.MaxUint32
|
||||
}
|
||||
return uint32(excess)
|
||||
}
|
||||
|
||||
func PlanSettlementProgression(
|
||||
tables *progression.Tables,
|
||||
level int,
|
||||
total uint32,
|
||||
gain uint32,
|
||||
points dnfrepo.SkillPointState,
|
||||
) (progression.ExperienceSkillPointPlan, error) {
|
||||
return PlanSettlementProgressionAtCap(tables, level, total, gain, points, progression.MaximumCharacterLevel)
|
||||
}
|
||||
|
||||
func PlanSettlementProgressionAtCap(
|
||||
tables *progression.Tables,
|
||||
level int,
|
||||
total uint32,
|
||||
gain uint32,
|
||||
points dnfrepo.SkillPointState,
|
||||
maximumLevel int,
|
||||
) (progression.ExperienceSkillPointPlan, error) {
|
||||
if tables == nil || level <= 0 || points.SyncedLevel != level || maximumLevel <= 0 ||
|
||||
maximumLevel > progression.MaximumCharacterLevel || level > maximumLevel {
|
||||
return progression.ExperienceSkillPointPlan{}, ErrSettlementStateInvalid
|
||||
}
|
||||
newTotal := uint64(total) + uint64(gain)
|
||||
if newTotal > math.MaxUint32 {
|
||||
newTotal = math.MaxUint32
|
||||
}
|
||||
result := progression.ExperienceResult{
|
||||
PreviousLevel: level,
|
||||
PreviousExperience: total,
|
||||
Gain: gain,
|
||||
NewLevel: level,
|
||||
NewExperience: uint32(newTotal),
|
||||
}
|
||||
for result.NewLevel < maximumLevel {
|
||||
threshold, err := tables.ThresholdToNext(result.NewLevel)
|
||||
if err != nil {
|
||||
return progression.ExperienceSkillPointPlan{}, err
|
||||
}
|
||||
if result.NewExperience < threshold {
|
||||
break
|
||||
}
|
||||
if _, ok := tables.SkillPointsAtLevel(result.NewLevel + 1); !ok {
|
||||
return progression.ExperienceSkillPointPlan{}, ErrSettlementStateInvalid
|
||||
}
|
||||
result.NewLevel++
|
||||
}
|
||||
// Max-level ordinary EXP stays at the entry threshold. The caller still
|
||||
// assigns the full post-cap award to HonorExpert.
|
||||
if result.NewLevel == maximumLevel && maximumLevel > 1 {
|
||||
entryThreshold, err := tables.ThresholdToNext(maximumLevel - 1)
|
||||
if err != nil {
|
||||
return progression.ExperienceSkillPointPlan{}, err
|
||||
}
|
||||
if result.NewExperience > entryThreshold {
|
||||
result.NewExperience = entryThreshold
|
||||
result.Saturated = true
|
||||
}
|
||||
}
|
||||
result.LevelsGained = result.NewLevel - result.PreviousLevel
|
||||
advance, err := tables.AdvanceSkillPoints(points, result.NewLevel)
|
||||
if err != nil {
|
||||
return progression.ExperienceSkillPointPlan{}, err
|
||||
}
|
||||
return progression.ExperienceSkillPointPlan{Experience: result, SkillPoints: advance}, nil
|
||||
}
|
||||
|
||||
func settlementReceiptMatches(stats map[string]int64, want [sha256.Size]byte) bool {
|
||||
if stats == nil || stats[settlementReceiptPrefix+"version"] != settlementReceiptVersion {
|
||||
return false
|
||||
}
|
||||
for index := 0; index < 4; index++ {
|
||||
if uint64(stats[fmt.Sprintf("%shash_%d", settlementReceiptPrefix, index)]) !=
|
||||
binary.LittleEndian.Uint64(want[index*8:(index+1)*8]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func writeSettlementReceipt(
|
||||
stats map[string]int64,
|
||||
hash [sha256.Size]byte,
|
||||
gain uint32,
|
||||
spGain int,
|
||||
newLevel int,
|
||||
newExperience uint32,
|
||||
luckyStarAttempted bool,
|
||||
luckyStarBefore uint32,
|
||||
luckyStarApplied uint32,
|
||||
luckyStarAfter uint32,
|
||||
) {
|
||||
stats[settlementReceiptPrefix+"version"] = settlementReceiptVersion
|
||||
for index := 0; index < 4; index++ {
|
||||
stats[fmt.Sprintf("%shash_%d", settlementReceiptPrefix, index)] =
|
||||
int64(binary.LittleEndian.Uint64(hash[index*8 : (index+1)*8]))
|
||||
}
|
||||
stats[settlementReceiptPrefix+"experience_gain"] = int64(gain)
|
||||
stats[settlementReceiptPrefix+"sp_gain"] = int64(spGain)
|
||||
stats[settlementReceiptPrefix+"new_level"] = int64(newLevel)
|
||||
stats[settlementReceiptPrefix+"new_experience"] = int64(newExperience)
|
||||
stats[settlementReceiptPrefix+"lucky_star_attempted"] = 0
|
||||
if luckyStarAttempted {
|
||||
stats[settlementReceiptPrefix+"lucky_star_attempted"] = 1
|
||||
}
|
||||
stats[settlementReceiptPrefix+"lucky_star_before"] = int64(luckyStarBefore)
|
||||
stats[settlementReceiptPrefix+"lucky_star_applied"] = int64(luckyStarApplied)
|
||||
stats[settlementReceiptPrefix+"lucky_star_after"] = int64(luckyStarAfter)
|
||||
}
|
||||
|
||||
type settlementReceipt struct {
|
||||
ExperienceGain uint32
|
||||
SPGain int
|
||||
LuckyStarBefore uint32
|
||||
LuckyStarApplied uint32
|
||||
LuckyStarAfter uint32
|
||||
LuckyStarAttempted bool
|
||||
}
|
||||
|
||||
func settlementReceiptValues(
|
||||
character dnfrepo.CharacterRecord,
|
||||
skill dnfrepo.SkillRecord,
|
||||
) (settlementReceipt, error) {
|
||||
stats := character.Stats
|
||||
gain, gainPresent := stats[settlementReceiptPrefix+"experience_gain"]
|
||||
spGain := stats[settlementReceiptPrefix+"sp_gain"]
|
||||
if !gainPresent || gain < 0 || gain > math.MaxUint32 || spGain < 0 || spGain > math.MaxInt ||
|
||||
stats[settlementReceiptPrefix+"new_level"] != int64(character.Level) ||
|
||||
stats[settlementReceiptPrefix+"new_experience"] != stats["exp"] ||
|
||||
skill.Points.SyncedLevel != character.Level {
|
||||
return settlementReceipt{}, ErrSettlementStateInvalid
|
||||
}
|
||||
receipt := settlementReceipt{ExperienceGain: uint32(gain), SPGain: int(spGain)}
|
||||
attempted := stats[settlementReceiptPrefix+"lucky_star_attempted"]
|
||||
if attempted == 0 {
|
||||
if stats[settlementReceiptPrefix+"lucky_star_before"] != 0 ||
|
||||
stats[settlementReceiptPrefix+"lucky_star_applied"] != 0 ||
|
||||
stats[settlementReceiptPrefix+"lucky_star_after"] != 0 {
|
||||
return settlementReceipt{}, ErrSettlementStateInvalid
|
||||
}
|
||||
return receipt, nil
|
||||
}
|
||||
if attempted != 1 {
|
||||
return settlementReceipt{}, ErrSettlementStateInvalid
|
||||
}
|
||||
before := stats[settlementReceiptPrefix+"lucky_star_before"]
|
||||
applied := stats[settlementReceiptPrefix+"lucky_star_applied"]
|
||||
after := stats[settlementReceiptPrefix+"lucky_star_after"]
|
||||
if before < 0 || before > math.MaxUint32 || applied < 0 || applied > math.MaxUint32 ||
|
||||
after < 0 || after > math.MaxUint32 || before+applied != after {
|
||||
return settlementReceipt{}, ErrSettlementStateInvalid
|
||||
}
|
||||
receipt.LuckyStarBefore = uint32(before)
|
||||
receipt.LuckyStarApplied = uint32(applied)
|
||||
receipt.LuckyStarAfter = uint32(after)
|
||||
receipt.LuckyStarAttempted = true
|
||||
return receipt, nil
|
||||
}
|
||||
Reference in new issue
Block a user