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,516 @@
|
||||
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")
|
||||
}
|
||||
Reference in new issue
Block a user