Files
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

673 lines
24 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 本文件负责把当前 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)
}