服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
517 lines
21 KiB
Go
517 lines
21 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
|
dnfenum "longheng.io/server/internal/modules/dnf/dnfenum"
|
|
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
)
|
|
|
|
const (
|
|
currentCustomAbilitySetNotifyOpcode uint16 = 1278
|
|
currentCustomAbilitySetErrorGeneric byte = 1
|
|
currentCustomAbilitySetErrorMaterial byte = 8
|
|
currentCustomAbilitySetOptionLimit = 4
|
|
currentCustomAbilitySetNotificationSize = 12
|
|
currentCustomAbilitySetOptionsExtra = "custom_ability_set_options"
|
|
currentCustomAbilitySetRequiredPartsExtra = "custom_ability_set_required_parts"
|
|
currentCustomAbilitySetGradeExtra = "custom_ability_set_grade"
|
|
currentCustomAbilitySetPartIndexExtra = "custom_ability_set_part_index"
|
|
)
|
|
|
|
var (
|
|
errCurrentCustomAbilitySetInvalid = errors.New("current custom ability set request is invalid")
|
|
errCurrentCustomAbilitySetMaterial = errors.New("current custom ability set material is insufficient")
|
|
)
|
|
|
|
type currentCustomAbilitySetRequest struct {
|
|
options []byte
|
|
slots []int16
|
|
}
|
|
|
|
type currentCustomAbilitySetSnapshot struct {
|
|
recordID byte
|
|
requiredParts byte
|
|
options [5]byte
|
|
grade byte
|
|
partSetIndex byte
|
|
}
|
|
|
|
type currentCustomAbilitySetResult struct {
|
|
snapshot currentCustomAbilitySetSnapshot
|
|
materialID uint32
|
|
materialCost int64
|
|
materialRemaining int64
|
|
spentPoints int
|
|
refreshes []alignedcmd.ItemSlotRefresh
|
|
equipmentChanged bool
|
|
}
|
|
|
|
func decodeCurrentCustomAbilitySetRequest(body []byte) (currentCustomAbilitySetRequest, error) {
|
|
if len(body) < 1 {
|
|
return currentCustomAbilitySetRequest{}, errCurrentCustomAbilitySetInvalid
|
|
}
|
|
offset := 0
|
|
optionCount := int(body[offset])
|
|
offset++
|
|
if optionCount < 1 || optionCount > currentCustomAbilitySetOptionLimit || len(body) < offset+optionCount+1 {
|
|
return currentCustomAbilitySetRequest{}, errCurrentCustomAbilitySetInvalid
|
|
}
|
|
request := currentCustomAbilitySetRequest{options: append([]byte(nil), body[offset:offset+optionCount]...)}
|
|
offset += optionCount
|
|
equipmentCount := int(body[offset])
|
|
offset++
|
|
if equipmentCount < 3 || equipmentCount > 6 || len(body) != offset+equipmentCount*2 {
|
|
return currentCustomAbilitySetRequest{}, errCurrentCustomAbilitySetInvalid
|
|
}
|
|
seenOptions := make(map[byte]struct{}, optionCount)
|
|
for _, optionID := range request.options {
|
|
if optionID == 0 {
|
|
return currentCustomAbilitySetRequest{}, errCurrentCustomAbilitySetInvalid
|
|
}
|
|
if _, duplicate := seenOptions[optionID]; duplicate {
|
|
return currentCustomAbilitySetRequest{}, errCurrentCustomAbilitySetInvalid
|
|
}
|
|
seenOptions[optionID] = struct{}{}
|
|
}
|
|
seenSlots := make(map[int16]struct{}, equipmentCount)
|
|
request.slots = make([]int16, 0, equipmentCount)
|
|
for index := 0; index < equipmentCount; index++ {
|
|
rawSlot := binary.LittleEndian.Uint16(body[offset : offset+2])
|
|
offset += 2
|
|
if rawSlot > math.MaxInt16 {
|
|
return currentCustomAbilitySetRequest{}, errCurrentCustomAbilitySetInvalid
|
|
}
|
|
slot := int16(rawSlot)
|
|
if _, duplicate := seenSlots[slot]; duplicate {
|
|
return currentCustomAbilitySetRequest{}, errCurrentCustomAbilitySetInvalid
|
|
}
|
|
seenSlots[slot] = struct{}{}
|
|
request.slots = append(request.slots, slot)
|
|
}
|
|
return request, nil
|
|
}
|
|
|
|
func buildCurrentCustomAbilitySetFailureBody(err error) []byte {
|
|
code := currentCustomAbilitySetErrorGeneric
|
|
if errors.Is(err, errCurrentCustomAbilitySetMaterial) {
|
|
code = currentCustomAbilitySetErrorMaterial
|
|
}
|
|
return []byte{0, code}
|
|
}
|
|
|
|
func buildCurrentCustomAbilitySetSuccessBody() []byte { return []byte{1} }
|
|
|
|
func buildCurrentCustomAbilitySetNotificationBody(actorID uint32, snapshot currentCustomAbilitySetSnapshot) []byte {
|
|
body := make([]byte, currentCustomAbilitySetNotificationSize)
|
|
binary.LittleEndian.PutUint32(body[0:4], actorID)
|
|
body[4] = snapshot.recordID
|
|
body[5] = snapshot.requiredParts
|
|
copy(body[6:11], snapshot.options[:])
|
|
body[11] = snapshot.grade
|
|
return body
|
|
}
|
|
|
|
func currentCustomAbilitySetOptionsText(options []byte) string {
|
|
values := make([]string, 0, len(options))
|
|
for _, optionID := range options {
|
|
if optionID == 0 {
|
|
break
|
|
}
|
|
values = append(values, strconv.Itoa(int(optionID)))
|
|
}
|
|
return strings.Join(values, ",")
|
|
}
|
|
|
|
func cloneCurrentCustomAbilityExtra(extra map[string]string) map[string]string {
|
|
clone := make(map[string]string, len(extra)+5)
|
|
for key, value := range extra {
|
|
clone[key] = value
|
|
}
|
|
return clone
|
|
}
|
|
|
|
func applyCurrentCustomAbilitySetMembership(raw []byte, extra map[string]string, recordID byte, snapshot *currentCustomAbilitySetSnapshot) ([]byte, map[string]string, error) {
|
|
if len(raw) != currentItemListEntryWireSize {
|
|
return nil, nil, errCurrentCustomAbilitySetInvalid
|
|
}
|
|
raw = append([]byte(nil), raw...)
|
|
raw[currentCustomAbilitySetRecordOffset] = recordID
|
|
extra = cloneCurrentCustomAbilityExtra(extra)
|
|
if recordID == 0 || snapshot == nil {
|
|
delete(extra, currentCustomAbilitySetOptionsExtra)
|
|
delete(extra, currentCustomAbilitySetRequiredPartsExtra)
|
|
delete(extra, currentCustomAbilitySetGradeExtra)
|
|
delete(extra, currentCustomAbilitySetPartIndexExtra)
|
|
} else {
|
|
extra[currentCustomAbilitySetOptionsExtra] = currentCustomAbilitySetOptionsText(snapshot.options[:])
|
|
extra[currentCustomAbilitySetRequiredPartsExtra] = strconv.Itoa(int(snapshot.requiredParts))
|
|
extra[currentCustomAbilitySetGradeExtra] = strconv.Itoa(int(snapshot.grade))
|
|
extra[currentCustomAbilitySetPartIndexExtra] = strconv.Itoa(int(snapshot.partSetIndex))
|
|
}
|
|
extra["custom_ability_set_record"] = strconv.Itoa(int(recordID))
|
|
tailHex := fmt.Sprintf("%x", raw[currentCustomAbilityRawBlockStart:currentCustomAbilityRawBlockEnd])
|
|
extra["tail_data_72"] = tailHex
|
|
for _, key := range []string{"mode1_raw_e_hex", "mode1_state_188_192_raw_hex"} {
|
|
if _, exists := extra[key]; exists {
|
|
extra[key] = tailHex
|
|
}
|
|
}
|
|
return raw, extra, nil
|
|
}
|
|
|
|
func validateCurrentCustomAbilitySetProfiles(profiles []currentCustomAbilityItemProfile) error {
|
|
occupied := make(map[int16]struct{}, len(profiles))
|
|
armorProfiles := make([]currentCustomAbilityItemProfile, 0, 5)
|
|
for _, profile := range profiles {
|
|
if !profile.applicable || profile.optionCount != 2 || profile.equipmentSlot < 0 {
|
|
return errCurrentCustomAbilitySetInvalid
|
|
}
|
|
if _, duplicate := occupied[profile.equipmentSlot]; duplicate {
|
|
return errCurrentCustomAbilitySetInvalid
|
|
}
|
|
occupied[profile.equipmentSlot] = struct{}{}
|
|
if profile.equipmentSlot >= 14 && profile.equipmentSlot <= 18 {
|
|
armorProfiles = append(armorProfiles, profile)
|
|
}
|
|
}
|
|
if len(armorProfiles) == 5 {
|
|
material := armorProfiles[0].subType
|
|
if !armorProfiles[0].hasSubType {
|
|
return errCurrentCustomAbilitySetInvalid
|
|
}
|
|
for _, profile := range armorProfiles[1:] {
|
|
if !profile.hasSubType || profile.subType != material {
|
|
return errCurrentCustomAbilitySetInvalid
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func commitCurrentCustomAbilitySet(ctx context.Context, repositories dnfrepo.Group, characterID string, request currentCustomAbilitySetRequest, individualCatalog *currentCustomAbilityCatalog, catalog *currentCustomAbilitySetCatalog, resolve currentCustomAbilityProfileResolver) (currentCustomAbilitySetResult, error) {
|
|
if repositories.CharacterItems == nil || individualCatalog == nil || catalog == nil || resolve == nil || len(request.options) < 1 || len(request.options) > int(catalog.maxOptions) || len(request.slots) < int(catalog.minParts) || len(request.slots) > int(catalog.maxParts) {
|
|
return currentCustomAbilitySetResult{}, errCurrentCustomAbilitySetInvalid
|
|
}
|
|
|
|
var result currentCustomAbilitySetResult
|
|
err := repositories.CharacterItems.WithinCharacterItems(ctx, characterID, func(inventoryRepo dnfrepo.InventoryRepository, equipmentRepo dnfrepo.EquipmentRepository) error {
|
|
inventory, found, err := inventoryRepo.Load(ctx, characterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
return errCurrentCustomAbilitySetInvalid
|
|
}
|
|
inventory = dnfrepo.CloneInventory(inventory)
|
|
equipment, equipmentFound, err := equipmentRepo.Load(ctx, characterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !equipmentFound {
|
|
equipment = dnfrepo.EquipmentRecord{CharacterID: characterID}
|
|
} else {
|
|
equipment = dnfrepo.CloneEquipment(equipment)
|
|
}
|
|
|
|
profiles := make([]currentCustomAbilityItemProfile, 0, len(request.slots))
|
|
grades := make([]byte, 0, len(request.slots))
|
|
selectedKeys := make(map[string]struct{}, len(request.slots))
|
|
for _, slot := range request.slots {
|
|
key := currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, slot)
|
|
stack, found := inventory.Slots[key]
|
|
if !found || stack.ItemID <= 0 || stack.ItemID > math.MaxUint32 || stack.Count != 1 || len(stack.RawEntry) != currentItemListEntryWireSize {
|
|
return errCurrentCustomAbilitySetInvalid
|
|
}
|
|
profile, err := resolve(uint32(stack.ItemID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !profile.applicable || profile.partSetIndex != catalog.partSetIndex {
|
|
return errCurrentCustomAbilitySetInvalid
|
|
}
|
|
state, valid := currentCustomAbilityStateFromRaw(stack.RawEntry, individualCatalog)
|
|
if !valid {
|
|
return errCurrentCustomAbilitySetInvalid
|
|
}
|
|
profiles = append(profiles, profile)
|
|
grades = append(grades, state.grade)
|
|
selectedKeys[key] = struct{}{}
|
|
}
|
|
if err := validateCurrentCustomAbilitySetProfiles(profiles); err != nil {
|
|
return err
|
|
}
|
|
setGrade, spentPoints, materialCost, err := catalog.evaluate(request.options, grades)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
materialSlots := currentCompoundSlotsForItem(inventory.Slots, int64(catalog.materialID))
|
|
available := int64(0)
|
|
for _, slot := range materialSlots {
|
|
available += inventory.Slots[currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, slot)].Count
|
|
}
|
|
if available < materialCost {
|
|
return errCurrentCustomAbilitySetMaterial
|
|
}
|
|
|
|
var snapshot currentCustomAbilitySetSnapshot
|
|
snapshot.recordID = catalog.partSetIndex
|
|
snapshot.partSetIndex = catalog.partSetIndex
|
|
snapshot.requiredParts = byte(len(request.slots))
|
|
snapshot.grade = setGrade
|
|
copy(snapshot.options[:], request.options)
|
|
|
|
refreshSet := make(map[int16]struct{})
|
|
equipmentChanged := false
|
|
for key, stack := range inventory.Slots {
|
|
if stack.Count != 1 || stack.ItemID <= 0 || stack.ItemID > math.MaxUint32 || len(stack.RawEntry) != currentItemListEntryWireSize || stack.RawEntry[currentCustomAbilitySetRecordOffset] != catalog.partSetIndex {
|
|
continue
|
|
}
|
|
listType, slot, ok := parseSceneInventorySlotKey(key)
|
|
if !ok || listType != dnfrepo.MainInventoryListType {
|
|
continue
|
|
}
|
|
profile, profileErr := resolve(uint32(stack.ItemID))
|
|
if profileErr != nil {
|
|
return profileErr
|
|
}
|
|
if !profile.applicable || profile.partSetIndex != catalog.partSetIndex {
|
|
continue
|
|
}
|
|
stack.RawEntry, stack.Extra, err = applyCurrentCustomAbilitySetMembership(stack.RawEntry, stack.Extra, 0, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inventory.Slots[key] = stack
|
|
refreshSet[slot] = struct{}{}
|
|
}
|
|
for key, equipped := range equipment.Entries {
|
|
if equipped.ItemID <= 0 || equipped.ItemID > math.MaxUint32 || len(equipped.RawEntry) != currentItemListEntryWireSize || equipped.RawEntry[currentCustomAbilitySetRecordOffset] != catalog.partSetIndex {
|
|
continue
|
|
}
|
|
profile, profileErr := resolve(uint32(equipped.ItemID))
|
|
if profileErr != nil {
|
|
return profileErr
|
|
}
|
|
if !profile.applicable || profile.partSetIndex != catalog.partSetIndex {
|
|
continue
|
|
}
|
|
equipped.RawEntry, equipped.Extra, err = applyCurrentCustomAbilitySetMembership(equipped.RawEntry, equipped.Extra, 0, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
equipment.Entries[key] = equipped
|
|
equipmentChanged = true
|
|
}
|
|
|
|
for key := range selectedKeys {
|
|
stack := inventory.Slots[key]
|
|
stack.RawEntry, stack.Extra, err = applyCurrentCustomAbilitySetMembership(stack.RawEntry, stack.Extra, catalog.partSetIndex, &snapshot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inventory.Slots[key] = stack
|
|
_, slot, _ := parseSceneInventorySlotKey(key)
|
|
refreshSet[slot] = struct{}{}
|
|
}
|
|
|
|
left := materialCost
|
|
for _, slot := range materialSlots {
|
|
if left == 0 {
|
|
break
|
|
}
|
|
key := currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, slot)
|
|
stack := inventory.Slots[key]
|
|
take := stack.Count
|
|
if take > left {
|
|
take = left
|
|
}
|
|
stack.Count -= take
|
|
left -= take
|
|
if stack.Count == 0 {
|
|
delete(inventory.Slots, key)
|
|
} else {
|
|
entry := currentItemListEntryFromStack(dnfrepo.MainInventoryListType, slot, stack)
|
|
stack.RawEntry = append([]byte(nil), entry.data[:]...)
|
|
inventory.Slots[key] = stack
|
|
}
|
|
refreshSet[slot] = struct{}{}
|
|
}
|
|
|
|
inventory.UpdatedAt = time.Now().UTC()
|
|
if err := dnfrepo.SaveInventoryFields(ctx, inventoryRepo, inventory, dnfrepo.InventoryFieldSlots); err != nil {
|
|
return err
|
|
}
|
|
if equipmentChanged {
|
|
equipment.UpdatedAt = time.Now().UTC()
|
|
if err := dnfrepo.SaveEquipmentFields(ctx, equipmentRepo, equipment, dnfrepo.EquipmentFieldEntries); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
refreshes := make([]alignedcmd.ItemSlotRefresh, 0, len(refreshSet))
|
|
for slot := range refreshSet {
|
|
refreshes = append(refreshes, alignedcmd.ItemSlotRefresh{ListType: dnfrepo.MainInventoryListType, SlotIndex: slot})
|
|
}
|
|
sort.Slice(refreshes, func(i, j int) bool { return refreshes[i].SlotIndex < refreshes[j].SlotIndex })
|
|
result = currentCustomAbilitySetResult{snapshot: snapshot, materialID: catalog.materialID, materialCost: materialCost, materialRemaining: available - materialCost, spentPoints: spentPoints, refreshes: refreshes, equipmentChanged: equipmentChanged}
|
|
return nil
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
func parseCurrentCustomAbilitySetSnapshot(extra map[string]string, recordID byte) (currentCustomAbilitySetSnapshot, bool) {
|
|
if recordID == 0 || extra == nil {
|
|
return currentCustomAbilitySetSnapshot{}, false
|
|
}
|
|
parts := strings.Split(strings.TrimSpace(extra[currentCustomAbilitySetOptionsExtra]), ",")
|
|
if len(parts) < 1 || len(parts) > 5 {
|
|
return currentCustomAbilitySetSnapshot{}, false
|
|
}
|
|
var snapshot currentCustomAbilitySetSnapshot
|
|
snapshot.recordID = recordID
|
|
seen := make(map[byte]struct{}, len(parts))
|
|
for index, value := range parts {
|
|
parsed, err := strconv.ParseUint(strings.TrimSpace(value), 10, 8)
|
|
if err != nil || parsed == 0 {
|
|
return currentCustomAbilitySetSnapshot{}, false
|
|
}
|
|
optionID := byte(parsed)
|
|
if _, duplicate := seen[optionID]; duplicate {
|
|
return currentCustomAbilitySetSnapshot{}, false
|
|
}
|
|
seen[optionID] = struct{}{}
|
|
snapshot.options[index] = optionID
|
|
}
|
|
required, requiredErr := strconv.ParseUint(strings.TrimSpace(extra[currentCustomAbilitySetRequiredPartsExtra]), 10, 8)
|
|
grade, gradeErr := strconv.ParseUint(strings.TrimSpace(extra[currentCustomAbilitySetGradeExtra]), 10, 8)
|
|
partIndex, partErr := strconv.ParseUint(strings.TrimSpace(extra[currentCustomAbilitySetPartIndexExtra]), 10, 8)
|
|
if requiredErr != nil || required < 3 || required > 6 || gradeErr != nil || grade > 3 || partErr != nil || partIndex == 0 || byte(partIndex) != recordID {
|
|
return currentCustomAbilitySetSnapshot{}, false
|
|
}
|
|
snapshot.requiredParts = byte(required)
|
|
snapshot.grade = byte(grade)
|
|
snapshot.partSetIndex = byte(partIndex)
|
|
return snapshot, true
|
|
}
|
|
|
|
func loadCurrentCustomAbilitySetSnapshot(ctx context.Context, repositories dnfrepo.Group, characterID string) (currentCustomAbilitySetSnapshot, bool, error) {
|
|
if repositories.Inventory == nil || repositories.Equipment == nil {
|
|
return currentCustomAbilitySetSnapshot{}, false, errors.New("custom ability set repositories unavailable")
|
|
}
|
|
inventory, _, err := repositories.Inventory.Load(ctx, characterID)
|
|
if err != nil {
|
|
return currentCustomAbilitySetSnapshot{}, false, err
|
|
}
|
|
equipment, _, err := repositories.Equipment.Load(ctx, characterID)
|
|
if err != nil {
|
|
return currentCustomAbilitySetSnapshot{}, false, err
|
|
}
|
|
counts := make(map[currentCustomAbilitySetSnapshot]int)
|
|
consider := func(raw []byte, extra map[string]string) {
|
|
if len(raw) != currentItemListEntryWireSize {
|
|
return
|
|
}
|
|
snapshot, valid := parseCurrentCustomAbilitySetSnapshot(extra, raw[currentCustomAbilitySetRecordOffset])
|
|
if !valid {
|
|
return
|
|
}
|
|
counts[snapshot]++
|
|
}
|
|
for _, stack := range inventory.Slots {
|
|
if stack.Count == 1 {
|
|
consider(stack.RawEntry, stack.Extra)
|
|
}
|
|
}
|
|
for _, equipped := range equipment.Entries {
|
|
consider(equipped.RawEntry, equipped.Extra)
|
|
}
|
|
for snapshot, memberCount := range counts {
|
|
if memberCount >= int(snapshot.requiredParts) {
|
|
return snapshot, true, nil
|
|
}
|
|
}
|
|
return currentCustomAbilitySetSnapshot{}, false, nil
|
|
}
|
|
|
|
func (s *Service) sendCurrentCustomAbilitySetNotification(session *gameSession, snapshot currentCustomAbilitySetSnapshot, source string) error {
|
|
if session == nil || session.selectedCharacterID == 0 {
|
|
return nil
|
|
}
|
|
body := buildCurrentCustomAbilitySetNotificationBody(uint32(session.selectedCharacterID), snapshot)
|
|
s.logPacketEvent("game-upper-custom-ability-set-notification-send", "conn_id", session.connID, "source", source, "char_id", session.selectedCharacterID, "msg_id", currentCustomAbilitySetNotifyOpcode, "classification", 0, "record_id", snapshot.recordID, "required_parts", snapshot.requiredParts, "options", currentCustomAbilitySetOptionsText(snapshot.options[:]), "grade", snapshot.grade, "body_len", len(body), "evidence", "current_exe_sub_1D6D760_registered_at_0x1D7F958_wire_0x04FE")
|
|
return s.sendGameUpperRawClass(session, currentCustomAbilitySetNotifyOpcode, body, 0)
|
|
}
|
|
|
|
func (s *Service) handleCurrentCustomAbilitySet(session *gameSession, body []byte) error {
|
|
request, err := decodeCurrentCustomAbilitySetRequest(body)
|
|
if err != nil || session == nil || session.selectedCharacterID == 0 {
|
|
if err == nil {
|
|
err = errCurrentCustomAbilitySetInvalid
|
|
}
|
|
s.logGameEvent(session, "game-custom-ability-set-rejected", "reason", err, "body_len", len(body))
|
|
return s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketCustomAbilitySetEquipOption), buildCurrentCustomAbilitySetFailureBody(err), dnfproto.DefaultChannelClassification)
|
|
}
|
|
s.initialEquipmentMu.Lock()
|
|
archive, archiveErr := s.initialEquipmentArchiveLocked()
|
|
s.initialEquipmentMu.Unlock()
|
|
if archiveErr != nil {
|
|
return archiveErr
|
|
}
|
|
catalog, err := parseCurrentCustomAbilitySetCatalog(archive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
individualCatalog, err := parseCurrentCustomAbilityCatalog(archive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
repositories, ok := s.repositoryGroup()
|
|
if !ok || repositories.Inventory == nil || repositories.Equipment == nil || repositories.CharacterItems == nil {
|
|
return errors.New("custom ability set repositories unavailable")
|
|
}
|
|
profileCache := make(map[uint32]currentCustomAbilityItemProfile)
|
|
resolve := func(itemID uint32) (currentCustomAbilityItemProfile, error) {
|
|
if profile, found := profileCache[itemID]; found {
|
|
return profile, nil
|
|
}
|
|
profile, resolveErr := s.resolveCurrentCustomAbilityItemProfile(itemID)
|
|
if resolveErr == nil {
|
|
profileCache[itemID] = profile
|
|
}
|
|
return profile, resolveErr
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), currentRepositorySnapshotTimeout)
|
|
defer cancel()
|
|
characterID := strconv.Itoa(int(session.selectedCharacterID))
|
|
result, err := commitCurrentCustomAbilitySet(ctx, repositories, characterID, request, individualCatalog, catalog, resolve)
|
|
if err != nil {
|
|
s.logGameEvent(session, "game-custom-ability-set-rejected", "options", currentCustomAbilitySetOptionsText(request.options), "slots", fmt.Sprint(request.slots), "reason", err, "body_len", len(body))
|
|
return s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketCustomAbilitySetEquipOption), buildCurrentCustomAbilitySetFailureBody(err), dnfproto.DefaultChannelClassification)
|
|
}
|
|
if err := s.sendSelectedIncrementalItemSlotRefreshes(session, "custom_ability_set_commit", result.refreshes); err != nil {
|
|
return err
|
|
}
|
|
if result.equipmentChanged {
|
|
if err := s.sendSelectedCurrentEquipmentSlotItemUpdates(session, "custom_ability_set_clear_previous_equipped_members"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketCustomAbilitySetEquipOption), buildCurrentCustomAbilitySetSuccessBody(), dnfproto.DefaultChannelClassification); err != nil {
|
|
return err
|
|
}
|
|
s.logGameEvent(session, "game-custom-ability-set-committed", "options", currentCustomAbilitySetOptionsText(request.options), "slots", fmt.Sprint(request.slots), "set_grade", result.snapshot.grade, "spent_points", result.spentPoints, "material_item_id", result.materialID, "material_cost", result.materialCost, "material_remaining", result.materialRemaining, "sequence", "op14_list0_then_optional_op14_list3_then_class1_op1341_ack_then_class0_wire1278")
|
|
return s.sendCurrentCustomAbilitySetNotification(session, result.snapshot, "custom_ability_set_commit")
|
|
}
|