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,543 @@
|
||||
// Package limitedcube owns the durable mutation for PVF [limited cube]
|
||||
// stackables. Wire dispatch and PVF parsing remain in dnfbridge.
|
||||
package limitedcube
|
||||
|
||||
import (
|
||||
"context"
|
||||
cryptorand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrOwnerUnavailable = errors.New("limited-cube owner unavailable")
|
||||
ErrAccountRequired = errors.New("limited-cube account id required")
|
||||
ErrCharacterRequired = errors.New("limited-cube character id required")
|
||||
ErrInventoryNotFound = errors.New("limited-cube inventory record not found")
|
||||
ErrPolicyInvalid = errors.New("limited-cube PVF policy is invalid")
|
||||
ErrTicketMissing = errors.New("limited-cube ticket is missing")
|
||||
ErrTicketMismatch = errors.New("limited-cube ticket does not match request")
|
||||
ErrTicketLocked = errors.New("limited-cube ticket is locked")
|
||||
ErrConditionItemMissing = errors.New("limited-cube condition item is missing")
|
||||
ErrConditionItemMismatch = errors.New("limited-cube condition item does not match request")
|
||||
ErrConditionItemLocked = errors.New("limited-cube condition item is locked")
|
||||
ErrMaterialInsufficient = errors.New("limited-cube material is insufficient")
|
||||
ErrResultSelectionFailed = errors.New("limited-cube result selection failed")
|
||||
)
|
||||
|
||||
const (
|
||||
mainInventoryListType byte = 0
|
||||
consumeTicketCount int64 = 1
|
||||
)
|
||||
|
||||
// Requirement is one PVF item/count requirement from a condition section.
|
||||
type Requirement struct {
|
||||
ItemID int64
|
||||
Count int64
|
||||
}
|
||||
|
||||
// WeightedResult is one exact [result item] candidate. Stack carries the
|
||||
// resolved PVF instance metadata prepared by the bridge; its count is the
|
||||
// PVF result count rather than client-controlled data.
|
||||
type WeightedResult struct {
|
||||
Stack dnfrepo.ItemStack
|
||||
Weight int64
|
||||
}
|
||||
|
||||
// Policy is the fully resolved, server-authoritative rule set for one ticket.
|
||||
// The bridge builds it exclusively from the active PVF document.
|
||||
type Policy struct {
|
||||
TicketItemID int64
|
||||
Conditions []Requirement
|
||||
Materials []Requirement
|
||||
Results []WeightedResult
|
||||
}
|
||||
|
||||
// Command is the trusted portion of the current-client op338 request. The
|
||||
// client sends the selected bead slot and ID first, then the ticket slot.
|
||||
type Command struct {
|
||||
AccountID string
|
||||
CharacterID string
|
||||
TicketSlot int16
|
||||
TicketItemID int64
|
||||
TargetSlot int16
|
||||
TargetItemID int64
|
||||
}
|
||||
|
||||
// Result reports the committed inventory changes. CharacterChangedSlots and
|
||||
// AccountChangedSlots remain at their native list-0 wire addresses, but have
|
||||
// distinct durable owners. Both are sorted for deterministic client refresh.
|
||||
type Result struct {
|
||||
TicketSlot int16
|
||||
TicketItemID int64
|
||||
TicketRemaining int64
|
||||
InputSlot int16
|
||||
InputItemID int64
|
||||
ResultItemID int64
|
||||
ChangedSlots []int16
|
||||
AccountChangedSlots []int16
|
||||
}
|
||||
|
||||
// Owner executes one limited-cube change in the account-and-character item
|
||||
// transaction supplied by the repository group.
|
||||
type Owner struct {
|
||||
items dnfrepo.AccountCharacterItemUnitOfWork
|
||||
}
|
||||
|
||||
func NewOwner(repositories dnfrepo.Group) (*Owner, error) {
|
||||
if repositories.Inventory == nil || repositories.AccountInventory == nil || repositories.AccountItems == nil {
|
||||
return nil, ErrOwnerUnavailable
|
||||
}
|
||||
return &Owner{items: repositories.AccountItems}, nil
|
||||
}
|
||||
|
||||
// Use consumes one ticket, the client-selected A-condition item, and all
|
||||
// B-condition material before replacing that item with one weighted PVF
|
||||
// result. Binding and an existing instance deadline stay with the transformed
|
||||
// item; result-template metadata replaces the old template metadata.
|
||||
//
|
||||
// The A-condition item is either a stackable (pet-bead change tickets) or an
|
||||
// equipment instance (title change coupons). When both the consumed item and
|
||||
// the result are equipment, the instance-owned fields of the player's copy —
|
||||
// the 宝珠 enchant and the 封装 mark — are carried onto the new template; see
|
||||
// equipmentInstanceExtraKeys.
|
||||
func (o *Owner) Use(ctx context.Context, command Command, policy Policy) (Result, error) {
|
||||
if o == nil || o.items == nil {
|
||||
return Result{}, ErrOwnerUnavailable
|
||||
}
|
||||
accountID := strings.TrimSpace(command.AccountID)
|
||||
if accountID == "" {
|
||||
return Result{}, ErrAccountRequired
|
||||
}
|
||||
characterID := strings.TrimSpace(command.CharacterID)
|
||||
if characterID == "" {
|
||||
return Result{}, ErrCharacterRequired
|
||||
}
|
||||
if err := validatePolicy(policy); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if command.TicketSlot < 0 || command.TargetSlot < 0 || command.TicketSlot == command.TargetSlot ||
|
||||
command.TicketItemID != policy.TicketItemID {
|
||||
return Result{}, fmt.Errorf("%w: slot=%d item=%d policy_item=%d", ErrTicketMismatch, command.TicketSlot, command.TicketItemID, policy.TicketItemID)
|
||||
}
|
||||
|
||||
var result Result
|
||||
err := o.items.WithinAccountCharacterItems(ctx, accountID, characterID, func(accounts dnfrepo.AccountInventoryRepository, inventory dnfrepo.InventoryRepository) error {
|
||||
record, found, err := inventory.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return ErrInventoryNotFound
|
||||
}
|
||||
record = dnfrepo.CloneInventory(record)
|
||||
record.CharacterID = characterID
|
||||
if record.Slots == nil {
|
||||
return ErrTicketMissing
|
||||
}
|
||||
account, accountFound, err := accounts.Load(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !accountFound {
|
||||
account = dnfrepo.AccountInventoryRecord{AccountID: accountID}
|
||||
}
|
||||
account = dnfrepo.CloneAccountInventory(account)
|
||||
account.AccountID = accountID
|
||||
if account.Slots == nil {
|
||||
account.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
|
||||
ticketKey := inventorySlotKey(command.TicketSlot)
|
||||
ticket, found := record.Slots[ticketKey]
|
||||
if !found || ticket.Count <= 0 {
|
||||
return fmt.Errorf("%w: slot=%d", ErrTicketMissing, command.TicketSlot)
|
||||
}
|
||||
if ticket.ItemID != command.TicketItemID {
|
||||
return fmt.Errorf("%w: slot=%d want=%d got=%d", ErrTicketMismatch, command.TicketSlot, command.TicketItemID, ticket.ItemID)
|
||||
}
|
||||
if limitedCubeItemLocked(ticket) {
|
||||
return fmt.Errorf("%w: slot=%d", ErrTicketLocked, command.TicketSlot)
|
||||
}
|
||||
|
||||
inputKey := inventorySlotKey(command.TargetSlot)
|
||||
input, found := record.Slots[inputKey]
|
||||
if !found || input.Count <= 0 {
|
||||
return fmt.Errorf("%w: slot=%d", ErrConditionItemMissing, command.TargetSlot)
|
||||
}
|
||||
if input.ItemID != command.TargetItemID {
|
||||
return fmt.Errorf("%w: slot=%d want=%d got=%d", ErrConditionItemMismatch, command.TargetSlot, command.TargetItemID, input.ItemID)
|
||||
}
|
||||
if limitedCubeItemLocked(input) {
|
||||
return fmt.Errorf("%w: slot=%d", ErrConditionItemLocked, command.TargetSlot)
|
||||
}
|
||||
conditionCounts, err := normalizedRequirements(policy.Conditions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requiredCount, allowed := conditionCounts[input.ItemID]
|
||||
if !allowed || input.Count != requiredCount {
|
||||
return fmt.Errorf("%w: slot=%d item=%d count=%d required=%d", ErrConditionItemMismatch, command.TargetSlot, input.ItemID, input.Count, requiredCount)
|
||||
}
|
||||
materialUses, err := planMaterialConsumption(record.Slots, account.Slots, policy.Materials, ticketKey, inputKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selected, err := selectWeightedResult(policy.Results, input.ItemID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ticketRemaining := ticket.Count - consumeTicketCount
|
||||
if ticketRemaining == 0 {
|
||||
delete(record.Slots, ticketKey)
|
||||
} else {
|
||||
ticket.Count = ticketRemaining
|
||||
record.Slots[ticketKey] = ticket
|
||||
}
|
||||
for _, use := range materialUses {
|
||||
items := record.Slots
|
||||
if use.accountOwned {
|
||||
items = account.Slots
|
||||
}
|
||||
stack := items[use.key]
|
||||
stack.Count -= use.count
|
||||
if stack.Count <= 0 {
|
||||
delete(items, use.key)
|
||||
continue
|
||||
}
|
||||
items[use.key] = stack
|
||||
}
|
||||
|
||||
resultStack := cloneStack(selected.Stack)
|
||||
resultStack.Bind = input.Bind
|
||||
if resultStack.ExpireAt.IsZero() {
|
||||
resultStack.ExpireAt = input.ExpireAt
|
||||
}
|
||||
// A new item template cannot reuse the old raw item row: item-specific
|
||||
// fields in that row would otherwise survive a PVF template change.
|
||||
resultStack.RawEntry = nil
|
||||
if equipmentTemplateSwap(input, resultStack) {
|
||||
resultStack.Extra = carryEquipmentInstanceExtras(resultStack.Extra, input.Extra)
|
||||
}
|
||||
record.Slots[inputKey] = resultStack
|
||||
|
||||
now := time.Now().UTC()
|
||||
record.UpdatedAt = now
|
||||
if err := dnfrepo.SaveInventoryFields(ctx, inventory, record, dnfrepo.InventoryFieldSlots); err != nil {
|
||||
return err
|
||||
}
|
||||
changed := map[int16]struct{}{command.TicketSlot: {}, command.TargetSlot: {}}
|
||||
accountChanged := make(map[int16]struct{})
|
||||
for _, use := range materialUses {
|
||||
if use.accountOwned {
|
||||
accountChanged[use.slot] = struct{}{}
|
||||
} else {
|
||||
changed[use.slot] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(accountChanged) > 0 {
|
||||
account.UpdatedAt = now
|
||||
if err := accounts.Save(ctx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
result = Result{
|
||||
TicketSlot: command.TicketSlot,
|
||||
TicketItemID: command.TicketItemID,
|
||||
TicketRemaining: ticketRemaining,
|
||||
InputSlot: command.TargetSlot,
|
||||
InputItemID: input.ItemID,
|
||||
ResultItemID: resultStack.ItemID,
|
||||
ChangedSlots: sortedSlots(changed),
|
||||
AccountChangedSlots: sortedSlots(accountChanged),
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func validatePolicy(policy Policy) error {
|
||||
if policy.TicketItemID <= 0 || len(policy.Conditions) == 0 || len(policy.Results) == 0 {
|
||||
return ErrPolicyInvalid
|
||||
}
|
||||
if _, err := normalizedRequirements(policy.Conditions); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := normalizedRequirements(policy.Materials); err != nil {
|
||||
return err
|
||||
}
|
||||
var total int64
|
||||
for _, candidate := range policy.Results {
|
||||
if candidate.Stack.ItemID <= 0 || candidate.Stack.Count <= 0 || candidate.Weight <= 0 || candidate.Weight > math.MaxInt64-total {
|
||||
return ErrPolicyInvalid
|
||||
}
|
||||
total += candidate.Weight
|
||||
}
|
||||
if total <= 0 {
|
||||
return ErrPolicyInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizedRequirements(requirements []Requirement) (map[int64]int64, error) {
|
||||
out := make(map[int64]int64, len(requirements))
|
||||
for _, requirement := range requirements {
|
||||
if requirement.ItemID <= 0 || requirement.Count <= 0 || requirement.Count > math.MaxInt64-out[requirement.ItemID] {
|
||||
return nil, ErrPolicyInvalid
|
||||
}
|
||||
out[requirement.ItemID] += requirement.Count
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type materialUse struct {
|
||||
key string
|
||||
slot int16
|
||||
count int64
|
||||
accountOwned bool
|
||||
}
|
||||
|
||||
func planMaterialConsumption(
|
||||
characterSlots map[string]dnfrepo.ItemStack,
|
||||
accountSlots map[string]dnfrepo.ItemStack,
|
||||
materials []Requirement,
|
||||
excludedKeys ...string,
|
||||
) ([]materialUse, error) {
|
||||
needed, err := normalizedRequirements(materials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(needed) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
excluded := make(map[string]struct{}, len(excludedKeys))
|
||||
for _, key := range excludedKeys {
|
||||
excluded[key] = struct{}{}
|
||||
}
|
||||
type candidate struct {
|
||||
key string
|
||||
slot int16
|
||||
stack dnfrepo.ItemStack
|
||||
accountOwned bool
|
||||
}
|
||||
characterCandidates := make([]candidate, 0, len(characterSlots))
|
||||
for key, stack := range characterSlots {
|
||||
if _, skip := excluded[key]; skip {
|
||||
continue
|
||||
}
|
||||
listType, slot, ok := parseInventorySlotKey(key)
|
||||
if !ok || listType != mainInventoryListType || dnfrepo.IsAccountSharedInventorySlot(listType, slot) ||
|
||||
stack.Count <= 0 || limitedCubeItemLocked(stack) {
|
||||
continue
|
||||
}
|
||||
if _, need := needed[stack.ItemID]; need {
|
||||
characterCandidates = append(characterCandidates, candidate{key: key, slot: slot, stack: stack})
|
||||
}
|
||||
}
|
||||
accountCandidates := make([]candidate, 0, len(accountSlots))
|
||||
for key, stack := range accountSlots {
|
||||
listType, slot, ok := parseInventorySlotKey(key)
|
||||
if !ok || !dnfrepo.IsAccountSharedInventorySlot(listType, slot) || stack.Count <= 0 || limitedCubeItemLocked(stack) {
|
||||
continue
|
||||
}
|
||||
if _, need := needed[stack.ItemID]; need {
|
||||
accountCandidates = append(accountCandidates, candidate{key: key, slot: slot, stack: stack, accountOwned: true})
|
||||
}
|
||||
}
|
||||
sort.Slice(characterCandidates, func(i, j int) bool { return characterCandidates[i].slot < characterCandidates[j].slot })
|
||||
sort.Slice(accountCandidates, func(i, j int) bool { return accountCandidates[i].slot < accountCandidates[j].slot })
|
||||
candidates := append(characterCandidates, accountCandidates...)
|
||||
uses := make([]materialUse, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
remaining := needed[candidate.stack.ItemID]
|
||||
if remaining <= 0 {
|
||||
continue
|
||||
}
|
||||
consume := minInt64(candidate.stack.Count, remaining)
|
||||
if consume <= 0 {
|
||||
continue
|
||||
}
|
||||
uses = append(uses, materialUse{
|
||||
key: candidate.key,
|
||||
slot: candidate.slot,
|
||||
count: consume,
|
||||
accountOwned: candidate.accountOwned,
|
||||
})
|
||||
needed[candidate.stack.ItemID] -= consume
|
||||
}
|
||||
for itemID, count := range needed {
|
||||
if count > 0 {
|
||||
return nil, fmt.Errorf("%w: item=%d count=%d", ErrMaterialInsufficient, itemID, count)
|
||||
}
|
||||
}
|
||||
return uses, nil
|
||||
}
|
||||
|
||||
// selectWeightedResult draws one [result item] candidate using exactly the
|
||||
// weights the PVF document declares. It deliberately does NOT filter the
|
||||
// consumed item out of the pool: a 变更券 is allowed to hand back the very item
|
||||
// it consumed, and that is what the published weights describe — the 热舞一夏
|
||||
// coupons list the current title at weight 3000 and the 宠物宝珠 coupons list
|
||||
// every eligible bead at an equal weight.
|
||||
//
|
||||
// The one pool refused is a document that cannot produce a change at all: when
|
||||
// every candidate is the consumed template, the roll would silently burn the
|
||||
// ticket and the B-condition material for an identical item.
|
||||
func selectWeightedResult(candidates []WeightedResult, inputItemID int64) (WeightedResult, error) {
|
||||
var total int64
|
||||
changes := false
|
||||
for _, candidate := range candidates {
|
||||
if candidate.Stack.ItemID <= 0 || candidate.Stack.Count <= 0 || candidate.Weight <= 0 || candidate.Weight > math.MaxInt64-total {
|
||||
return WeightedResult{}, ErrResultSelectionFailed
|
||||
}
|
||||
total += candidate.Weight
|
||||
if candidate.Stack.ItemID != inputItemID {
|
||||
changes = true
|
||||
}
|
||||
}
|
||||
if total <= 0 || !changes {
|
||||
return WeightedResult{}, ErrResultSelectionFailed
|
||||
}
|
||||
roll, err := cryptorand.Int(cryptorand.Reader, big.NewInt(total))
|
||||
if err != nil {
|
||||
return WeightedResult{}, fmt.Errorf("%w: %v", ErrResultSelectionFailed, err)
|
||||
}
|
||||
value := roll.Int64()
|
||||
for _, candidate := range candidates {
|
||||
if value < candidate.Weight {
|
||||
return candidate, nil
|
||||
}
|
||||
value -= candidate.Weight
|
||||
}
|
||||
return WeightedResult{}, ErrResultSelectionFailed
|
||||
}
|
||||
|
||||
func inventorySlotKey(slot int16) string {
|
||||
return "0:" + strconv.FormatInt(int64(slot), 10)
|
||||
}
|
||||
|
||||
func parseInventorySlotKey(key string) (byte, int16, bool) {
|
||||
var list, slot int64
|
||||
if _, err := fmt.Sscanf(key, "%d:%d", &list, &slot); err != nil || list < 0 || list > math.MaxUint8 || slot < math.MinInt16 || slot > math.MaxInt16 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return byte(list), int16(slot), true
|
||||
}
|
||||
|
||||
func sortedSlots(slots map[int16]struct{}) []int16 {
|
||||
out := make([]int16, 0, len(slots))
|
||||
for slot := range slots {
|
||||
out = append(out, slot)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneStack(stack dnfrepo.ItemStack) dnfrepo.ItemStack {
|
||||
stack.RawEntry = append([]byte(nil), stack.RawEntry...)
|
||||
if len(stack.Extra) > 0 {
|
||||
extra := make(map[string]string, len(stack.Extra))
|
||||
for key, value := range stack.Extra {
|
||||
extra[key] = value
|
||||
}
|
||||
stack.Extra = extra
|
||||
}
|
||||
return stack
|
||||
}
|
||||
|
||||
func minInt64(left, right int64) int64 {
|
||||
if left < right {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
func limitedCubeItemLocked(stack dnfrepo.ItemStack) bool {
|
||||
if stack.Extra == nil {
|
||||
return false
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(stack.Extra["equipment_lock_state"])) {
|
||||
case "1", "2", "active", "locked", "unlocking", "pending_unlock":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// equipmentInstanceExtraKeys are the durable fields that belong to the player's
|
||||
// copy of an equipment item rather than to the PVF template it currently points
|
||||
// at. A [limited cube] that swaps one equipment template for another has to
|
||||
// carry them across, or the transformation would silently strip them.
|
||||
//
|
||||
// The 90CN coupon text states the rule outright: 「称号变更后, 附在称号上的宝珠属性
|
||||
// 仍然有效, 并且在无法交易的称号上使用变更券时, 变更后的称号也无法交易。」
|
||||
//
|
||||
// Every key here is one the current EXE reads back out of the same 0x77 row, so
|
||||
// dropping it changes what the player sees:
|
||||
// - value_a / enchant_card_id / byte_12 / enchant_upgrade_count are the 宝珠
|
||||
// enchant card and its upgrade count, projected to raw +0x0E and +0x12;
|
||||
// - random_title_enchant_config_id / _row_id are the random title enchant
|
||||
// pair at raw +0x61 / +0x63;
|
||||
// - seal_flag is the 封装 mark at raw +0x0D.
|
||||
//
|
||||
// See dnfbridge/current_random_title_enchant_wire.go and
|
||||
// inventory/owner.go setEnchantFields / setRandomTitleEnchantFields for the
|
||||
// writers of these same keys.
|
||||
var equipmentInstanceExtraKeys = []string{
|
||||
"value_a",
|
||||
"enchant_card_id",
|
||||
"byte_12",
|
||||
"enchant_upgrade_count",
|
||||
"random_title_enchant_config_id",
|
||||
"random_title_enchant_row_id",
|
||||
"seal_flag",
|
||||
}
|
||||
|
||||
func limitedCubeStackIsEquipment(stack dnfrepo.ItemStack) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(stack.Extra["item_kind"]), "equipment")
|
||||
}
|
||||
|
||||
// equipmentTemplateSwap reports whether this use replaces one equipment
|
||||
// instance with a different equipment template — the title change coupons.
|
||||
// A stackable input or a stackable result is a different situation: there the
|
||||
// old instance state (bead identity, seal) has no meaning on the new template
|
||||
// and must keep being dropped exactly as before.
|
||||
func equipmentTemplateSwap(input, result dnfrepo.ItemStack) bool {
|
||||
return limitedCubeStackIsEquipment(input) && limitedCubeStackIsEquipment(result)
|
||||
}
|
||||
|
||||
// carryEquipmentInstanceExtras copies the instance-owned keys from the consumed
|
||||
// equipment onto its replacement. The result slice is mutated in place and
|
||||
// returned; the PVF-derived keys of the new template always win, because this
|
||||
// function only fills keys the new template did not set.
|
||||
func carryEquipmentInstanceExtras(result, input map[string]string) map[string]string {
|
||||
if len(input) == 0 {
|
||||
return result
|
||||
}
|
||||
for _, key := range equipmentInstanceExtraKeys {
|
||||
if _, present := result[key]; present {
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(input[key])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if result == nil {
|
||||
result = make(map[string]string, len(equipmentInstanceExtraKeys))
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in new issue
Block a user