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,912 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/adventuregroup"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfdungeon "longheng.io/server/internal/modules/dnf/dungeon"
|
||||
dnfhonor "longheng.io/server/internal/modules/dnf/honor"
|
||||
"longheng.io/server/internal/modules/dnf/progression"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const currentAdventureGenericFailureCode = 1
|
||||
|
||||
// 冒险团商店(op1414)的结果码取自客户端 0x00C79940 的跳转表(重启后只认这些):
|
||||
//
|
||||
// 19 → dstr 44465 "超过购买限制"
|
||||
// 22 → adv 100002122 "购买材料不足。"
|
||||
// 23 → adv 100002156 "无法购买决斗场专用角色。"
|
||||
// 204 → dstr 746 "物品栏空间不足"
|
||||
// 235 → adv 100003018 "冒险团Lv较低。"
|
||||
//
|
||||
// 表里其余的码(**包括我们过去一律回的 1**)落到 default 分支 = 什么都不弹。
|
||||
// "买超限了却没提示"就是这么来的。
|
||||
const (
|
||||
currentAdventureShopPurchaseLimitCode = 19
|
||||
currentAdventureShopInsufficientCode = 22
|
||||
currentAdventureShopLevelTooLowCode = 235
|
||||
currentAdventureShopInventoryFullCode = 204
|
||||
)
|
||||
|
||||
// currentAdventureShopFailureCode 把拒绝原因翻成客户端认识的 19/22/204,
|
||||
// 认不出来的原因仍回笼统码(静默)——宁可不说,也不能把"背包满"说成"材料不足"。
|
||||
func currentAdventureShopFailureCode(err error) byte {
|
||||
switch {
|
||||
case errors.Is(err, adventuregroup.ErrPurchaseLimitReached):
|
||||
return currentAdventureShopPurchaseLimitCode
|
||||
case errors.Is(err, adventuregroup.ErrInsufficientShopPoints):
|
||||
return currentAdventureShopInsufficientCode
|
||||
case errors.Is(err, errDungeonPickupInventoryFull),
|
||||
errors.Is(err, dnfdungeon.ErrInventoryFull):
|
||||
return currentAdventureShopInventoryFullCode
|
||||
default:
|
||||
return currentAdventureGenericFailureCode
|
||||
}
|
||||
}
|
||||
|
||||
// The current NoPack executable and its DNF90 content profile cap characters
|
||||
// at level 90. The progression PVF contains later table rows for compatibility,
|
||||
// so its row count is not a playable-level cap.
|
||||
const currentAdventureCharacterLevelCap = progression.MaximumCharacterLevel
|
||||
|
||||
var (
|
||||
errCurrentAdventureRuntimeUnavailable = errors.New("current adventure-group runtime is unavailable")
|
||||
errCurrentAdventureRequestInvalid = errors.New("current adventure-group request is invalid")
|
||||
errCurrentAdventureStateInvalid = errors.New("current adventure-group state is invalid")
|
||||
)
|
||||
|
||||
type currentAdventureSelectedState struct {
|
||||
Repositories dnfrepo.Group
|
||||
AccountID string
|
||||
CharacterID string
|
||||
Character dnfrepo.CharacterRecord
|
||||
Account dnfrepo.AccountRecord
|
||||
Characters []dnfrepo.CharacterRecord
|
||||
Summary adventuregroup.Summary
|
||||
Config adventuregroup.RuntimeConfig
|
||||
Runtime adventuregroup.RuntimeState
|
||||
}
|
||||
|
||||
func (s *Service) currentAdventureSelectedState(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
) (currentAdventureSelectedState, error) {
|
||||
if s == nil || session == nil || session.selectedCharacterID == 0 {
|
||||
return currentAdventureSelectedState{}, errCurrentAdventureRuntimeUnavailable
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.Account == nil || repositories.Character == nil {
|
||||
return currentAdventureSelectedState{}, errCurrentAdventureRuntimeUnavailable
|
||||
}
|
||||
accountID := strings.TrimSpace(s.accountIDForSession(session))
|
||||
characterID := strconv.Itoa(int(session.selectedCharacterID))
|
||||
character, found, err := repositories.Character.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return currentAdventureSelectedState{}, err
|
||||
}
|
||||
if !found || strings.TrimSpace(character.AccountID) != accountID {
|
||||
return currentAdventureSelectedState{}, errCurrentAdventureStateInvalid
|
||||
}
|
||||
account, found, err := repositories.Account.Load(ctx, accountID)
|
||||
if err != nil {
|
||||
return currentAdventureSelectedState{}, err
|
||||
}
|
||||
if !found || strings.TrimSpace(account.AccountID) != accountID {
|
||||
return currentAdventureSelectedState{}, errCurrentAdventureStateInvalid
|
||||
}
|
||||
state, err := s.currentAccountAdventureGroupState(ctx, character, true, session)
|
||||
if err != nil {
|
||||
return currentAdventureSelectedState{}, err
|
||||
}
|
||||
config := s.adventureGroupTable.Runtime()
|
||||
runtime, err := adventuregroup.ParseRuntimeState(account, config, s.gameplayNow())
|
||||
if err != nil {
|
||||
return currentAdventureSelectedState{}, err
|
||||
}
|
||||
return currentAdventureSelectedState{
|
||||
Repositories: repositories,
|
||||
AccountID: accountID,
|
||||
CharacterID: characterID,
|
||||
Character: character,
|
||||
Account: account,
|
||||
Characters: state.Characters,
|
||||
Summary: state.Summary,
|
||||
Config: config,
|
||||
Runtime: runtime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func saveCurrentAdventureRuntime(
|
||||
ctx context.Context,
|
||||
state currentAdventureSelectedState,
|
||||
now time.Time,
|
||||
) error {
|
||||
value, err := state.Runtime.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state.Repositories.CharacterSettlement == nil {
|
||||
return errCurrentAdventureRuntimeUnavailable
|
||||
}
|
||||
return state.Repositories.CharacterSettlement.WithinCharacterSettlement(ctx, state.CharacterID, func(tx dnfrepo.Group) error {
|
||||
account, found, err := tx.Account.Load(ctx, state.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || account.AccountID != state.AccountID ||
|
||||
account.Metadata[adventuregroup.RuntimeStateMetadataKey] != state.Account.Metadata[adventuregroup.RuntimeStateMetadataKey] {
|
||||
return errCurrentAdventureStateInvalid
|
||||
}
|
||||
// Reject a stale expedition write if settlement or another session accrued currency.
|
||||
return dnfrepo.SaveAccountMetadataEntry(ctx, tx.Account, account, adventuregroup.RuntimeStateMetadataKey, value, now)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentAdventureExpeditionInfo(session *gameSession, request []byte) error {
|
||||
if len(request) < 1 || int(request[0])+1 != len(request) {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryInfo), currentAdventureGenericFailureCode)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
s.adventureGroupRuntimeMu.Lock()
|
||||
state, err := s.currentAdventureSelectedState(ctx, session)
|
||||
s.adventureGroupRuntimeMu.Unlock()
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-adventure-expedition-info-rejected", "error", err)
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryInfo), currentAdventureGenericFailureCode)
|
||||
}
|
||||
body := buildCurrentAdventureExpeditionStateBody(currentAdventureExpeditionProjection(state, s.gameplayNow()), s.gameplayNow())
|
||||
return s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketMercenaryInfo), body)
|
||||
}
|
||||
|
||||
type currentAdventureExpeditionStartRequest struct {
|
||||
Area byte
|
||||
DurationSeconds uint32
|
||||
Members []currentAdventureExpeditionMemberRequest
|
||||
}
|
||||
|
||||
type currentAdventureExpeditionMemberRequest struct {
|
||||
Selector byte
|
||||
Job byte
|
||||
GrowType byte
|
||||
}
|
||||
|
||||
func parseCurrentAdventureExpeditionStartRequest(body []byte) (currentAdventureExpeditionStartRequest, error) {
|
||||
if len(body) < 7 {
|
||||
return currentAdventureExpeditionStartRequest{}, errCurrentAdventureRequestInvalid
|
||||
}
|
||||
count := int(binary.LittleEndian.Uint16(body[5:7]))
|
||||
if count <= 0 || count > 4 || len(body) != 7+count*3 {
|
||||
return currentAdventureExpeditionStartRequest{}, errCurrentAdventureRequestInvalid
|
||||
}
|
||||
request := currentAdventureExpeditionStartRequest{
|
||||
Area: body[0],
|
||||
DurationSeconds: binary.LittleEndian.Uint32(body[1:5]),
|
||||
Members: make([]currentAdventureExpeditionMemberRequest, count),
|
||||
}
|
||||
for index := range request.Members {
|
||||
offset := 7 + index*3
|
||||
request.Members[index] = currentAdventureExpeditionMemberRequest{
|
||||
Selector: body[offset],
|
||||
Job: body[offset+1],
|
||||
GrowType: body[offset+2],
|
||||
}
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentAdventureExpeditionStart(session *gameSession, body []byte) error {
|
||||
request, err := parseCurrentAdventureExpeditionStartRequest(body)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetition), currentAdventureGenericFailureCode)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
now := s.gameplayNow()
|
||||
s.adventureGroupRuntimeMu.Lock()
|
||||
defer s.adventureGroupRuntimeMu.Unlock()
|
||||
state, err := s.currentAdventureSelectedState(ctx, session)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetition), currentAdventureGenericFailureCode)
|
||||
}
|
||||
area, found := state.Config.Area(request.Area)
|
||||
if !found || state.Summary.ManageLevel < area.RequiredManageLevel {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetition), 7)
|
||||
}
|
||||
if _, exists := state.Runtime.Expeditions[adventuregroup.ExpeditionKey(request.Area)]; exists {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetition), 14)
|
||||
}
|
||||
if _, found := area.RewardRates[request.DurationSeconds]; !found {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetition), 18)
|
||||
}
|
||||
members, rewardInputs, ok := resolveCurrentAdventureExpeditionMembers(
|
||||
request.Members,
|
||||
state.Characters,
|
||||
state.Runtime,
|
||||
)
|
||||
if !ok {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetition), 22)
|
||||
}
|
||||
// The designated attribute set comes from the same rotated snapshot the
|
||||
// client was shown, so the paid reward matches the preview.
|
||||
reward, ok := state.Config.ExpeditionReward(
|
||||
request.Area,
|
||||
request.DurationSeconds,
|
||||
rewardInputs,
|
||||
state.Config.AreaAttributes(request.Area, now),
|
||||
)
|
||||
if !ok {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetition), 22)
|
||||
}
|
||||
startedAt := now.Unix()
|
||||
state.Runtime.Expeditions[adventuregroup.ExpeditionKey(request.Area)] = adventuregroup.Expedition{
|
||||
Area: request.Area,
|
||||
State: 1,
|
||||
StartedAt: startedAt,
|
||||
EndsAt: startedAt + int64(request.DurationSeconds),
|
||||
Attributes: state.Config.AreaAttributes(request.Area, now),
|
||||
Members: members,
|
||||
Reward: reward,
|
||||
}
|
||||
if err := saveCurrentAdventureRuntime(ctx, state, now); err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetition), currentAdventureGenericFailureCode)
|
||||
}
|
||||
if err := s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketMercenaryCompetition), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.sendCurrentAdventureExpeditionStatePush(session, state, now)
|
||||
}
|
||||
|
||||
func resolveCurrentAdventureExpeditionMembers(
|
||||
requests []currentAdventureExpeditionMemberRequest,
|
||||
characters []dnfrepo.CharacterRecord,
|
||||
runtime adventuregroup.RuntimeState,
|
||||
) ([]adventuregroup.ExpeditionMember, []adventuregroup.ExpeditionMemberInput, bool) {
|
||||
used := make(map[byte]struct{}, len(requests))
|
||||
members := make([]adventuregroup.ExpeditionMember, 0, len(requests))
|
||||
inputs := make([]adventuregroup.ExpeditionMemberInput, 0, len(requests))
|
||||
for _, request := range requests {
|
||||
if _, duplicate := used[request.Selector]; duplicate {
|
||||
return nil, nil, false
|
||||
}
|
||||
used[request.Selector] = struct{}{}
|
||||
var character dnfrepo.CharacterRecord
|
||||
found := false
|
||||
for _, candidate := range characters {
|
||||
// The selector is a roster slot. A character whose numeric ID
|
||||
// happens to equal it is a different character, and a stale
|
||||
// selector left over from a roster reorder must not resolve.
|
||||
if candidate.Slot == int(request.Selector) {
|
||||
character = candidate
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
job := byte(numericCharacterStat(character.Job))
|
||||
grow := byte(numericCharacterStatValue(character, "grow_type")) & 0x0f
|
||||
if !found || job != request.Job || grow != request.GrowType || character.Level <= 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
// Busy is decided by durable identity, so reordering the roster cannot
|
||||
// let the same character be sent out twice.
|
||||
if currentAdventureExpeditionCharacterBusy(runtime, character) {
|
||||
return nil, nil, false
|
||||
}
|
||||
members = append(members, adventuregroup.ExpeditionMember{
|
||||
CharacterID: uint16(request.Selector),
|
||||
PersistentCharacterID: character.CharacterID,
|
||||
Name: character.Name,
|
||||
Level: uint32(character.Level),
|
||||
Job: job,
|
||||
GrowType: grow,
|
||||
})
|
||||
inputs = append(inputs, adventuregroup.ExpeditionMemberInput{
|
||||
Level: character.Level, Job: job, GrowType: grow,
|
||||
})
|
||||
}
|
||||
return members, inputs, true
|
||||
}
|
||||
|
||||
// currentAdventureExpeditionCharacterBusy reports whether this character is
|
||||
// already sent out. It matches on the durable character ID first, and falls
|
||||
// back to the profession row because the native candidate table holds one row
|
||||
// per profession and old saves only recorded that pair.
|
||||
func currentAdventureExpeditionCharacterBusy(runtime adventuregroup.RuntimeState, character dnfrepo.CharacterRecord) bool {
|
||||
for _, expedition := range runtime.Expeditions {
|
||||
for _, member := range expedition.Members {
|
||||
if member.PersistentCharacterID == character.CharacterID ||
|
||||
(member.Job == byte(numericCharacterStat(character.Job)) &&
|
||||
member.GrowType&0x0f == byte(numericCharacterStatValue(character, "grow_type"))&0x0f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// currentAdventureExpeditionProjection builds the state the client reads. The
|
||||
// native sub_D44860 accepts area 0 as the idle-candidate table and rebuilds the
|
||||
// whole list from it, so every eligible character is published there; without
|
||||
// it the expedition window has no characters to offer.
|
||||
func currentAdventureExpeditionProjection(state currentAdventureSelectedState, now time.Time) adventuregroup.RuntimeState {
|
||||
projection := state.Runtime
|
||||
projection.Expeditions = make(map[string]adventuregroup.Expedition, len(state.Config.ExpeditionAreas)+1)
|
||||
idle := adventuregroup.Expedition{}
|
||||
for _, character := range state.Characters {
|
||||
if character.Slot < 0 || character.Slot > math.MaxUint8 ||
|
||||
currentAdventureExpeditionCharacterBusy(state.Runtime, character) {
|
||||
continue
|
||||
}
|
||||
idle.Members = append(idle.Members, adventuregroup.ExpeditionMember{
|
||||
CharacterID: uint16(character.Slot),
|
||||
PersistentCharacterID: character.CharacterID,
|
||||
Name: character.Name,
|
||||
Level: uint32(character.Level),
|
||||
Job: byte(numericCharacterStat(character.Job)),
|
||||
GrowType: byte(numericCharacterStatValue(character, "grow_type")) & 0x0f,
|
||||
})
|
||||
}
|
||||
projection.Expeditions["0"] = idle
|
||||
for _, area := range state.Config.ExpeditionAreas {
|
||||
key := adventuregroup.ExpeditionKey(area.Index)
|
||||
expedition, exists := state.Runtime.Expeditions[key]
|
||||
if !exists {
|
||||
expedition = adventuregroup.Expedition{Area: area.Index, Attributes: state.Config.AreaAttributes(area.Index, now)}
|
||||
}
|
||||
projection.Expeditions[key] = expedition
|
||||
}
|
||||
return projection
|
||||
}
|
||||
|
||||
func (s *Service) refreshCurrentAdventureExpeditionState(ctx context.Context, session *gameSession) error {
|
||||
// Representative ACKs update the collection only; op1342 rebuilds the cache.
|
||||
state, err := s.currentAdventureSelectedState(ctx, session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.sendCurrentAdventureExpeditionStatePush(session, state, s.gameplayNow())
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentAdventureExpeditionCancel(session *gameSession, body []byte) error {
|
||||
if len(body) != 1 {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetitionCancle), currentAdventureGenericFailureCode)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
now := s.gameplayNow()
|
||||
s.adventureGroupRuntimeMu.Lock()
|
||||
defer s.adventureGroupRuntimeMu.Unlock()
|
||||
state, err := s.currentAdventureSelectedState(ctx, session)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetitionCancle), currentAdventureGenericFailureCode)
|
||||
}
|
||||
key := adventuregroup.ExpeditionKey(body[0])
|
||||
if _, found := state.Runtime.Expeditions[key]; !found {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetitionCancle), currentAdventureGenericFailureCode)
|
||||
}
|
||||
delete(state.Runtime.Expeditions, key)
|
||||
if err := saveCurrentAdventureRuntime(ctx, state, now); err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetitionCancle), currentAdventureGenericFailureCode)
|
||||
}
|
||||
if err := s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketMercenaryCompetitionCancle), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.sendCurrentAdventureExpeditionStatePush(session, state, now)
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentAdventureExpeditionReward(session *gameSession, body []byte) error {
|
||||
if len(body) != 1 {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetitionRewardRequest), currentAdventureGenericFailureCode)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
now := s.gameplayNow()
|
||||
s.adventureGroupRuntimeMu.Lock()
|
||||
defer s.adventureGroupRuntimeMu.Unlock()
|
||||
state, err := s.currentAdventureSelectedState(ctx, session)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetitionRewardRequest), currentAdventureGenericFailureCode)
|
||||
}
|
||||
key := adventuregroup.ExpeditionKey(body[0])
|
||||
expedition, found := state.Runtime.Expeditions[key]
|
||||
if !found || expedition.State == 0 || expedition.EndsAt > now.Unix() || expedition.EndsAt <= expedition.StartedAt || expedition.EndsAt-expedition.StartedAt > math.MaxUint32 {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetitionRewardRequest), currentAdventureGenericFailureCode)
|
||||
}
|
||||
// Recompute old saved estimates with the native formula and the departure
|
||||
// attribute snapshot, so pending expeditions also stop using level * rate.
|
||||
rewardInputs := make([]adventuregroup.ExpeditionMemberInput, 0, len(expedition.Members))
|
||||
for _, member := range expedition.Members {
|
||||
rewardInputs = append(rewardInputs, adventuregroup.ExpeditionMemberInput{Level: int(member.Level), Job: member.Job, GrowType: member.GrowType})
|
||||
}
|
||||
reward, ok := state.Config.ExpeditionReward(expedition.Area, uint32(expedition.EndsAt-expedition.StartedAt), rewardInputs, expedition.Attributes)
|
||||
if !ok {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetitionRewardRequest), currentAdventureGenericFailureCode)
|
||||
}
|
||||
delete(state.Runtime.Expeditions, key)
|
||||
previousGlory := state.Runtime.ShopPoints[adventuregroup.ShopPointGlory]
|
||||
state.Runtime.AddGlory(state.Config, reward)
|
||||
if err := saveCurrentAdventureRuntime(ctx, state, now); err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryCompetitionRewardRequest), currentAdventureGenericFailureCode)
|
||||
}
|
||||
s.logGameEvent(session, "game-adventure-expedition-reward-committed",
|
||||
"area", expedition.Area, "duration_seconds", expedition.EndsAt-expedition.StartedAt,
|
||||
"members", len(expedition.Members), "previous_estimate", expedition.Reward,
|
||||
"native_reward", reward, "granted", state.Runtime.ShopPoints[adventuregroup.ShopPointGlory]-previousGlory)
|
||||
var response packetWriter
|
||||
response.writeUint32(state.Runtime.ShopPoints[adventuregroup.ShopPointGlory] - previousGlory)
|
||||
if err := s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketMercenaryCompetitionRewardRequest), response.bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendCurrentAdventureExpeditionStatePush(session, state, now); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.sendCurrentAdventureInfoPushFromAccount(session, session.selectedCharacterID, "expedition_reward")
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentAdventurePointRecalculate(session *gameSession, body []byte) error {
|
||||
if len(body) != 0 {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryPointRecalculate), currentAdventureGenericFailureCode)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
s.adventureGroupRuntimeMu.Lock()
|
||||
state, err := s.currentAdventureSelectedState(ctx, session)
|
||||
s.adventureGroupRuntimeMu.Unlock()
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketMercenaryPointRecalculate), currentAdventureGenericFailureCode)
|
||||
}
|
||||
var response packetWriter
|
||||
response.writeUint32(state.Runtime.ShopPoints[adventuregroup.ShopPointGlory])
|
||||
response.writeUint32(state.Runtime.ShopPoints[adventuregroup.ShopPointBrave])
|
||||
response.writeByte(0)
|
||||
return s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketMercenaryPointRecalculate), response.bytes())
|
||||
}
|
||||
|
||||
func buildCurrentAdventureExpeditionStateBody(state adventuregroup.RuntimeState, now time.Time) []byte {
|
||||
keys := make([]int, 0, len(state.Expeditions))
|
||||
for key := range state.Expeditions {
|
||||
value, err := strconv.Atoi(key)
|
||||
// Area 0 is the native idle-candidate table the client reads to list
|
||||
// the characters it may send out; filtering it out leaves that list
|
||||
// empty and the expedition window shows no characters at all.
|
||||
if err == nil && value >= 0 && value <= math.MaxUint8 {
|
||||
keys = append(keys, value)
|
||||
}
|
||||
}
|
||||
sort.Ints(keys)
|
||||
if len(keys) > math.MaxUint8 {
|
||||
keys = keys[:math.MaxUint8]
|
||||
}
|
||||
var writer packetWriter
|
||||
writer.writeByte(byte(len(keys)))
|
||||
for _, areaValue := range keys {
|
||||
expedition := state.Expeditions[strconv.Itoa(areaValue)]
|
||||
expeditionState := expedition.State
|
||||
// The idle table carries state 0 with no timestamps; only a real
|
||||
// expedition can expire.
|
||||
if expedition.State != 0 && expedition.EndsAt <= now.Unix() {
|
||||
expeditionState = 2
|
||||
}
|
||||
writer.writeByte(byte(areaValue))
|
||||
writer.writeByte(expeditionState)
|
||||
writer.writeUint32(clampAdventureUnix(expedition.StartedAt))
|
||||
writer.writeUint32(clampAdventureUnix(expedition.EndsAt))
|
||||
attributeCount := len(expedition.Attributes)
|
||||
if attributeCount > math.MaxUint8 {
|
||||
attributeCount = math.MaxUint8
|
||||
}
|
||||
writer.writeByte(byte(attributeCount))
|
||||
for _, attribute := range expedition.Attributes[:attributeCount] {
|
||||
writer.writeByte(attribute)
|
||||
}
|
||||
memberCount := len(expedition.Members)
|
||||
if memberCount > math.MaxUint16 {
|
||||
memberCount = math.MaxUint16
|
||||
}
|
||||
writer.writeUint16(uint16(memberCount))
|
||||
for _, member := range expedition.Members[:memberCount] {
|
||||
writer.writeUint16(member.CharacterID)
|
||||
writer.writeRawDstr(rosterNameBytes(member.Name))
|
||||
writer.writeUint32(member.Level)
|
||||
writer.writeByte(member.Job)
|
||||
writer.writeByte(member.GrowType)
|
||||
writer.writeByte(member.Status)
|
||||
}
|
||||
}
|
||||
return writer.bytes()
|
||||
}
|
||||
|
||||
func clampAdventureUnix(value int64) uint32 {
|
||||
if value <= 0 {
|
||||
return 0
|
||||
}
|
||||
if value > math.MaxUint32 {
|
||||
return math.MaxUint32
|
||||
}
|
||||
return uint32(value)
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentAdventureExpeditionStatePush(
|
||||
session *gameSession,
|
||||
selected currentAdventureSelectedState,
|
||||
now time.Time,
|
||||
) error {
|
||||
var body packetWriter
|
||||
body.writeByte(1)
|
||||
body.writeBytes(buildCurrentAdventureExpeditionStateBody(currentAdventureExpeditionProjection(selected, now), now))
|
||||
return s.sendGameUpperRawClass(session, 1342, body.bytes(), 0)
|
||||
}
|
||||
|
||||
type currentAdventureShopCommit struct {
|
||||
Inventory dnfrepo.InventoryRecord
|
||||
// AccountInventory / AccountSlots 是账号共享仓那一侧(无色小晶块、灵魂落 354..365)。
|
||||
AccountInventory dnfrepo.AccountInventoryRecord
|
||||
AccountSlots []uint16
|
||||
Slots []uint16
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentAdventureShopPurchase(session *gameSession, body []byte) error {
|
||||
if len(body) != 5 {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventurerShopPurchase), currentAdventureGenericFailureCode)
|
||||
}
|
||||
categoryIndex := body[0]
|
||||
selector := binary.LittleEndian.Uint32(body[1:5])
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
now := s.gameplayNow()
|
||||
|
||||
s.adventureGroupRuntimeMu.Lock()
|
||||
defer s.adventureGroupRuntimeMu.Unlock()
|
||||
state, err := s.currentAdventureSelectedState(ctx, session)
|
||||
if err != nil || state.Repositories.CeraShopAssets == nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventurerShopPurchase), currentAdventureGenericFailureCode)
|
||||
}
|
||||
category, found := state.Config.Shop(categoryIndex)
|
||||
if !found {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventurerShopPurchase), currentAdventureGenericFailureCode)
|
||||
}
|
||||
product, found := resolveCurrentAdventureShopProduct(category, selector)
|
||||
if !found {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventurerShopPurchase), currentAdventureGenericFailureCode)
|
||||
}
|
||||
if state.Summary.ManageLevel < product.RequiredManageLevel {
|
||||
// 客户端 235 = "冒险团Lv较低。"(0x00C79A2B)。回笼统码会让它什么都不弹。
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventurerShopPurchase), currentAdventureShopLevelTooLowCode)
|
||||
}
|
||||
catalog, err := s.currentPVFItemCatalog()
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventurerShopPurchase), currentAdventureGenericFailureCode)
|
||||
}
|
||||
definition, err := catalog.ResolveItem(product.ItemID)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventurerShopPurchase), currentAdventureGenericFailureCode)
|
||||
}
|
||||
var result currentAdventureShopCommit
|
||||
err = state.Repositories.CeraShopAssets.WithinCeraShopAssets(
|
||||
ctx,
|
||||
state.AccountID,
|
||||
state.CharacterID,
|
||||
"adventure_group_shop:"+state.CharacterID,
|
||||
func(
|
||||
accounts dnfrepo.AccountRepository,
|
||||
accountInventories dnfrepo.AccountInventoryRepository,
|
||||
characters dnfrepo.CharacterRepository,
|
||||
inventories dnfrepo.InventoryRepository,
|
||||
_ dnfrepo.EquipmentRepository,
|
||||
_ dnfrepo.SettingsRepository,
|
||||
) error {
|
||||
account, found, err := accounts.Load(ctx, state.AccountID)
|
||||
if err != nil || !found {
|
||||
return errCurrentAdventureStateInvalid
|
||||
}
|
||||
character, found, err := characters.Load(ctx, state.CharacterID)
|
||||
if err != nil || !found || strings.TrimSpace(character.AccountID) != state.AccountID {
|
||||
return errCurrentAdventureStateInvalid
|
||||
}
|
||||
inventory, found, err := inventories.Load(ctx, state.CharacterID)
|
||||
if err != nil || !found {
|
||||
return errCurrentAdventureStateInvalid
|
||||
}
|
||||
runtime, err := adventuregroup.ParseRuntimeState(account, state.Config, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runtime.Spend(category, product); err != nil {
|
||||
return err
|
||||
}
|
||||
// 账号共享优先:冒险团商店直接卖传说灵魂(PVF [adventurer shop purchase info]
|
||||
// 里的 `10099775 15 70 3 3`),它的家是账号共享仓的 365 格。
|
||||
// 账号记录与本事务同生共死(扣点、发货一起提交)。
|
||||
accountInventory, accountFound, err := accountInventories.Load(ctx, state.AccountID)
|
||||
if err != nil {
|
||||
return errCurrentAdventureStateInvalid
|
||||
}
|
||||
if !accountFound {
|
||||
accountInventory = dnfrepo.AccountInventoryRecord{AccountID: state.AccountID}
|
||||
}
|
||||
accountInventory = dnfrepo.CloneAccountInventory(accountInventory)
|
||||
accountInventory.AccountID = state.AccountID
|
||||
if accountInventory.Slots == nil {
|
||||
accountInventory.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
var accountSlot int16
|
||||
shared := grantCurrentAccountSharedFirst(&accountInventory, definition, 1, "adventure_shop")
|
||||
if shared.Changed {
|
||||
accountSlot = shared.Slot
|
||||
}
|
||||
var slots []uint16
|
||||
if shared.Remainder > 0 {
|
||||
bagSlots, err := grantCurrentCeraShopProduct(&inventory, definition, shared.Remainder, s.currentInventoryPlacementMainSlotCount(ctx, &inventory, ""))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slots = append(slots, bagSlots...)
|
||||
}
|
||||
if accountSlot == 0 && len(slots) == 0 {
|
||||
return errCurrentAdventureStateInvalid
|
||||
}
|
||||
encoded, err := runtime.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account = dnfrepo.CloneAccount(account)
|
||||
if account.Metadata == nil {
|
||||
account.Metadata = make(map[string]string)
|
||||
}
|
||||
account.Metadata[adventuregroup.RuntimeStateMetadataKey] = encoded
|
||||
account.UpdatedAt = now.UTC()
|
||||
inventory.UpdatedAt = now.UTC()
|
||||
if err := accounts.Save(ctx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
if accountSlot != 0 {
|
||||
accountInventory.UpdatedAt = now.UTC()
|
||||
if err := accountInventories.Save(ctx, accountInventory); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := inventories.Save(ctx, inventory); err != nil {
|
||||
return err
|
||||
}
|
||||
result = currentAdventureShopCommit{
|
||||
Inventory: dnfrepo.CloneInventory(inventory),
|
||||
AccountInventory: dnfrepo.CloneAccountInventory(accountInventory),
|
||||
Slots: append([]uint16(nil), slots...),
|
||||
}
|
||||
if accountSlot != 0 {
|
||||
result.AccountSlots = []uint16{uint16(accountSlot)}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
failureCode := currentAdventureShopFailureCode(err)
|
||||
s.logGameEvent(session, "game-adventure-shop-purchase-rejected",
|
||||
"category", categoryIndex, "selector", selector, "item_id", product.ItemID,
|
||||
"failure_code", failureCode, "error", err)
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventurerShopPurchase), failureCode)
|
||||
}
|
||||
if err := s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketAdventurerShopPurchase), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sendCurrentAdventureInventoryUpdates(s, session, result); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.sendCurrentAdventureInfoPushFromAccount(session, session.selectedCharacterID, "adventure_shop_purchase")
|
||||
}
|
||||
|
||||
func resolveCurrentAdventureShopProduct(category adventuregroup.ShopCategory, selector uint32) (adventuregroup.ShopProduct, bool) {
|
||||
for _, product := range category.Products {
|
||||
if product.ItemID == selector {
|
||||
return product, true
|
||||
}
|
||||
}
|
||||
if uint64(selector) < uint64(len(category.Products)) {
|
||||
return category.Products[selector], true
|
||||
}
|
||||
return adventuregroup.ShopProduct{}, false
|
||||
}
|
||||
|
||||
func sendCurrentAdventureInventoryUpdates(
|
||||
service *Service,
|
||||
session *gameSession,
|
||||
result currentAdventureShopCommit,
|
||||
) error {
|
||||
entries := make([]currentItemListEntry, 0, len(result.Slots)+len(result.AccountSlots))
|
||||
for _, slot := range result.Slots {
|
||||
signedSlot := int16(slot)
|
||||
stack, found := result.Inventory.Slots[currentDungeonPickupMainSlotKey(signedSlot)]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, currentItemListEntryFromStack(dnfrepo.MainInventoryListType, signedSlot, stack))
|
||||
}
|
||||
// 账号共享槽也是 list-0 线地址(354..365):不加这一行,客户端要等下一次
|
||||
// 列表刷新才看得到买到的灵魂。
|
||||
for _, slot := range result.AccountSlots {
|
||||
signedSlot := int16(slot)
|
||||
stack, found := result.AccountInventory.Slots[dnfrepo.AccountSharedInventorySlotKey(signedSlot)]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, currentItemListEntryFromStack(dnfrepo.MainInventoryListType, signedSlot, stack))
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
sortCurrentItemListEntries(entries)
|
||||
body := buildCurrentItemUpdateBody(dnfrepo.MainInventoryListType, entries)
|
||||
return service.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketWalkoutPartyMember), body, 0)
|
||||
}
|
||||
|
||||
type currentAdventureGrowthResult struct {
|
||||
PreviousLevel int
|
||||
Character dnfrepo.CharacterRecord
|
||||
Skill dnfrepo.SkillRecord
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentAdventureGrowthCapsule(session *gameSession, body []byte) error {
|
||||
if len(body) != 0 {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventureGrowthcapsuleExp), currentAdventureGenericFailureCode)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
now := s.gameplayNow()
|
||||
s.adventureGroupRuntimeMu.Lock()
|
||||
defer s.adventureGroupRuntimeMu.Unlock()
|
||||
state, err := s.currentAdventureSelectedState(ctx, session)
|
||||
if err != nil || state.Repositories.CharacterSettlement == nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventureGrowthcapsuleExp), currentAdventureGenericFailureCode)
|
||||
}
|
||||
s.initialEquipmentMu.Lock()
|
||||
archive, err := s.initialEquipmentArchiveLocked()
|
||||
s.initialEquipmentMu.Unlock()
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventureGrowthcapsuleExp), currentAdventureGenericFailureCode)
|
||||
}
|
||||
tables, err := progression.Load(ctx, archive)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventureGrowthcapsuleExp), currentAdventureGenericFailureCode)
|
||||
}
|
||||
preAwardExperience, present := state.Character.Stats["exp"]
|
||||
if !present || preAwardExperience < 0 || uint64(preAwardExperience) > math.MaxUint32 {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventureGrowthcapsuleExp), currentAdventureGenericFailureCode)
|
||||
}
|
||||
preAwardHonorExpertGain, err := currentHonorExpertExperienceGain(
|
||||
tables,
|
||||
state.Character.Level,
|
||||
uint32(preAwardExperience),
|
||||
state.Config.Capsule.GrantedExperience,
|
||||
)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventureGrowthcapsuleExp), currentAdventureGenericFailureCode)
|
||||
}
|
||||
var honorTables *dnfhonor.Tables
|
||||
if preAwardHonorExpertGain > 0 {
|
||||
honorTables, err = s.loadHonorTable(ctx)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventureGrowthcapsuleExp), currentAdventureGenericFailureCode)
|
||||
}
|
||||
}
|
||||
var result currentAdventureGrowthResult
|
||||
err = state.Repositories.CharacterSettlement.WithinCharacterSettlement(ctx, state.CharacterID, func(tx dnfrepo.Group) error {
|
||||
if tx.Account == nil {
|
||||
return errCurrentAdventureRuntimeUnavailable
|
||||
}
|
||||
account, found, err := tx.Account.Load(ctx, state.AccountID)
|
||||
if err != nil || !found {
|
||||
return errCurrentAdventureStateInvalid
|
||||
}
|
||||
character, found, err := tx.Character.Load(ctx, state.CharacterID)
|
||||
if err != nil || !found || strings.TrimSpace(character.AccountID) != state.AccountID {
|
||||
return errCurrentAdventureStateInvalid
|
||||
}
|
||||
skill, found, err := tx.Skill.Load(ctx, state.CharacterID)
|
||||
if err != nil || !found || skill.Points.SyncedLevel != character.Level {
|
||||
return errCurrentAdventureStateInvalid
|
||||
}
|
||||
capsule := state.Config.Capsule
|
||||
if character.Level < capsule.MinimumLevel || character.Level > capsule.MaximumLevel {
|
||||
return errCurrentAdventureRequestInvalid
|
||||
}
|
||||
runtime, err := adventuregroup.ParseRuntimeState(account, state.Config, now)
|
||||
if err != nil || !runtime.ConsumeCapsule(state.Config) {
|
||||
return errCurrentAdventureRequestInvalid
|
||||
}
|
||||
currentExperience := character.Stats["exp"]
|
||||
if currentExperience < 0 || uint64(currentExperience) > math.MaxUint32 {
|
||||
return errCurrentAdventureStateInvalid
|
||||
}
|
||||
plan, err := dnfdungeon.PlanSettlementProgressionAtCap(
|
||||
tables,
|
||||
character.Level,
|
||||
uint32(currentExperience),
|
||||
capsule.GrantedExperience,
|
||||
skill.Points,
|
||||
currentAdventureCharacterLevelCap,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
honorExpertGain, err := currentHonorExpertExperienceGain(
|
||||
tables,
|
||||
character.Level,
|
||||
uint32(currentExperience),
|
||||
capsule.GrantedExperience,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
previousLevel := character.Level
|
||||
character = dnfrepo.CloneCharacter(character)
|
||||
skill = dnfrepo.CloneSkill(skill)
|
||||
account = dnfrepo.CloneAccount(account)
|
||||
character.Level = plan.Experience.NewLevel
|
||||
character.Stats["exp"] = int64(plan.Experience.NewExperience)
|
||||
if honorExpertGain > 0 {
|
||||
nextHonor, err := planCurrentHonorExpertProgress(honorTables, character, honorExpertGain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range currentHonorExpertStats(nextHonor) {
|
||||
character.Stats[key] = value
|
||||
}
|
||||
}
|
||||
character.UpdatedAt = now.UTC()
|
||||
skill.Points = plan.SkillPoints.New
|
||||
skill.UpdatedAt = now.UTC()
|
||||
encoded, err := runtime.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account.Metadata == nil {
|
||||
account.Metadata = make(map[string]string)
|
||||
}
|
||||
account.Metadata[adventuregroup.RuntimeStateMetadataKey] = encoded
|
||||
account.UpdatedAt = now.UTC()
|
||||
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 err := tx.Account.Save(ctx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
result = currentAdventureGrowthResult{
|
||||
PreviousLevel: previousLevel,
|
||||
Character: character,
|
||||
Skill: skill,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-adventure-growth-capsule-rejected", "error", err)
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketAdventureGrowthcapsuleExp), currentAdventureGenericFailureCode)
|
||||
}
|
||||
if err := s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketAdventureGrowthcapsuleExp), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
characterBody := buildCurrentFinishLoadingCharacterStateBody(result.Character, result.Skill.Points)
|
||||
if err := s.sendGameUpperRawClass(session, currentDungeonCharacterStateMsgID, characterBody, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if result.Character.Level != result.PreviousLevel {
|
||||
if err := s.sendCurrentAcceptableQuestListForSession(
|
||||
session,
|
||||
"adventure_growth_capsule_after_atomic_level_change",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.sendCurrentAdventureInfoPushFromAccount(session, session.selectedCharacterID, "adventure_growth_capsule")
|
||||
}
|
||||
Reference in new issue
Block a user