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,494 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
chronicleOptionVectorOffset = 0x27
|
||||
chronicleOptionVectorSize = 17
|
||||
chronicleOptionCapacity = 2
|
||||
chronicleFailureFragmentCount int64 = 5
|
||||
|
||||
chronicleRefineErrorInvalidMaterial byte = 0x04
|
||||
chronicleRefineErrorInvalidTarget byte = 0x13
|
||||
)
|
||||
|
||||
var errChronicleFailureRewardUnavailable = errors.New("chronicle refine failure reward is unavailable")
|
||||
|
||||
type chronicleOption struct {
|
||||
CanonicalAuraOptionID int32
|
||||
CharacterJob byte
|
||||
FirstGrowType byte
|
||||
EquipmentType byte
|
||||
OptionNo byte
|
||||
}
|
||||
|
||||
// ChronicleRefineReward is one destruction reward row consumed by the
|
||||
// current op370 result reader. UpdatedStack is the final durable slot state
|
||||
// used by the following op14 refresh.
|
||||
type ChronicleRefineReward struct {
|
||||
SlotIndex int16
|
||||
ItemID int64
|
||||
Count int64
|
||||
UpdatedStack dnfrepo.ItemStack
|
||||
}
|
||||
|
||||
// ChronicleRefineResult is the durable result of one current-client op370
|
||||
// attempt. A failed chance roll consumes the aura, destroys the target, and
|
||||
// grants only five dimensional fragments plus the PVF-backed no-element cube
|
||||
// baseline. Normal disassembly additional rewards are deliberately excluded.
|
||||
type ChronicleRefineResult struct {
|
||||
CharacterID string
|
||||
Success bool
|
||||
ErrorCode byte
|
||||
MaterialSlotIndex int16
|
||||
MaterialRemainingCount int64
|
||||
TargetListType byte
|
||||
TargetSlotIndex int16
|
||||
TargetUpdatedStack dnfrepo.ItemStack
|
||||
MaterialUpdatedStack dnfrepo.ItemStack
|
||||
AuraType byte
|
||||
CanonicalAuraOptionID int32
|
||||
OptionCount byte
|
||||
Probability byte
|
||||
ProbabilityRoll byte
|
||||
RefineSucceeded bool
|
||||
TargetDestroyed bool
|
||||
FailureRewards []ChronicleRefineReward
|
||||
Changed bool
|
||||
}
|
||||
|
||||
// RefineChronicle applies one PVF-authorized aura record and consumes one
|
||||
// material in the same character-asset transaction.
|
||||
func (o *Owner) RefineChronicle(ctx context.Context, cmd Command, resolver alignedcmd.ChronicleRefineResolver) (ChronicleRefineResult, error) {
|
||||
base := ChronicleRefineResult{
|
||||
ErrorCode: chronicleRefineErrorInvalidMaterial,
|
||||
MaterialSlotIndex: cmd.MaterialSlotIndex,
|
||||
TargetListType: cmd.TargetListType,
|
||||
TargetSlotIndex: cmd.TargetSlotIndex,
|
||||
}
|
||||
if o == nil || o.repo == nil || o.equipment == nil || o.characters == nil {
|
||||
return base, ErrOwnerUnavailable
|
||||
}
|
||||
if cmd.SelectedCharacterID == 0 {
|
||||
return base, ErrCharacterRequired
|
||||
}
|
||||
if resolver == nil {
|
||||
return base, fmt.Errorf("chronicle refine runtime-PVF resolver required")
|
||||
}
|
||||
if cmd.MaterialSlotIndex < 0 || cmd.TargetSlotIndex < 0 ||
|
||||
(cmd.TargetListType != listTypeMain && cmd.TargetListType != listTypeEquipment) ||
|
||||
(cmd.TargetListType == listTypeMain && cmd.MaterialSlotIndex == cmd.TargetSlotIndex) {
|
||||
return base, nil
|
||||
}
|
||||
|
||||
characterID := strconv.FormatUint(uint64(cmd.SelectedCharacterID), 10)
|
||||
base.CharacterID = characterID
|
||||
if !o.inItemTransaction {
|
||||
accountID := strings.TrimSpace(cmd.AccountID)
|
||||
if accountID == "" {
|
||||
return base, ErrAccountRequired
|
||||
}
|
||||
var result ChronicleRefineResult
|
||||
err := o.withinAccountCharacterAssetTransaction(ctx, accountID, characterID, func(txOwner *Owner) error {
|
||||
var innerErr error
|
||||
result, innerErr = txOwner.RefineChronicle(ctx, cmd, resolver)
|
||||
return innerErr
|
||||
})
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
character, found, err := o.characters.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
if !found {
|
||||
return base, ErrCharacterRequired
|
||||
}
|
||||
jobValue, err := strconv.ParseUint(strings.TrimSpace(character.Job), 10, 8)
|
||||
if err != nil {
|
||||
return base, fmt.Errorf("chronicle refine character job %q invalid", character.Job)
|
||||
}
|
||||
firstGrowType := byte(character.Stats["grow_type"]) & 0x0f
|
||||
|
||||
inventoryRecord, found, err := o.repo.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
if !found {
|
||||
return base, ErrInventoryNotFound
|
||||
}
|
||||
inventoryRecord = dnfrepo.CloneInventory(inventoryRecord)
|
||||
if inventoryRecord.Slots == nil {
|
||||
inventoryRecord.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
|
||||
materialKey := slotKey(listTypeMain, cmd.MaterialSlotIndex)
|
||||
material, found := inventoryRecord.Slots[materialKey]
|
||||
if !found || material.ItemID != int64(cmd.MaterialItemTemplateID) || material.Count <= 0 || !isStackable(material) {
|
||||
return base, nil
|
||||
}
|
||||
|
||||
var target dnfrepo.ItemStack
|
||||
var equipmentRecord dnfrepo.EquipmentRecord
|
||||
var equipmentEntry dnfrepo.EquipmentEntry
|
||||
var targetKey string
|
||||
switch cmd.TargetListType {
|
||||
case listTypeMain:
|
||||
targetKey = slotKey(listTypeMain, cmd.TargetSlotIndex)
|
||||
stack, ok := inventoryRecord.Slots[targetKey]
|
||||
if !ok || stack.ItemID != int64(cmd.TargetItemTemplateID) || stack.Count <= 0 {
|
||||
base.ErrorCode = chronicleRefineErrorInvalidTarget
|
||||
return base, nil
|
||||
}
|
||||
target = cloneStack(stack)
|
||||
case listTypeEquipment:
|
||||
loaded, ok, loadErr := o.equipment.Load(ctx, characterID)
|
||||
if loadErr != nil {
|
||||
return base, loadErr
|
||||
}
|
||||
if !ok {
|
||||
base.ErrorCode = chronicleRefineErrorInvalidTarget
|
||||
return base, nil
|
||||
}
|
||||
equipmentRecord = dnfrepo.CloneEquipment(loaded)
|
||||
targetKey = strconv.FormatInt(int64(cmd.TargetSlotIndex), 10)
|
||||
entry, ok := equipmentRecord.Entries[targetKey]
|
||||
if !ok || entry.ItemID != int64(cmd.TargetItemTemplateID) {
|
||||
base.ErrorCode = chronicleRefineErrorInvalidTarget
|
||||
return base, nil
|
||||
}
|
||||
equipmentEntry = entry
|
||||
target = forgingStackFromEquipmentEntry(entry)
|
||||
}
|
||||
if isEquipmentLocked(target) || len(target.RawEntry) != currentItemListEntrySize {
|
||||
base.ErrorCode = chronicleRefineErrorInvalidTarget
|
||||
return base, nil
|
||||
}
|
||||
|
||||
resolution, err := resolver(
|
||||
int64(cmd.MaterialItemTemplateID),
|
||||
int64(cmd.TargetItemTemplateID),
|
||||
byte(jobValue),
|
||||
firstGrowType,
|
||||
cmd.ChronicleOptionNo,
|
||||
)
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
if !validChronicleRefineResolution(resolution) || !forgingDurabilityOK(target, resolution.TargetDurability) {
|
||||
base.ErrorCode = chronicleRefineErrorInvalidTarget
|
||||
return base, nil
|
||||
}
|
||||
|
||||
options, err := parseChronicleOptions(target.RawEntry)
|
||||
if err != nil || len(options) >= chronicleOptionCapacity {
|
||||
base.ErrorCode = chronicleRefineErrorInvalidTarget
|
||||
return base, nil
|
||||
}
|
||||
for _, option := range options {
|
||||
if option.CanonicalAuraOptionID == resolution.CanonicalAuraOptionID && option.OptionNo == cmd.ChronicleOptionNo {
|
||||
base.ErrorCode = chronicleRefineErrorInvalidTarget
|
||||
return base, nil
|
||||
}
|
||||
}
|
||||
probability := resolution.Probabilities[len(options)]
|
||||
base.Probability = probability
|
||||
roll := byte(0)
|
||||
refineSucceeded := probability >= 100
|
||||
if !refineSucceeded {
|
||||
roll = byte(o.chronicleRandomIntn(100))
|
||||
refineSucceeded = roll < probability
|
||||
}
|
||||
base.ProbabilityRoll = roll
|
||||
base.RefineSucceeded = refineSucceeded
|
||||
|
||||
material = cloneStack(material)
|
||||
material.Count--
|
||||
if material.Count == 0 {
|
||||
delete(inventoryRecord.Slots, materialKey)
|
||||
} else {
|
||||
updateStackRawAmount(&material)
|
||||
inventoryRecord.Slots[materialKey] = material
|
||||
}
|
||||
|
||||
if refineSucceeded {
|
||||
options = append(options, chronicleOption{
|
||||
CanonicalAuraOptionID: resolution.CanonicalAuraOptionID,
|
||||
CharacterJob: byte(jobValue),
|
||||
FirstGrowType: firstGrowType,
|
||||
EquipmentType: resolution.TargetEquipmentTypeCode,
|
||||
OptionNo: cmd.ChronicleOptionNo,
|
||||
})
|
||||
setChronicleOptions(&target, options)
|
||||
switch cmd.TargetListType {
|
||||
case listTypeMain:
|
||||
inventoryRecord.Slots[targetKey] = target
|
||||
case listTypeEquipment:
|
||||
equipmentEntry.RawEntry = append([]byte(nil), target.RawEntry...)
|
||||
equipmentEntry.Extra = cloneStringMapForForging(target.Extra)
|
||||
equipmentRecord.Entries[targetKey] = equipmentEntry
|
||||
}
|
||||
} else {
|
||||
if !validChronicleFailureResolution(resolution) {
|
||||
return base, errChronicleFailureRewardUnavailable
|
||||
}
|
||||
switch cmd.TargetListType {
|
||||
case listTypeMain:
|
||||
delete(inventoryRecord.Slots, targetKey)
|
||||
case listTypeEquipment:
|
||||
delete(equipmentRecord.Entries, targetKey)
|
||||
}
|
||||
failureRewards, accountRecord, rewardErr := o.grantChronicleFailureRewards(
|
||||
ctx,
|
||||
strings.TrimSpace(cmd.AccountID),
|
||||
&inventoryRecord,
|
||||
resolution,
|
||||
)
|
||||
if rewardErr != nil {
|
||||
return base, rewardErr
|
||||
}
|
||||
base.FailureRewards = failureRewards
|
||||
base.TargetDestroyed = true
|
||||
accountRecord.UpdatedAt = time.Now()
|
||||
if err := o.accountRepo.Save(ctx, accountRecord); err != nil {
|
||||
return base, err
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.TargetListType == listTypeEquipment {
|
||||
equipmentRecord.UpdatedAt = time.Now()
|
||||
if err := dnfrepo.SaveEquipmentFields(ctx, o.equipment, equipmentRecord, dnfrepo.EquipmentFieldEntries); err != nil {
|
||||
return base, err
|
||||
}
|
||||
}
|
||||
inventoryRecord.UpdatedAt = time.Now()
|
||||
if err := dnfrepo.SaveInventoryFields(ctx, o.repo, inventoryRecord, dnfrepo.InventoryFieldSlots); err != nil {
|
||||
return base, err
|
||||
}
|
||||
|
||||
base.Success = true
|
||||
base.MaterialRemainingCount = material.Count
|
||||
base.TargetUpdatedStack = inventoryRecord.Slots[targetKey]
|
||||
if cmd.TargetListType == listTypeEquipment && refineSucceeded {
|
||||
base.TargetUpdatedStack = target
|
||||
}
|
||||
base.MaterialUpdatedStack = inventoryRecord.Slots[materialKey]
|
||||
base.AuraType = resolution.AuraType
|
||||
base.CanonicalAuraOptionID = resolution.CanonicalAuraOptionID
|
||||
base.OptionCount = byte(len(options))
|
||||
base.Changed = true
|
||||
return base, nil
|
||||
}
|
||||
|
||||
func validChronicleRefineResolution(resolution alignedcmd.ChronicleRefineResolution) bool {
|
||||
return resolution.Matched && resolution.CanonicalAuraOptionID == int32(1254)+int32(resolution.AuraType) &&
|
||||
resolution.AuraType <= 2 && strings.EqualFold(strings.TrimSpace(resolution.TargetKind), "equipment") &&
|
||||
validChronicleEquipmentDefinitionType(resolution.TargetEquipmentTypeCode) &&
|
||||
resolution.Probabilities[0] <= 100 && resolution.Probabilities[1] <= 100
|
||||
}
|
||||
|
||||
func validChronicleEquipmentDefinitionType(code byte) bool {
|
||||
switch code {
|
||||
case 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 25:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validChronicleFailureResolution(resolution alignedcmd.ChronicleRefineResolution) bool {
|
||||
cubeSlot, cubeAccountOwned := upgradeAccountSharedMaterialSlot(resolution.FailureCubeItem.ItemID)
|
||||
return resolution.FailureCubeCount > 0 && resolution.FailureCubeItem.ItemID > 0 &&
|
||||
strings.EqualFold(strings.TrimSpace(resolution.FailureCubeItem.Kind), "stackable") &&
|
||||
resolution.FailureCubeItem.TargetListType == listTypeMain && cubeAccountOwned &&
|
||||
cubeSlot >= dnfrepo.CrystalWarehouseFirstSlot && cubeSlot <= dnfrepo.CrystalWarehouseLastSlot &&
|
||||
resolution.FailureFragmentItemID == 3311 &&
|
||||
resolution.FailureFragmentItem.ItemID == resolution.FailureFragmentItemID &&
|
||||
strings.EqualFold(strings.TrimSpace(resolution.FailureFragmentItem.Kind), "stackable") &&
|
||||
resolution.FailureFragmentItem.TargetListType == listTypeMain &&
|
||||
resolution.FailureFragmentItem.SlotStart >= 0 &&
|
||||
resolution.FailureFragmentItem.SlotEnd >= resolution.FailureFragmentItem.SlotStart
|
||||
}
|
||||
|
||||
func (o *Owner) chronicleRandomIntn(limit int) int {
|
||||
if limit <= 1 || o == nil || o.chronicleRandom == nil {
|
||||
return 0
|
||||
}
|
||||
value := o.chronicleRandom(limit)
|
||||
if value < 0 {
|
||||
return 0
|
||||
}
|
||||
if value >= limit {
|
||||
return value % limit
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (o *Owner) grantChronicleFailureRewards(
|
||||
ctx context.Context,
|
||||
accountID string,
|
||||
inventory *dnfrepo.InventoryRecord,
|
||||
resolution alignedcmd.ChronicleRefineResolution,
|
||||
) ([]ChronicleRefineReward, dnfrepo.AccountInventoryRecord, error) {
|
||||
if o == nil || o.accountRepo == nil || strings.TrimSpace(accountID) == "" || inventory == nil ||
|
||||
!validChronicleFailureResolution(resolution) {
|
||||
return nil, dnfrepo.AccountInventoryRecord{}, errChronicleFailureRewardUnavailable
|
||||
}
|
||||
account, _, err := o.accountRepo.Load(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, dnfrepo.AccountInventoryRecord{}, err
|
||||
}
|
||||
account = dnfrepo.CloneAccountInventory(account)
|
||||
account.AccountID = strings.TrimSpace(accountID)
|
||||
if account.Slots == nil {
|
||||
account.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
if inventory.Slots == nil {
|
||||
inventory.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
|
||||
rewards, err := grantChronicleBackpackReward(inventory.Slots, resolution.FailureFragmentItem, chronicleFailureFragmentCount)
|
||||
if err != nil {
|
||||
return nil, dnfrepo.AccountInventoryRecord{}, err
|
||||
}
|
||||
cubeReward, err := grantChronicleAccountReward(&account, resolution.FailureCubeItem, resolution.FailureCubeCount)
|
||||
if err != nil {
|
||||
return nil, dnfrepo.AccountInventoryRecord{}, err
|
||||
}
|
||||
rewards = append(rewards, cubeReward)
|
||||
return rewards, account, nil
|
||||
}
|
||||
|
||||
func grantChronicleBackpackReward(items map[string]dnfrepo.ItemStack, reward alignedcmd.MagicBoxRewardItem, count int64) ([]ChronicleRefineReward, error) {
|
||||
before := make(map[int16]int64)
|
||||
for slot := reward.SlotStart; slot <= reward.SlotEnd; slot++ {
|
||||
if stack, found := items[slotKey(listTypeMain, slot)]; found && stack.ItemID == reward.ItemID && stack.Count > 0 {
|
||||
before[slot] = stack.Count
|
||||
}
|
||||
}
|
||||
slots, err := grantRandomRewardItem(items, reward, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]ChronicleRefineReward, 0, len(slots))
|
||||
for _, slot := range slots {
|
||||
stack, found := items[slotKey(listTypeMain, slot)]
|
||||
granted := stack.Count - before[slot]
|
||||
if !found || stack.ItemID != reward.ItemID || granted <= 0 {
|
||||
return nil, errChronicleFailureRewardUnavailable
|
||||
}
|
||||
result = append(result, ChronicleRefineReward{
|
||||
SlotIndex: slot,
|
||||
ItemID: reward.ItemID,
|
||||
Count: granted,
|
||||
UpdatedStack: cloneStack(stack),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func grantChronicleAccountReward(account *dnfrepo.AccountInventoryRecord, reward alignedcmd.MagicBoxRewardItem, count int64) (ChronicleRefineReward, error) {
|
||||
if account == nil || count <= 0 || reward.ItemID <= 0 || !strings.EqualFold(strings.TrimSpace(reward.Kind), "stackable") {
|
||||
return ChronicleRefineReward{}, errChronicleFailureRewardUnavailable
|
||||
}
|
||||
slot, ok := upgradeAccountSharedMaterialSlot(reward.ItemID)
|
||||
if !ok || slot < dnfrepo.CrystalWarehouseFirstSlot || slot > dnfrepo.CrystalWarehouseLastSlot {
|
||||
return ChronicleRefineReward{}, errChronicleFailureRewardUnavailable
|
||||
}
|
||||
key := dnfrepo.AccountSharedInventorySlotKey(slot)
|
||||
stack, found := account.Slots[key]
|
||||
if found && stack.ItemID > 0 && stack.ItemID != reward.ItemID {
|
||||
return ChronicleRefineReward{}, errChronicleFailureRewardUnavailable
|
||||
}
|
||||
// 账号共享晶石/灵魂行是账号金库这个容器的一部分:封顶按"这容器里该道具总量"
|
||||
// 算,不能只看目标格那一行;容器里已经有该道具的行时也不能再开第二行。
|
||||
space := dnfrepo.ContainerItemSpace(account.Slots, dnfrepo.MainInventoryListType, reward.ItemID, reward.StackLimit)
|
||||
if reward.StackLimit > 0 && count > space {
|
||||
return ChronicleRefineReward{}, ErrRandomRewardInventoryFull
|
||||
}
|
||||
if rowKey, _, exists := dnfrepo.ContainerItemRow(account.Slots, dnfrepo.MainInventoryListType, reward.ItemID); exists && rowKey != key {
|
||||
return ChronicleRefineReward{}, ErrRandomRewardInventoryFull
|
||||
}
|
||||
if !found || stack.ItemID <= 0 {
|
||||
stack = dnfrepo.ItemStack{
|
||||
ItemID: reward.ItemID,
|
||||
Bind: reward.Seal,
|
||||
Extra: map[string]string{
|
||||
"item_kind": "stackable",
|
||||
"pvf_path": reward.PVFPath,
|
||||
"source": "chronicle_refine_failure",
|
||||
},
|
||||
}
|
||||
if reward.StackLimit > 0 {
|
||||
stack.Extra["stack_limit"] = strconv.FormatInt(reward.StackLimit, 10)
|
||||
}
|
||||
}
|
||||
stack = cloneStack(stack)
|
||||
stack.Count += count
|
||||
updateStackRawAmount(&stack)
|
||||
account.Slots[key] = stack
|
||||
return ChronicleRefineReward{
|
||||
SlotIndex: slot,
|
||||
ItemID: reward.ItemID,
|
||||
Count: count,
|
||||
UpdatedStack: cloneStack(stack),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseChronicleOptions(raw []byte) ([]chronicleOption, error) {
|
||||
if len(raw) != currentItemListEntrySize {
|
||||
return nil, fmt.Errorf("chronicle target raw length=%d want=%d", len(raw), currentItemListEntrySize)
|
||||
}
|
||||
count := int(raw[chronicleOptionVectorOffset])
|
||||
if count > chronicleOptionCapacity {
|
||||
return nil, fmt.Errorf("chronicle option count=%d", count)
|
||||
}
|
||||
options := make([]chronicleOption, 0, count)
|
||||
for index := 0; index < count; index++ {
|
||||
optionID := int32(binary.LittleEndian.Uint32(raw[0x28+index*4 : 0x2c+index*4]))
|
||||
if optionID < 1254 || optionID > 1256 {
|
||||
return nil, fmt.Errorf("chronicle canonical option id=%d unsupported", optionID)
|
||||
}
|
||||
options = append(options, chronicleOption{
|
||||
CanonicalAuraOptionID: optionID,
|
||||
CharacterJob: raw[0x30+index],
|
||||
FirstGrowType: raw[0x32+index],
|
||||
EquipmentType: raw[0x34+index],
|
||||
OptionNo: raw[0x36+index],
|
||||
})
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func setChronicleOptions(stack *dnfrepo.ItemStack, options []chronicleOption) {
|
||||
if stack == nil || len(stack.RawEntry) != currentItemListEntrySize || len(options) > chronicleOptionCapacity {
|
||||
return
|
||||
}
|
||||
stack.RawEntry = append([]byte(nil), stack.RawEntry...)
|
||||
stack.Extra = cloneStringMapForForging(stack.Extra)
|
||||
vector := stack.RawEntry[chronicleOptionVectorOffset : chronicleOptionVectorOffset+chronicleOptionVectorSize]
|
||||
clear(vector)
|
||||
vector[0] = byte(len(options))
|
||||
for index, option := range options {
|
||||
binary.LittleEndian.PutUint32(vector[1+index*4:5+index*4], uint32(option.CanonicalAuraOptionID))
|
||||
vector[9+index] = option.CharacterJob
|
||||
vector[11+index] = option.FirstGrowType
|
||||
vector[13+index] = option.EquipmentType
|
||||
vector[15+index] = option.OptionNo
|
||||
}
|
||||
stack.Extra["middle_data_1a"] = fmt.Sprintf("%x", vector)
|
||||
}
|
||||
Reference in new issue
Block a user