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,672 @@
|
||||
// 本文件负责把当前 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)
|
||||
}
|
||||
Reference in new issue
Block a user