DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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