服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
673 lines
24 KiB
Go
673 lines
24 KiB
Go
// 本文件负责把当前 EXE 的 0x166 容器重建包从角色库存仓储动态构造出来。
|
||
package dnfbridge
|
||
|
||
import (
|
||
"context"
|
||
"encoding/binary"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
|
||
dnfachievement "longheng.io/server/internal/modules/dnf/achievement"
|
||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||
dnfquest "longheng.io/server/internal/modules/dnf/quest"
|
||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||
)
|
||
|
||
type currentRequestOverseerRow struct {
|
||
slot uint16
|
||
itemID uint32
|
||
amount uint32
|
||
packed byte
|
||
valueB uint16
|
||
createFlag byte
|
||
valueA uint32
|
||
unused byte
|
||
valueC byte
|
||
valueD uint16
|
||
}
|
||
|
||
type currentRequestOverseerChunk struct {
|
||
refresh byte
|
||
selector uint16
|
||
index uint32
|
||
rows []currentRequestOverseerRow
|
||
}
|
||
|
||
type currentInsertOverseerRow struct {
|
||
achievementID uint32
|
||
p1 uint16
|
||
p2 uint16
|
||
p3 uint16
|
||
}
|
||
|
||
const (
|
||
legacyAchievementChunksTable = "legacy_character_achievement_chunks"
|
||
legacyAchievementCompleteTable = "legacy_character_achievement_complete"
|
||
currentRequestOverseerFixedRowWireSize = 22
|
||
currentRequestOverseerRawLenWireSize = 4
|
||
currentInsertOverseerRowWireSize = 10
|
||
currentInsertOverseerTailWireSize = 16
|
||
)
|
||
|
||
func buildCurrentRequestOverseerBodyWithRows(refresh byte, selector uint16, listIndex uint32, rows []currentRequestOverseerRow) []byte {
|
||
var writer packetWriter
|
||
writer.writeByte(refresh)
|
||
writer.writeUint16(selector)
|
||
writer.writeUint32(listIndex)
|
||
writer.writeUint32(uint32(len(rows)))
|
||
for _, row := range rows {
|
||
writer.writeUint16(row.slot)
|
||
writer.writeUint32(row.itemID)
|
||
writer.writeUint32(row.amount)
|
||
writer.writeByte(row.packed)
|
||
writer.writeUint16(row.valueB)
|
||
writer.writeByte(row.createFlag)
|
||
writer.writeUint32(row.valueA)
|
||
writer.writeByte(row.unused)
|
||
writer.writeByte(row.valueC)
|
||
writer.writeUint16(row.valueD)
|
||
// Current EXE sub_1D76D00 calls sub_3457C50 after every fixed
|
||
// 22-byte row. sub_3457C50 always reads u32 raw_len followed by raw.
|
||
// No repository owns that optional block yet, so encode its proved
|
||
// empty state instead of truncating the row at the fixed fields.
|
||
writer.writeUint32(0)
|
||
}
|
||
return writer.bytes()
|
||
}
|
||
|
||
func buildCurrentInsertOverseerBodyWithRows(rows []currentInsertOverseerRow) []byte {
|
||
var writer packetWriter
|
||
writer.writeUint32(uint32(len(rows)))
|
||
for _, row := range rows {
|
||
writer.writeUint32(row.achievementID)
|
||
writer.writeUint16(row.p1)
|
||
writer.writeUint16(row.p2)
|
||
writer.writeUint16(row.p3)
|
||
}
|
||
// Current EXE sub_1D625C0 reads this fixed 16-byte block after all
|
||
// count*10-byte rows, including when count is zero. Each nonzero dword is
|
||
// treated as an object/achievement id; zero is the real empty state.
|
||
writer.writeZero(currentInsertOverseerTailWireSize)
|
||
return writer.bytes()
|
||
}
|
||
|
||
func parseCurrentRequestOverseerHeader(body []byte) (byte, uint16, uint32, bool) {
|
||
if len(body) < 11 {
|
||
return 0, 0, 0, false
|
||
}
|
||
return body[0], binary.LittleEndian.Uint16(body[1:3]), binary.LittleEndian.Uint32(body[3:7]), true
|
||
}
|
||
|
||
func (s *Service) buildCurrentRequestOverseerBodyForSession(ctx context.Context, session *gameSession, fallback []byte) []byte {
|
||
refresh, selector, listIndex, ok := parseCurrentRequestOverseerHeader(fallback)
|
||
if !ok || session == nil || session.selectedCharacterID == 0 {
|
||
return fallback
|
||
}
|
||
repos, ok := s.repositoryGroup()
|
||
if !ok {
|
||
return fallback
|
||
}
|
||
characterID := strconv.Itoa(int(session.selectedCharacterID))
|
||
rows := currentRequestOverseerRowsFromInventory(ctx, repos.Inventory, characterID, selector, listIndex)
|
||
if len(rows) > 0 {
|
||
body := buildCurrentRequestOverseerBodyWithRows(refresh, selector, listIndex, rows)
|
||
s.logPacketEvent("game-upper-current-overseer-titlebook-page-built",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"msg_id", uint16(dnfenum.CmdPacketRequestOverseer),
|
||
"selector", selector,
|
||
"list_index", listIndex,
|
||
"source", "inventory_title_book",
|
||
"row_count", len(rows),
|
||
"body_len", len(body))
|
||
return body
|
||
}
|
||
if repos.LegacyUserInfo != nil {
|
||
if chunk, ok := s.currentRequestOverseerChunkFromLegacyAchievements(ctx, session, repos.LegacyUserInfo, characterID, listIndex); ok {
|
||
body := buildCurrentRequestOverseerBodyWithRows(chunk.refresh, chunk.selector, chunk.index, chunk.rows)
|
||
s.logPacketEvent("game-upper-current-overseer-titlebook-page-built",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"msg_id", uint16(dnfenum.CmdPacketRequestOverseer),
|
||
"selector", chunk.selector,
|
||
"list_index", chunk.index,
|
||
"source", legacyAchievementChunksTable,
|
||
"row_count", len(chunk.rows),
|
||
"body_len", len(body),
|
||
"fixed_row_size", currentRequestOverseerFixedRowWireSize,
|
||
"row_wire_size", currentRequestOverseerFixedRowWireSize+currentRequestOverseerRawLenWireSize,
|
||
"raw_len", 0)
|
||
return body
|
||
}
|
||
}
|
||
body := buildCurrentRequestOverseerBodyWithRows(refresh, selector, listIndex, nil)
|
||
s.logPacketEvent("game-upper-current-overseer-titlebook-page-empty",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"msg_id", uint16(dnfenum.CmdPacketRequestOverseer),
|
||
"selector", selector,
|
||
"list_index", listIndex,
|
||
"source", "durable_title_book",
|
||
"body_len", len(body))
|
||
return body
|
||
}
|
||
|
||
func (s *Service) buildCurrentInsertOverseerBodyForSession(ctx context.Context, session *gameSession, fallback []byte) []byte {
|
||
if session == nil || session.selectedCharacterID == 0 {
|
||
return fallback
|
||
}
|
||
repos, ok := s.repositoryGroup()
|
||
if !ok {
|
||
return fallback
|
||
}
|
||
characterID := strconv.Itoa(int(session.selectedCharacterID))
|
||
if owner, err := dnfachievement.NewOwner(repos, nil); err == nil {
|
||
if removed, repairErr := owner.RepairLegacyRows(ctx, session.selectedCharacterID); repairErr != nil {
|
||
s.logPacketEvent("game-upper-insert-overseer-legacy-progress-repair-failed",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"err", repairErr)
|
||
} else if removed > 0 {
|
||
s.logPacketEvent("game-upper-insert-overseer-legacy-progress-repaired",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"removed", removed)
|
||
}
|
||
}
|
||
entries := make([]currentInsertOverseerRow, 0)
|
||
seen := make(map[uint32]struct{})
|
||
record := dnfrepo.QuestRecord{CharacterID: characterID}
|
||
if repos.Quest != nil {
|
||
loaded, found, err := repos.Quest.Load(ctx, characterID)
|
||
if err != nil {
|
||
s.logPacketEvent("game-upper-insert-overseer-achievement-load-failed",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"msg_id", uint16(dnfenum.CmdPacketInsertOverseer),
|
||
"source", "quest_progress",
|
||
"err", err)
|
||
return fallback
|
||
}
|
||
if found {
|
||
record = loaded
|
||
for _, progress := range dnfachievement.Snapshot(record) {
|
||
id := uint32(progress.QuestID)
|
||
entries = append(entries, currentInsertOverseerRow{
|
||
achievementID: id,
|
||
p1: progress.Remain1,
|
||
p2: progress.Remain2,
|
||
p3: progress.Remain3,
|
||
})
|
||
seen[id] = struct{}{}
|
||
}
|
||
}
|
||
}
|
||
if repos.LegacyUserInfo != nil {
|
||
rows, err := repos.LegacyUserInfo.SelectRows(ctx, characterID, legacyAchievementCompleteTable,
|
||
[]string{"sort_order", "achievement_id", "p1", "p2", "p3"},
|
||
[]string{"sort_order"})
|
||
if err == nil {
|
||
for _, row := range rows {
|
||
achievementID, ok := currentRequestOverseerLegacyUint(row, "achievement_id", 32)
|
||
if !ok {
|
||
continue
|
||
}
|
||
id := uint32(achievementID)
|
||
if _, duplicate := seen[id]; duplicate {
|
||
continue
|
||
}
|
||
p1, _ := currentRequestOverseerLegacyUint(row, "p1", 16)
|
||
p2, _ := currentRequestOverseerLegacyUint(row, "p2", 16)
|
||
p3, _ := currentRequestOverseerLegacyUint(row, "p3", 16)
|
||
entries = append(entries, currentInsertOverseerRow{
|
||
achievementID: id,
|
||
p1: uint16(p1),
|
||
p2: uint16(p2),
|
||
p3: uint16(p3),
|
||
})
|
||
seen[id] = struct{}{}
|
||
}
|
||
}
|
||
}
|
||
seededCount := 0
|
||
if repos.Character != nil {
|
||
character, found, err := repos.Character.Load(ctx, characterID)
|
||
if err != nil {
|
||
s.logPacketEvent("game-upper-insert-overseer-title-definition-character-load-failed",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"err", err)
|
||
} else if found {
|
||
job, validJob := characterJobByte(character)
|
||
catalog, catalogErr := s.loadQuestCatalog(ctx)
|
||
mapping := s.currentTitleBookMapping()
|
||
if !validJob {
|
||
s.logPacketEvent("game-upper-insert-overseer-title-definition-skipped",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"reason", "invalid_character_job")
|
||
} else if catalogErr != nil {
|
||
s.logPacketEvent("game-upper-insert-overseer-title-definition-load-failed",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"err", catalogErr)
|
||
} else if mapping != nil {
|
||
definitions := catalog.TitleAchievementDefinitions(dnfquest.CharacterEligibility{
|
||
Level: character.Level,
|
||
Job: int(job),
|
||
GrowType: int(numericCharacterStatValue(character, "grow_type")),
|
||
})
|
||
seeded := currentInitialTitleAchievementRows(definitions, mapping, seen)
|
||
entries = append(entries, seeded...)
|
||
seededCount = len(seeded)
|
||
}
|
||
}
|
||
}
|
||
sort.Slice(entries, func(i, j int) bool { return entries[i].achievementID < entries[j].achievementID })
|
||
body := buildCurrentInsertOverseerBodyWithRows(entries)
|
||
s.logPacketEvent("game-upper-insert-overseer-achievement-built",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"msg_id", uint16(dnfenum.CmdPacketInsertOverseer),
|
||
"source", "quest_progress",
|
||
"row_count", len(entries),
|
||
"pvf_initial_row_count", seededCount,
|
||
"body_len", len(body),
|
||
"row_size", currentInsertOverseerRowWireSize,
|
||
"tail_size", currentInsertOverseerTailWireSize)
|
||
return body
|
||
}
|
||
|
||
func currentInitialTitleAchievementRows(
|
||
definitions []dnfquest.Definition,
|
||
mapping map[int32]titleBookMappingEntry,
|
||
seen map[uint32]struct{},
|
||
) []currentInsertOverseerRow {
|
||
rows := make([]currentInsertOverseerRow, 0, len(definitions))
|
||
for _, definition := range definitions {
|
||
if definition.ID <= 0 || definition.ID > int64(^uint32(0)>>1) {
|
||
continue
|
||
}
|
||
questID := int32(definition.ID)
|
||
if _, mapped := mapping[questID]; !mapped {
|
||
continue
|
||
}
|
||
id := uint32(questID)
|
||
if _, exists := seen[id]; exists {
|
||
continue
|
||
}
|
||
targets := currentAchievementTargets(definition)
|
||
if targets[0] == 0 && targets[1] == 0 && targets[2] == 0 {
|
||
continue
|
||
}
|
||
rows = append(rows, currentInsertOverseerRow{
|
||
achievementID: id,
|
||
p1: targets[0],
|
||
p2: targets[1],
|
||
p3: targets[2],
|
||
})
|
||
seen[id] = struct{}{}
|
||
}
|
||
return rows
|
||
}
|
||
|
||
func (s *Service) currentRequestOverseerChunkFromLegacyAchievements(ctx context.Context, session *gameSession, repo dnfrepo.LegacyUserInfoRepository, characterID string, listIndex uint32) (currentRequestOverseerChunk, bool) {
|
||
if repo == nil {
|
||
return currentRequestOverseerChunk{}, false
|
||
}
|
||
rows, err := repo.SelectRows(ctx, characterID, legacyAchievementChunksTable,
|
||
[]string{"chunk_index", "mode_byte", "owner_id16", "entries_blob"},
|
||
[]string{"chunk_index"})
|
||
if err != nil {
|
||
if session != nil {
|
||
s.logPacketEvent("game-upper-current-overseer-titlebook-load-failed",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"msg_id", uint16(dnfenum.CmdPacketRequestOverseer),
|
||
"list_index", listIndex,
|
||
"source", legacyAchievementChunksTable,
|
||
"err", err)
|
||
}
|
||
return currentRequestOverseerChunk{}, false
|
||
}
|
||
for _, row := range rows {
|
||
chunkIndex, ok := currentRequestOverseerLegacyUint(row, "chunk_index", 32)
|
||
if !ok || uint32(chunkIndex) != listIndex {
|
||
continue
|
||
}
|
||
refresh, _ := currentRequestOverseerLegacyUint(row, "mode_byte", 8)
|
||
selector, _ := currentRequestOverseerLegacyUint(row, "owner_id16", 16)
|
||
entries, trailing := currentRequestOverseerRowsFromLegacyAchievementBlob(rowBytes(row, "entries_blob"))
|
||
if trailing > 0 && session != nil {
|
||
s.logPacketEvent("game-upper-current-overseer-titlebook-partial-entry-ignored",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"msg_id", uint16(dnfenum.CmdPacketRequestOverseer),
|
||
"list_index", listIndex,
|
||
"source", legacyAchievementChunksTable,
|
||
"trailing_bytes", trailing)
|
||
}
|
||
return currentRequestOverseerChunk{
|
||
refresh: byte(refresh),
|
||
selector: uint16(selector),
|
||
index: listIndex,
|
||
rows: entries,
|
||
}, true
|
||
}
|
||
return currentRequestOverseerChunk{}, false
|
||
}
|
||
|
||
func currentRequestOverseerRowsFromLegacyAchievementBlob(blob []byte) ([]currentRequestOverseerRow, int) {
|
||
const rowSize = currentRequestOverseerFixedRowWireSize
|
||
if len(blob) == 0 {
|
||
return nil, 0
|
||
}
|
||
count := len(blob) / rowSize
|
||
rows := make([]currentRequestOverseerRow, 0, count)
|
||
for offset := 0; offset+rowSize <= len(blob); offset += rowSize {
|
||
raw := blob[offset : offset+rowSize]
|
||
rows = append(rows, currentRequestOverseerRow{
|
||
slot: binary.LittleEndian.Uint16(raw[0:2]),
|
||
itemID: binary.LittleEndian.Uint32(raw[2:6]),
|
||
amount: binary.LittleEndian.Uint32(raw[6:10]),
|
||
packed: raw[10],
|
||
valueB: binary.LittleEndian.Uint16(raw[11:13]),
|
||
createFlag: raw[13],
|
||
valueA: binary.LittleEndian.Uint32(raw[14:18]),
|
||
unused: raw[18],
|
||
valueC: raw[19],
|
||
valueD: binary.LittleEndian.Uint16(raw[20:22]),
|
||
})
|
||
}
|
||
return rows, len(blob) % rowSize
|
||
}
|
||
|
||
func currentRequestOverseerRowsFromInventory(ctx context.Context, repo dnfrepo.InventoryRepository, characterID string, selector uint16, listIndex uint32) []currentRequestOverseerRow {
|
||
if repo == nil || selector != 0 || listIndex >= 5 {
|
||
return nil
|
||
}
|
||
record, found, err := repo.Load(ctx, characterID)
|
||
if err != nil || !found {
|
||
return nil
|
||
}
|
||
rows := make([]currentRequestOverseerRow, 0)
|
||
for key, stack := range record.Slots {
|
||
if stack.ItemID <= 0 || dnfachievement.IsLegacyInventoryProgress(key, stack) {
|
||
continue
|
||
}
|
||
listType, slot, ok := parseSceneInventorySlotKey(key)
|
||
if !ok || listType != 100 || slot < 0 ||
|
||
uint32(slot/currentTitleBookSlotBase) != listIndex {
|
||
continue
|
||
}
|
||
rows = append(rows, currentRequestOverseerRow{
|
||
slot: uint16(slot % currentTitleBookSlotBase),
|
||
itemID: sceneInventoryUint32FromInt64(stack.ItemID),
|
||
amount: sceneInventoryStackAmount(stack),
|
||
packed: sceneInventoryExtraByte(stack.Extra, "packed_flag_byte", "packed_flag", "packed", "ext_data0", "ext0"),
|
||
valueB: sceneInventoryExtraUint16(stack.Extra, "value_b_word", "durability", "max_durability"),
|
||
// 这一位是**封装标记**(不是「新建标记」):0 = 已解封,1 = 封装。
|
||
// 依据官方参考服务端 df_game_r(1):
|
||
// * CTitleBook::_putItemData(0x08641A6A) 逐字段写行,封那位写的是
|
||
// Inven_Item 自己的 +0 字节(put_byte([item+0]),落在行内 0x0D);
|
||
// * Inven_Item::reset(0x80CB7D8) 把 +0 清 0,只有「再封装」才写 1
|
||
// (本仓库对应 itemgrade/reseal.go 的 applyResealState:RawEntry[0x0D]=1
|
||
// 且 seal_flag/seal=1;而 Reseal 要求 SealFlag==0 才允许封装)。
|
||
// 2026-09-18 玩家报障「已解封的称号放进称号簿、小退重登又变成封装」就是
|
||
// 这里写死读 extra create_flag(没有任何代码会写它)并缺省 1 造成的:
|
||
// 同一件称号背包行给 0(解封)、簿里给 1(封装)。改成和背包行同一个推导
|
||
// (currentItemListBindFlag:extra seal_flag/seal/bind_flag/bind → Bind → 0),
|
||
// 簿内状态才会跟着物品本身走。
|
||
createFlag: currentItemListBindFlag(stack.Bind, stack.Extra),
|
||
valueA: sceneInventoryExtraUint32(stack.Extra, "value_a", "instance_value", "item_uid", "serial", "count_or_instance"),
|
||
unused: sceneInventoryExtraByte(stack.Extra, "unused_byte_after_value_a", "unused"),
|
||
valueC: sceneInventoryExtraByte(stack.Extra, "value_c"),
|
||
valueD: sceneInventoryExtraUint16(stack.Extra, "value_d"),
|
||
})
|
||
}
|
||
sort.Slice(rows, func(i, j int) bool { return rows[i].slot < rows[j].slot })
|
||
return rows
|
||
}
|
||
|
||
func (s *Service) currentRequestOverseerRowsFromLegacy(ctx context.Context, session *gameSession, repo dnfrepo.LegacyInventoryRepository, characterID string, selector uint16, listIndex uint32) []currentRequestOverseerRow {
|
||
if repo == nil || selector != 0 || listIndex > 255 {
|
||
return nil
|
||
}
|
||
items, err := repo.SelectItems(ctx, characterID, byte(listIndex))
|
||
if err != nil {
|
||
s.logPacketEvent("game-upper-current-overseer-legacy-inventory-load-failed",
|
||
"conn_id", session.connID,
|
||
"char_id", session.selectedCharacterID,
|
||
"list_index", listIndex,
|
||
"err", err)
|
||
return nil
|
||
}
|
||
return currentRequestOverseerRowsFromLegacyItems(items)
|
||
}
|
||
|
||
func currentRequestOverseerRows(record dnfrepo.InventoryRecord, selector uint16, listIndex uint32) []currentRequestOverseerRow {
|
||
if selector != 0 || listIndex > 255 {
|
||
return nil
|
||
}
|
||
listType := byte(listIndex)
|
||
rows := make([]currentRequestOverseerRow, 0)
|
||
rows = append(rows, currentRequestOverseerRowsFromMap(record.Slots, listType)...)
|
||
if listType == 2 {
|
||
rows = append(rows, currentRequestOverseerRowsFromMap(record.Warehouse, listType)...)
|
||
}
|
||
sort.Slice(rows, func(i, j int) bool {
|
||
return rows[i].slot < rows[j].slot
|
||
})
|
||
return rows
|
||
}
|
||
|
||
func currentRequestOverseerRowsFromLegacyItems(items []dnfrepo.LegacyInventoryItem) []currentRequestOverseerRow {
|
||
if len(items) == 0 {
|
||
return nil
|
||
}
|
||
rows := make([]currentRequestOverseerRow, 0, len(items))
|
||
for _, item := range items {
|
||
if item.SlotIndex < 0 || item.ItemTemplateID <= 0 {
|
||
continue
|
||
}
|
||
extra := item.Extra
|
||
rows = append(rows, currentRequestOverseerRow{
|
||
slot: uint16(item.SlotIndex),
|
||
itemID: sceneInventoryUint32FromInt64(item.ItemTemplateID),
|
||
amount: sceneInventoryLegacyAmount(item),
|
||
packed: sceneInventoryLegacyPacked(item),
|
||
valueB: sceneInventoryLegacyUint16(item.Durability, extra, "value_b_word", "durability", "max_durability"),
|
||
// 第 13 字节是贴膜(封装)位,不是"已创建"标志:客户端 0x1D76D00 把它当布尔
|
||
// 传给 0x2759A70,只对可封装物品写进 item+0x5C。缺 create_flag 就默认 1,
|
||
// 会把解封过的称号画成"封装"。与上面 currentRequestOverseerRowsFromInventory
|
||
// 同一个取值口径(上游 2f0927b)。legacy 行用 legacy 自己的 SealFlag 优先。
|
||
createFlag: sceneInventoryLegacyByte(item.SealFlag, extra, "seal_flag", "seal"),
|
||
valueA: sceneInventoryLegacyUint32(item.InstanceValue, extra, "value_a", "instance_value", "item_uid", "serial", "count_or_instance"),
|
||
unused: sceneInventoryExtraByte(extra, "unused_byte_after_value_a", "unused"),
|
||
valueC: sceneInventoryLegacyByte(item.OptionValue, extra, "value_c"),
|
||
valueD: sceneInventoryLegacyUint16(item.Marker16, extra, "value_d"),
|
||
})
|
||
}
|
||
sort.Slice(rows, func(i, j int) bool {
|
||
return rows[i].slot < rows[j].slot
|
||
})
|
||
return rows
|
||
}
|
||
|
||
func currentRequestOverseerRowsFromMap(items map[string]dnfrepo.ItemStack, listType byte) []currentRequestOverseerRow {
|
||
if len(items) == 0 {
|
||
return nil
|
||
}
|
||
rows := make([]currentRequestOverseerRow, 0, len(items))
|
||
for key, stack := range items {
|
||
keyListType, slot, ok := parseSceneInventorySlotKey(key)
|
||
if !ok || keyListType != listType || slot < 0 || stack.ItemID <= 0 {
|
||
continue
|
||
}
|
||
rows = append(rows, currentRequestOverseerRow{
|
||
slot: uint16(slot),
|
||
itemID: sceneInventoryUint32FromInt64(stack.ItemID),
|
||
amount: sceneInventoryStackAmount(stack),
|
||
packed: sceneInventoryExtraByte(stack.Extra, "packed_flag_byte", "packed_flag", "packed", "ext_data0", "ext0"),
|
||
valueB: sceneInventoryExtraUint16(stack.Extra, "value_b_word", "durability", "max_durability"),
|
||
// 同上:贴膜位跟着物品本身走,不再是"缺 create_flag 就 1"。
|
||
// 与 currentRequestOverseerRowsFromInventory 用同一个推导(上游 2f0927b)。
|
||
createFlag: currentItemListBindFlag(stack.Bind, stack.Extra),
|
||
valueA: sceneInventoryExtraUint32(stack.Extra, "value_a", "instance_value", "item_uid", "serial", "count_or_instance"),
|
||
unused: sceneInventoryExtraByte(stack.Extra, "unused_byte_after_value_a", "unused"),
|
||
valueC: sceneInventoryExtraByte(stack.Extra, "value_c"),
|
||
valueD: sceneInventoryExtraUint16(stack.Extra, "value_d"),
|
||
})
|
||
}
|
||
return rows
|
||
}
|
||
|
||
func currentRequestOverseerLegacyUint(row dnfrepo.LegacyUserInfoRow, column string, bits int) (uint64, bool) {
|
||
if row == nil {
|
||
return 0, false
|
||
}
|
||
raw := strings.TrimSpace(row[column])
|
||
if raw == "" {
|
||
return 0, false
|
||
}
|
||
value, err := strconv.ParseUint(raw, 0, bits)
|
||
if err != nil {
|
||
return 0, false
|
||
}
|
||
return value, true
|
||
}
|
||
|
||
func sceneInventoryLegacyAmount(item dnfrepo.LegacyInventoryItem) uint32 {
|
||
if value, ok := sceneInventoryExtraUint(item.Extra, "amount_or_count", "amount", "count", "stack", "quantity"); ok {
|
||
return sceneInventoryClampUint32(value)
|
||
}
|
||
if item.StackCount > 0 {
|
||
return sceneInventoryUint32FromInt64(item.StackCount)
|
||
}
|
||
return 1
|
||
}
|
||
|
||
func sceneInventoryLegacyPacked(item dnfrepo.LegacyInventoryItem) byte {
|
||
if value := sceneInventoryExtraByte(item.Extra, "packed_flag_byte", "packed_flag", "packed", "ext_data0", "ext0"); value != 0 {
|
||
return value
|
||
}
|
||
if item.SealFlag > 0 && item.SealFlag <= 255 {
|
||
return byte(item.SealFlag)
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func sceneInventoryLegacyByte(fallback int64, extra map[string]string, keys ...string) byte {
|
||
if value := sceneInventoryExtraByte(extra, keys...); value != 0 {
|
||
return value
|
||
}
|
||
if fallback > 0 && fallback <= 255 {
|
||
return byte(fallback)
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func sceneInventoryLegacyUint16(fallback int64, extra map[string]string, keys ...string) uint16 {
|
||
if value := sceneInventoryExtraUint16(extra, keys...); value != 0 {
|
||
return value
|
||
}
|
||
if fallback > 0 && fallback <= 65535 {
|
||
return uint16(fallback)
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func sceneInventoryLegacyUint32(fallback int64, extra map[string]string, keys ...string) uint32 {
|
||
if value := sceneInventoryExtraUint32(extra, keys...); value != 0 {
|
||
return value
|
||
}
|
||
return sceneInventoryUint32FromInt64(fallback)
|
||
}
|
||
|
||
func parseSceneInventorySlotKey(key string) (byte, int16, bool) {
|
||
listRaw, slotRaw, ok := strings.Cut(key, ":")
|
||
if !ok {
|
||
return 0, 0, false
|
||
}
|
||
listValue, err := strconv.ParseInt(strings.TrimSpace(listRaw), 10, 16)
|
||
if err != nil || listValue < 0 || listValue > 255 {
|
||
return 0, 0, false
|
||
}
|
||
slotValue, err := strconv.ParseInt(strings.TrimSpace(slotRaw), 10, 16)
|
||
if err != nil || slotValue < 0 || slotValue > 32767 {
|
||
return 0, 0, false
|
||
}
|
||
return byte(listValue), int16(slotValue), true
|
||
}
|
||
|
||
func sceneInventoryStackAmount(stack dnfrepo.ItemStack) uint32 {
|
||
if value, ok := sceneInventoryExtraUint(stack.Extra, "amount_or_count", "amount", "count", "stack", "quantity"); ok {
|
||
return sceneInventoryClampUint32(value)
|
||
}
|
||
if stack.Count > 0 {
|
||
return sceneInventoryUint32FromInt64(stack.Count)
|
||
}
|
||
return 1
|
||
}
|
||
|
||
func sceneInventoryExtraByte(extra map[string]string, keys ...string) byte {
|
||
return sceneInventoryExtraByteDefault(extra, 0, keys...)
|
||
}
|
||
|
||
func sceneInventoryExtraByteDefault(extra map[string]string, fallback byte, keys ...string) byte {
|
||
value, ok := sceneInventoryExtraUint(extra, keys...)
|
||
if !ok || value > 255 {
|
||
return fallback
|
||
}
|
||
return byte(value)
|
||
}
|
||
|
||
func sceneInventoryExtraUint16(extra map[string]string, keys ...string) uint16 {
|
||
value, ok := sceneInventoryExtraUint(extra, keys...)
|
||
if !ok || value > 65535 {
|
||
return 0
|
||
}
|
||
return uint16(value)
|
||
}
|
||
|
||
func sceneInventoryExtraUint32(extra map[string]string, keys ...string) uint32 {
|
||
value, ok := sceneInventoryExtraUint(extra, keys...)
|
||
if !ok {
|
||
return 0
|
||
}
|
||
return sceneInventoryClampUint32(value)
|
||
}
|
||
|
||
func sceneInventoryExtraUint(extra map[string]string, keys ...string) (uint64, bool) {
|
||
for _, key := range keys {
|
||
raw := strings.TrimSpace(extra[key])
|
||
if raw == "" {
|
||
continue
|
||
}
|
||
value, err := strconv.ParseUint(raw, 0, 64)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
return value, true
|
||
}
|
||
return 0, false
|
||
}
|
||
|
||
func sceneInventoryUint32FromInt64(value int64) uint32 {
|
||
if value <= 0 {
|
||
return 0
|
||
}
|
||
if value > int64(^uint32(0)) {
|
||
return ^uint32(0)
|
||
}
|
||
return uint32(value)
|
||
}
|
||
|
||
func sceneInventoryClampUint32(value uint64) uint32 {
|
||
if value > uint64(^uint32(0)) {
|
||
return ^uint32(0)
|
||
}
|
||
return uint32(value)
|
||
}
|