Files
90CNServer/server/internal/modules/dnf/creaturestate/inventory.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

456 lines
14 KiB
Go

package creaturestate
import (
"context"
"encoding/binary"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
var (
ErrInventoryNotFound = errors.New("creature inventory record not found")
ErrStateInvalid = errors.New("creature inventory state is invalid")
)
const (
petListType byte = 7
petInventorySlotMax int16 = 139
petEquipmentSlot int16 = 26
fullSatietyMicros = int64(100_000_000)
)
type inventoryCreature struct {
slot int16
stack dnfrepo.ItemStack
}
// ReconcileInventory creates typed state only for PVF-proven direct creature
// grants, then synchronizes every known inventory creature's durable slot.
// Callers must provide transaction-scoped repositories.
func ReconcileInventory(
ctx context.Context,
characterID string,
inventories dnfrepo.InventoryRepository,
equipment dnfrepo.EquipmentRepository,
pets dnfrepo.PetRepository,
) (bool, error) {
if inventories == nil || equipment == nil || pets == nil {
return false, fmt.Errorf("%w: repositories unavailable", ErrStateInvalid)
}
inventory, found, err := inventories.Load(ctx, characterID)
if err != nil {
return false, err
}
if !found {
return false, ErrInventoryNotFound
}
inventory = dnfrepo.CloneInventory(inventory)
if inventory.Slots == nil {
inventory.Slots = make(map[string]dnfrepo.ItemStack)
}
equipmentRecord, found, err := equipment.Load(ctx, characterID)
if err != nil {
return false, err
}
if !found {
equipmentRecord = dnfrepo.EquipmentRecord{CharacterID: characterID, Entries: make(map[string]dnfrepo.EquipmentEntry)}
} else {
equipmentRecord = dnfrepo.CloneEquipment(equipmentRecord)
}
petRecord, petRecordFound, err := pets.Load(ctx, characterID)
if err != nil {
return false, err
}
if !petRecordFound {
petRecord = dnfrepo.PetRecord{CharacterID: characterID, Entries: make(map[string]dnfrepo.PetEntry)}
} else {
petRecord = dnfrepo.ClonePet(petRecord)
petRecord.CharacterID = characterID
if petRecord.Entries == nil {
petRecord.Entries = make(map[string]dnfrepo.PetEntry)
}
}
creatures := inventoryCreatures(inventory)
entriesBySerial := make(map[uint32]string, len(petRecord.Entries))
used := make(map[uint32]struct{}, len(creatures)+len(petRecord.Entries)+1)
for key, entry := range petRecord.Entries {
serial, ok := petEntrySerial(key, entry)
if !ok {
continue
}
if previous, duplicate := entriesBySerial[serial]; duplicate && previous != key {
return false, fmt.Errorf("%w: duplicate pet serial=%d keys=%s,%s", ErrStateInvalid, serial, previous, key)
}
entriesBySerial[serial] = key
used[serial] = struct{}{}
}
containerSerials := make(map[uint32]string, len(creatures)+1)
for _, creature := range creatures {
if serial, ok := stackSerial(creature.stack.Extra); ok {
location := fmt.Sprintf("7:%d", creature.slot)
if previous, duplicate := containerSerials[serial]; duplicate && previous != location {
return false, fmt.Errorf("%w: duplicate creature serial=%d locations=%s,%s", ErrStateInvalid, serial, previous, location)
}
containerSerials[serial] = location
used[serial] = struct{}{}
}
}
if worn, ok := equipmentRecord.Entries[strconv.Itoa(int(petEquipmentSlot))]; ok && worn.ItemID > 0 {
if serial, ok := equipmentSerial(worn); ok {
location := "3:26"
if previous, duplicate := containerSerials[serial]; duplicate && previous != location {
return false, fmt.Errorf("%w: duplicate creature serial=%d locations=%s,%s", ErrStateInvalid, serial, previous, location)
}
containerSerials[serial] = location
used[serial] = struct{}{}
}
}
inventoryChanged := false
equipmentChanged := false
petChanged := false
for _, creature := range creatures {
stack := creature.stack
// 蛋不是宠物。历史上 `equipment/creature/` 下的蛋被 isDirectCreatureGrant 当成
// "直接发放的宠物",于是分配了 serial、写了 creature_serial_or_handle,还建出
// 宠物记录;丢弃侧随后按"已绑定宠物"拒绝 —— 现场就是「宠物蛋无法丢弃」。
// 这里既不再新建,也顺手自愈历史脏数据(见 healPetEggRow)。
if IsPetEgg(stack.ItemID) {
healedInventory, healedPet := healPetEggRow(&inventory, &petRecord, entriesBySerial, used, creature, stack)
inventoryChanged = inventoryChanged || healedInventory
petChanged = petChanged || healedPet
continue
}
serial, hasSerial := stackSerial(stack.Extra)
entryKey := ""
if hasSerial {
entryKey = entriesBySerial[serial]
}
if entryKey == "" {
if !isDirectCreatureGrant(stack) {
continue
}
if !hasSerial {
serial, err = allocateSerial(used)
if err != nil {
return false, err
}
stack.Extra = cloneExtra(stack.Extra)
stack.Extra["creature_serial_or_handle"] = strconv.FormatUint(uint64(serial), 10)
stack.Extra["creature_key"] = strconv.FormatUint(uint64(serial), 10)
inventory.Slots[slotKey(creature.slot)] = stack
inventoryChanged = true
}
entryKey = strconv.FormatUint(uint64(serial), 10)
if existing, occupied := petRecord.Entries[entryKey]; occupied && existing.ItemID != stack.ItemID {
return false, fmt.Errorf("%w: pet key=%s item=%d inventory_item=%d", ErrStateInvalid, entryKey, existing.ItemID, stack.ItemID)
}
petRecord.Entries[entryKey] = dnfrepo.PetEntry{
PetKey: entryKey,
CreatureKey: serial,
ItemID: stack.ItemID,
SourceListType: petListType,
SourceSlotIndex: creature.slot,
Satiety: 100,
SatietyMicros: fullSatietyMicros,
Level: 1,
Extra: cloneExtra(stack.Extra),
}
entriesBySerial[serial] = entryKey
used[serial] = struct{}{}
petChanged = true
continue
}
entry := petRecord.Entries[entryKey]
if entry.ItemID != stack.ItemID {
return false, fmt.Errorf("%w: serial=%d pet_item=%d inventory_item=%d", ErrStateInvalid, serial, entry.ItemID, stack.ItemID)
}
if entry.SourceListType != petListType || entry.SourceSlotIndex != creature.slot {
entry.SourceListType = petListType
entry.SourceSlotIndex = creature.slot
petRecord.Entries[entryKey] = entry
petChanged = true
}
if syncPetStackEnchant(&stack, entry) {
inventory.Slots[slotKey(creature.slot)] = stack
inventoryChanged = true
}
}
if worn, ok := equipmentRecord.Entries[strconv.Itoa(int(petEquipmentSlot))]; ok && worn.ItemID > 0 {
if serial, serialOK := equipmentSerial(worn); serialOK {
if key := entriesBySerial[serial]; key != "" {
if entry, entryOK := petRecord.Entries[key]; entryOK && syncPetEquipmentEnchant(&worn, entry) {
equipmentRecord.Entries[strconv.Itoa(int(petEquipmentSlot))] = worn
equipmentChanged = true
}
}
}
}
if inventoryChanged {
inventory.CharacterID = characterID
inventory.UpdatedAt = time.Now()
if err := dnfrepo.SaveInventoryFields(ctx, inventories, inventory, dnfrepo.InventoryFieldSlots); err != nil {
return false, err
}
}
if petChanged {
petRecord.UpdatedAt = time.Now()
if err := dnfrepo.SavePetFields(ctx, pets, petRecord, dnfrepo.PetFieldEntries); err != nil {
return false, err
}
}
if equipmentChanged {
equipmentRecord.CharacterID = characterID
equipmentRecord.UpdatedAt = time.Now()
if err := dnfrepo.SaveEquipmentFields(ctx, equipment, equipmentRecord, dnfrepo.EquipmentFieldEntries); err != nil {
return false, err
}
}
return inventoryChanged || equipmentChanged || petChanged, nil
}
func syncPetStackEnchant(stack *dnfrepo.ItemStack, entry dnfrepo.PetEntry) bool {
if stack == nil {
return false
}
cardID, upgrade, ok := petEnchantFields(entry.Extra)
if !ok {
return false
}
changed := false
stack.Extra = cloneExtra(stack.Extra)
for key, value := range map[string]string{
"pet_enchant_card_item_id": strconv.FormatUint(uint64(cardID), 10),
"enchant_card_id": strconv.FormatUint(uint64(cardID), 10),
"value_a": strconv.FormatUint(uint64(cardID), 10),
"pet_enchant_upgrade_count": strconv.Itoa(int(upgrade)),
"enchant_upgrade_count": strconv.Itoa(int(upgrade)),
"byte_12": strconv.Itoa(int(upgrade)),
} {
if stack.Extra[key] != value {
stack.Extra[key] = value
changed = true
}
}
if len(stack.RawEntry) >= 0x13 {
raw := append([]byte(nil), stack.RawEntry...)
if binary.LittleEndian.Uint32(raw[0x0E:0x12]) != cardID || raw[0x12] != upgrade {
binary.LittleEndian.PutUint32(raw[0x0E:0x12], cardID)
raw[0x12] = upgrade
stack.RawEntry = raw
changed = true
}
}
return changed
}
func syncPetEquipmentEnchant(entry *dnfrepo.EquipmentEntry, pet dnfrepo.PetEntry) bool {
if entry == nil {
return false
}
cardID, upgrade, ok := petEnchantFields(pet.Extra)
if !ok {
return false
}
changed := false
entry.Extra = cloneExtra(entry.Extra)
for key, value := range map[string]string{
"pet_enchant_card_item_id": strconv.FormatUint(uint64(cardID), 10),
"enchant_card_id": strconv.FormatUint(uint64(cardID), 10),
"value_a": strconv.FormatUint(uint64(cardID), 10),
"pet_enchant_upgrade_count": strconv.Itoa(int(upgrade)),
"enchant_upgrade_count": strconv.Itoa(int(upgrade)),
"byte_12": strconv.Itoa(int(upgrade)),
} {
if entry.Extra[key] != value {
entry.Extra[key] = value
changed = true
}
}
if len(entry.RawEntry) >= 0x15 {
raw := append([]byte(nil), entry.RawEntry...)
if binary.LittleEndian.Uint32(raw[0x10:0x14]) != cardID || raw[0x14] != upgrade {
binary.LittleEndian.PutUint32(raw[0x10:0x14], cardID)
raw[0x14] = upgrade
entry.RawEntry = raw
changed = true
}
}
return changed
}
func petEnchantFields(extra map[string]string) (uint32, byte, bool) {
var first uint64
found := false
for _, key := range []string{"pet_enchant_card_item_id", "enchant_card_id", "value_a"} {
value, err := strconv.ParseUint(strings.TrimSpace(extra[key]), 0, 32)
if err != nil {
continue
}
if !found {
first, found = value, true
}
if value != 0 {
first = value
break
}
}
if !found || first == 0 {
return 0, 0, false
}
upgrade := uint64(0)
for _, key := range []string{"pet_enchant_upgrade_count", "enchant_upgrade_count", "byte_12"} {
if value, err := strconv.ParseUint(strings.TrimSpace(extra[key]), 0, 8); err == nil {
upgrade = value
break
}
}
return uint32(first), byte(upgrade), true
}
func inventoryCreatures(record dnfrepo.InventoryRecord) []inventoryCreature {
out := make([]inventoryCreature, 0)
for key, stack := range record.Slots {
listType, slot, ok := parseSlotKey(key)
if !ok || listType != petListType || slot < 0 || slot > petInventorySlotMax || stack.ItemID <= 0 || stack.Count <= 0 {
continue
}
out = append(out, inventoryCreature{slot: slot, stack: stack})
}
sort.Slice(out, func(i, j int) bool { return out[i].slot < out[j].slot })
return out
}
// healPetEggRow 自愈历史上被误当成宠物发放的蛋行:删掉旧版本写在行上的宠物绑定
// extra,并移除它顺手建出的宠物记录。
//
// 如果那条宠物记录正好是**当前出战宠物**,就整行不动 —— 蛋不可能出战,这种状态
// 只可能来自更早的错误数据,猜着清理比留着更危险。
func healPetEggRow(
inventory *dnfrepo.InventoryRecord,
petRecord *dnfrepo.PetRecord,
entriesBySerial map[uint32]string,
used map[uint32]struct{},
creature inventoryCreature,
stack dnfrepo.ItemStack,
) (inventoryChanged bool, petChanged bool) {
serial, hasSerial := stackSerial(stack.Extra)
if !hasSerial {
return false, false
}
entryKey := entriesBySerial[serial]
if entryKey != "" && petRecord.EquippedKey == entryKey {
return false, false
}
if entryKey != "" {
delete(petRecord.Entries, entryKey)
delete(entriesBySerial, serial)
petChanged = true
}
// 释放 serial,避免它继续占号。
delete(used, serial)
stack.Extra = cloneExtra(stack.Extra)
for _, key := range []string{
"creature_serial_or_handle", "creature_key", "creature_serial", "pet_serial",
} {
delete(stack.Extra, key)
}
inventory.Slots[slotKey(creature.slot)] = stack
return true, petChanged
}
func isDirectCreatureGrant(stack dnfrepo.ItemStack) bool {
if stack.Count != 1 {
return false
}
kind := strings.ToLower(strings.TrimSpace(stack.Extra["item_kind"]))
equipmentType := strings.ToLower(strings.Trim(strings.TrimSpace(stack.Extra["equipment_type"]), "[]"))
path := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(stack.Extra["pvf_path"]), "\\", "/"))
return kind == "equipment" && equipmentType == "creature" && strings.HasPrefix(path, "equipment/creature/")
}
func allocateSerial(used map[uint32]struct{}) (uint32, error) {
for serial := uint32(1); serial != 0; serial++ {
if _, exists := used[serial]; !exists {
used[serial] = struct{}{}
return serial, nil
}
}
return 0, fmt.Errorf("%w: no creature serial available", ErrStateInvalid)
}
func equipmentSerial(entry dnfrepo.EquipmentEntry) (uint32, bool) {
if len(entry.RawEntry) >= 28 {
left := binary.LittleEndian.Uint32(entry.RawEntry[5:9])
right := binary.LittleEndian.Uint32(entry.RawEntry[24:28])
if left != 0 && left == right {
return left, true
}
}
return stackSerial(entry.Extra)
}
func stackSerial(extra map[string]string) (uint32, bool) {
for _, key := range []string{"creature_serial_or_handle", "creature_serial", "pet_serial", "serial", "handle", "instance_value", "item_uid"} {
value, err := strconv.ParseUint(strings.TrimSpace(extra[key]), 0, 32)
if err == nil && value != 0 {
return uint32(value), true
}
}
return 0, false
}
func petEntrySerial(key string, entry dnfrepo.PetEntry) (uint32, bool) {
if entry.CreatureKey != 0 {
return entry.CreatureKey, true
}
value, err := strconv.ParseUint(strings.TrimSpace(entry.PetKey), 0, 32)
if err != nil || value == 0 {
value, err = strconv.ParseUint(strings.TrimSpace(key), 0, 32)
}
if err != nil || value == 0 {
return 0, false
}
return uint32(value), true
}
func parseSlotKey(key string) (byte, int16, bool) {
listRaw, slotRaw, ok := strings.Cut(key, ":")
if !ok {
return 0, 0, false
}
listType, err := strconv.ParseUint(listRaw, 10, 8)
if err != nil {
return 0, 0, false
}
slot, err := strconv.ParseInt(slotRaw, 10, 16)
if err != nil {
return 0, 0, false
}
return byte(listType), int16(slot), true
}
func slotKey(slot int16) string {
return fmt.Sprintf("%d:%d", petListType, slot)
}
func cloneExtra(extra map[string]string) map[string]string {
out := make(map[string]string, len(extra)+2)
for key, value := range extra {
out[key] = value
}
return out
}