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,617 @@
package dnfbridge
import (
"context"
"encoding/binary"
"errors"
"fmt"
"sort"
"strings"
dnfachievement "longheng.io/server/internal/modules/dnf/achievement"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfquest "longheng.io/server/internal/modules/dnf/quest"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnftitlebook "longheng.io/server/internal/modules/dnf/titlebook"
)
// Title book uses inventory list type 100 with slot encoding:
// slot = category * 1000 + bookIndex
const (
currentTitleBookListType byte = dnftitlebook.ListType
currentTitleBookSlotBase int16 = dnftitlebook.SlotBase
currentTitleBookMsgID uint16 = 0x0166
currentTitleBookPutMsgID uint16 = uint16(dnfenum.CmdPacketTitleBookPut)
currentTitleBookGetMsgID uint16 = uint16(dnfenum.CmdPacketTitleBookGet)
currentTitleBookMaxPerCategory = dnftitlebook.MaxPerCategory
// currentTitleBookEquipmentItemSpace 是客户端 op413 里表示「目标=装备栏」的物品空间号。
// 实测 2026-09-18:右键「使用」发 space=3(+目标槽 13),「拿出来」发 space=0(+期望背包格)。
currentTitleBookEquipmentItemSpace int32 = 3
)
// currentTitleBookEntry is one title in the title book. The current EXE reads
// the 22-byte fixed projection followed by u32 raw_len and raw_len bytes.
type currentTitleBookEntry struct {
SlotIndex uint16
ItemID int32
Value int32
Attr byte
Durability uint16
SealFlag byte
EnchantIndex int32
EnchantUpgrade byte
AmplifyType byte
AmplifyValue uint16
}
func currentTitleBookSlotKey(category int32, index int32) string {
slot := int16(category)*currentTitleBookSlotBase + int16(index)
return fmt.Sprintf("%d:%d", currentTitleBookListType, slot)
}
func currentTitleBookCategoryFromSlot(slot int16) int32 {
return int32(slot / currentTitleBookSlotBase)
}
func currentTitleBookIndexFromSlot(slot int16) int32 {
return int32(slot % currentTitleBookSlotBase)
}
// handleCurrentTitleBookPut processes CMD 0x019C (412): put a title from
// inventory/equipment into the title book.
func (s *Service) handleCurrentTitleBookPut(session *gameSession, body []byte) error {
if session == nil || session.selectedCharacterID == 0 {
return nil
}
if len(body) < 20 {
s.logGameEvent(session, "game-title-book-put-blocked", "body_len", len(body), "reason", "body_too_short")
return nil
}
itemSpace := int32(binary.LittleEndian.Uint32(body[0:4]))
slot := int16(int32(binary.LittleEndian.Uint32(body[4:8])))
itemID := int32(binary.LittleEndian.Uint32(body[8:12]))
category := int32(binary.LittleEndian.Uint32(body[12:16]))
bookIndex := int32(binary.LittleEndian.Uint32(body[16:20]))
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
defer cancel()
repos, ok := s.repositoryGroup()
if !ok {
return nil
}
owner, err := dnftitlebook.NewOwner(repos)
if err != nil {
return nil
}
result, err := owner.Put(ctx, dnftitlebook.PutCommand{
SelectedCharacterID: session.selectedCharacterID,
ItemSpace: itemSpace,
SourceSlot: slot,
ItemID: itemID,
Category: category,
BookIndex: bookIndex,
})
switch {
case errors.Is(err, dnftitlebook.ErrInventoryNotFound):
s.logGameEvent(session, "game-title-book-put-blocked",
"char_id", session.selectedCharacterID,
"reason", "inventory_not_found")
return s.sendTitleBookFailure(session, currentTitleBookPutMsgID, 1, itemSpace, category)
case errors.Is(err, dnftitlebook.ErrSourceNotFound):
s.logGameEvent(session, "game-title-book-put-blocked",
"char_id", session.selectedCharacterID,
"source_list", byte(itemSpace),
"source_slot", slot,
"item_id", itemID,
"reason", "source_not_found")
return s.sendTitleBookFailure(session, currentTitleBookPutMsgID, 2, itemSpace, category)
case errors.Is(err, dnftitlebook.ErrCategoryFull):
s.logGameEvent(session, "game-title-book-put-blocked",
"char_id", session.selectedCharacterID,
"category", category,
"reason", "category_full")
return s.sendTitleBookFailure(session, currentTitleBookPutMsgID, 3, itemSpace, category)
case errors.Is(err, dnftitlebook.ErrCategoryInvalid):
return s.sendTitleBookFailure(session, currentTitleBookPutMsgID, 4, itemSpace, category)
case err != nil:
s.logGameEvent(session, "game-title-book-put-failed",
"char_id", session.selectedCharacterID,
"reason", err)
return nil
}
s.logGameEvent(session, "game-title-book-put-success",
"char_id", result.CharacterID,
"item_id", result.ItemID,
"category", result.Category,
"book_index", result.BookIndex,
"source_list", result.SourceList,
"source_slot", result.SourceSlot,
"target_slot", result.TargetSlot)
if err := s.sendTitleBookPutAck(
session,
result.ItemSpace,
result.SourceSlot,
result.Category,
result.BookIndex,
); err != nil {
return err
}
var emptyEntry currentItemListEntry
emptyEntry.patchCore(result.SourceSlot, 0xFFFFFFFF, 0)
updateBody := buildCurrentItemUpdateBody(result.SourceList, []currentItemListEntry{emptyEntry})
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketWalkoutPartyMember), updateBody, 0); err != nil {
return err
}
return s.sendCurrentTitleBookList(session, ctx, repos, result.CharacterID, result.Category)
}
// handleCurrentTitleBookGet processes CMD 0x019D (413): take a title from
// the title book back to inventory.
func (s *Service) handleCurrentTitleBookGet(session *gameSession, body []byte) error {
if session == nil || session.selectedCharacterID == 0 {
return nil
}
if len(body) < 20 {
s.logGameEvent(session, "game-title-book-get-blocked", "body_len", len(body), "reason", "body_too_short")
return nil
}
itemSpace := int32(binary.LittleEndian.Uint32(body[0:4]))
slot := int16(int32(binary.LittleEndian.Uint32(body[4:8])))
itemID := int32(binary.LittleEndian.Uint32(body[8:12]))
category := int32(binary.LittleEndian.Uint32(body[12:16]))
bookIndex := int32(binary.LittleEndian.Uint32(body[16:20]))
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
defer cancel()
repos, ok := s.repositoryGroup()
if !ok {
return nil
}
owner, err := dnftitlebook.NewOwner(repos)
if err != nil {
return nil
}
cmd := dnftitlebook.GetCommand{
SelectedCharacterID: session.selectedCharacterID,
ItemSpace: itemSpace,
SourceSlot: slot,
PreferredSlot: slot,
ItemID: itemID,
Category: category,
BookIndex: bookIndex,
}
// 【2026-09-18 修复:按客户端给的目标空间落位】客户端在 op413 里明确给出目标物品
// 空间并**按回包里的 (space, slot) 摆放物品**(实测两条请求:space=3 是右键「使用」
// → 穿身上;space=0 是「拿出来」→ 回背包)。早期实现忽略了这个字段、一律穿上,
// 于是「拿出来」时玩家看到背包里一个副本,小退重登却变成自动穿戴(服务端真实状态)。
if itemSpace == currentTitleBookEquipmentItemSpace {
result, equipErr := owner.Equip(ctx, cmd)
if equipErr != nil {
return s.handleCurrentTitleBookGetError(session, equipErr, itemSpace, category, bookIndex)
}
return s.sendCurrentTitleBookEquipped(session, ctx, repos, result)
}
result, getErr := owner.Get(ctx, cmd)
if getErr != nil {
return s.handleCurrentTitleBookGetError(session, getErr, itemSpace, category, bookIndex)
}
return s.sendCurrentTitleBookTakenOut(session, ctx, repos, result)
}
// handleCurrentTitleBookGetError 把 owner 的错误映射成客户端认识的失败码。
func (s *Service) handleCurrentTitleBookGetError(session *gameSession, err error, itemSpace, category, bookIndex int32) error {
switch {
case errors.Is(err, dnftitlebook.ErrInventoryNotFound):
return s.sendTitleBookFailure(session, currentTitleBookGetMsgID, 1, itemSpace, category)
case errors.Is(err, dnftitlebook.ErrSourceNotFound):
s.logGameEvent(session, "game-title-book-get-blocked",
"char_id", session.selectedCharacterID,
"category", category,
"book_index", bookIndex,
"reason", "not_found_in_book")
return s.sendTitleBookFailure(session, currentTitleBookGetMsgID, 2, itemSpace, category)
case errors.Is(err, dnftitlebook.ErrTargetFull):
s.logGameEvent(session, "game-title-book-get-blocked",
"char_id", session.selectedCharacterID,
"reason", "inventory_full")
return s.sendTitleBookFailure(session, currentTitleBookGetMsgID, 3, itemSpace, category)
case errors.Is(err, dnftitlebook.ErrCategoryInvalid):
return s.sendTitleBookFailure(session, currentTitleBookGetMsgID, 4, itemSpace, category)
case err != nil:
s.logGameEvent(session, "game-title-book-get-failed",
"char_id", session.selectedCharacterID,
"reason", err)
}
return nil
}
// sendCurrentTitleBookEquipped 处理 space=3(右键「使用」)的成功回包。
//
// 【2026-09-18 方案A(闪退修正版)】称号簿右键使用 = 直接装备:
// - 服务端把称号写进 list-3 称号槽并落库(owner.Equip),登录时随装备回放,
// 所以小退重登后称号还在——修掉「穿戴栏的称号小退就消失」;
// - 不再发「物品回背包」的 op14 —— 那正是玩家看到的「背包里多一个」;
// - 换下来的旧称号要回背包,这里补一行 op14(list0 单行,客户端已验证的形态)
// 让客户端立刻看到;
// - **不再发 sendSelectedCurrentEquipmentSlotItemUpdates**:那是 22 行的 op14 list-3
// 全量刷新,客户端必崩(2026-09-18 23:41:17 客户端 90CN_trace:dispatch
// opcode=14 packet_len=2637 之后立刻 0xC0000005 eip=0x03B25D03,该地址位于
// __report_gsfailure 内,WER 同址报 0xC0000409 即栈缓冲溢出;而 list-3 历史上
// 只发过单行、共 88 次,从未出事)。最可能是客户端按行数在栈上开临时区被压爆,
// 但机制未完全坐实——不管机制如何,官方在这条路径上根本不发列表包,对齐官方最稳。
// 官方参考服务端 CTitleBook::getTitle(0x086425C4) 在装备分支末尾只调
// CUser::send_equip(0x865DD14),而 send_equip 对槽位 13 直接 return,
// 即官方这里根本不发任何物品列表:客户端拿 413 回包里的 (space=3, slot=13)
// 自己就穿上了。
func (s *Service) sendCurrentTitleBookEquipped(session *gameSession, ctx context.Context, repos dnfrepo.Group, result dnftitlebook.EquipResult) error {
s.logGameEvent(session, "game-title-book-get-success",
"char_id", result.CharacterID,
"item_id", result.ItemID,
"category", result.Category,
"book_index", result.BookIndex,
"equipment_slot", result.EquipmentSlot,
"replaced", result.Replaced,
"return_slot", result.ReturnSlot,
"action", "moved_from_book_to_equipment")
if err := s.sendTitleBookGetAck(
session,
currentTitleBookEquipmentItemSpace,
int16(result.EquipmentSlot),
result.Category,
result.BookIndex,
); err != nil {
return err
}
if result.Replaced {
if err := s.sendCurrentTitleBookInventoryRow(session, result.ReturnSlot, result.ReturnItem); err != nil {
return err
}
}
// 称号已穿上,这一页要用「移除后」的列表(owner.Equip 已把簿内条目删除)。
return s.sendCurrentTitleBookList(session, ctx, repos, result.CharacterID, result.Category)
}
// sendCurrentTitleBookTakenOut 处理 space=0 等背包目标(「拿出来」)的成功回包。
// 背包里必须有这一行,客户端才会立刻看到;ack 回**真实落点**——空间号统一报背包
// (标题只会落到背包或装备栏;万一客户端给出别的空间号,也不能把背包槽号报成仓库/
// 装扮的空间,否则客户端会按错误的容器摆放物品)。
func (s *Service) sendCurrentTitleBookTakenOut(session *gameSession, ctx context.Context, repos dnfrepo.Group, result dnftitlebook.GetResult) error {
s.logGameEvent(session, "game-title-book-get-success",
"char_id", result.CharacterID,
"item_id", result.ItemID,
"category", result.Category,
"book_index", result.BookIndex,
"target_slot", result.TargetSlot,
"requested_space", result.ItemSpace,
"action", "moved_from_book_to_inventory")
if err := s.sendTitleBookGetAck(
session,
int32(dnfrepo.MainInventoryListType),
result.TargetSlot,
result.Category,
result.BookIndex,
); err != nil {
return err
}
if err := s.sendCurrentTitleBookInventoryRow(session, result.TargetSlot, result.TargetStack); err != nil {
return err
}
return s.sendCurrentTitleBookList(session, ctx, repos, result.CharacterID, result.Category)
}
// sendCurrentTitleBookInventoryRow 发一行 list-0 单行 op14(客户端已验证的形态:
// 历史上 list-0 单行更新发过上千次,多行也一直是同一种 119 字节行)。
func (s *Service) sendCurrentTitleBookInventoryRow(session *gameSession, slot int16, stack dnfrepo.ItemStack) error {
entry := currentItemListEntryFromStack(dnfrepo.MainInventoryListType, slot, stack)
updateBody := buildCurrentItemUpdateBody(dnfrepo.MainInventoryListType, []currentItemListEntry{entry})
return s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketWalkoutPartyMember), updateBody, 0)
}
// sendCurrentTitleBookList sends NOTI 0x0166 for one category.
func (s *Service) sendCurrentTitleBookList(session *gameSession, ctx context.Context, repos dnfrepo.Group, characterID string, category int32) error {
inventory, found, err := repos.Inventory.Load(ctx, characterID)
if err != nil || !found {
return nil
}
entries := currentTitleBookEntriesForCategory(inventory, category)
body := buildCurrentTitleBookListBody(session.selectedCharacterID, category, entries)
return s.sendGameUpperRawClass(session, currentTitleBookMsgID, body, 0)
}
// sendAllCurrentTitleBookLists sends NOTI 0x0166 for all categories that have entries.
func (s *Service) sendAllCurrentTitleBookLists(session *gameSession, ctx context.Context, repos dnfrepo.Group, characterID string) error {
inventory, found, err := repos.Inventory.Load(ctx, characterID)
if err != nil || !found {
return nil
}
categories := currentTitleBookCategories(inventory)
for _, category := range categories {
entries := currentTitleBookEntriesForCategory(inventory, category)
body := buildCurrentTitleBookListBody(session.selectedCharacterID, category, entries)
if err := s.sendGameUpperRawClass(session, currentTitleBookMsgID, body, 0); err != nil {
return err
}
}
return nil
}
func currentTitleBookCategories(inventory dnfrepo.InventoryRecord) []int32 {
categorySet := make(map[int32]struct{})
for key, stack := range inventory.Slots {
if stack.ItemID <= 0 || dnfachievement.IsLegacyInventoryProgress(key, stack) {
continue
}
listType, slot, ok := parseSceneInventorySlotKey(key)
if !ok || listType != currentTitleBookListType {
continue
}
categorySet[currentTitleBookCategoryFromSlot(slot)] = struct{}{}
}
categories := make([]int32, 0, len(categorySet))
for c := range categorySet {
categories = append(categories, c)
}
sort.Slice(categories, func(i, j int) bool { return categories[i] < categories[j] })
return categories
}
func currentTitleBookEntriesForCategory(inventory dnfrepo.InventoryRecord, category int32) []currentTitleBookEntry {
entries := make([]currentTitleBookEntry, 0, 8)
for key, stack := range inventory.Slots {
if stack.ItemID <= 0 || dnfachievement.IsLegacyInventoryProgress(key, stack) {
continue
}
listType, slot, ok := parseSceneInventorySlotKey(key)
if !ok || listType != currentTitleBookListType {
continue
}
if currentTitleBookCategoryFromSlot(slot) != category {
continue
}
entries = append(entries, currentTitleBookEntry{
SlotIndex: uint16(currentTitleBookIndexFromSlot(slot)),
ItemID: int32(stack.ItemID),
Value: int32(sceneInventoryStackAmount(stack)),
Attr: sceneInventoryExtraByte(stack.Extra, "packed_flag_byte", "packed_flag", "packed", "ext_data0", "ext0"),
Durability: sceneInventoryExtraUint16(stack.Extra, "value_b_word", "durability", "max_durability"),
// 封装标记:0 = 已解封,1 = 封装。必须和背包行同源(currentItemListBindFlag),
// 否则同一件称号在背包显示解封、在簿里显示封装(2026-09-18 玩家报障)。
// 官方 CTitleBook::_putItemData 写的也是物品自己的 Inven_Item+0 字节,
// 详见 scene_inventory.go currentRequestOverseerRowsFromInventory 的注释。
SealFlag: currentItemListBindFlag(stack.Bind, stack.Extra),
EnchantIndex: int32(sceneInventoryExtraUint32(stack.Extra, "value_a", "instance_value", "item_uid", "serial", "count_or_instance")),
EnchantUpgrade: sceneInventoryExtraByte(stack.Extra, "unused_byte_after_value_a", "unused"),
AmplifyType: sceneInventoryExtraByte(stack.Extra, "value_c"),
AmplifyValue: sceneInventoryExtraUint16(stack.Extra, "value_d"),
})
}
sort.Slice(entries, func(i, j int) bool { return entries[i].SlotIndex < entries[j].SlotIndex })
return entries
}
// buildCurrentTitleBookListBody builds NOTI 0x0166 body.
// Format: u8 info_type + u16 owner_id + i32 category + i32 count + count * 22B entries.
func buildCurrentTitleBookListBody(ownerID uint16, category int32, entries []currentTitleBookEntry) []byte {
var w packetWriter
w.writeByte(0) // info_type: 0 = self
w.writeUint16(ownerID)
w.writeInt32(int(category))
w.writeInt32(len(entries))
for _, e := range entries {
w.writeUint16(e.SlotIndex)
w.writeInt32(int(e.ItemID))
w.writeInt32(int(e.Value))
w.writeByte(e.Attr)
w.writeUint16(e.Durability)
w.writeByte(e.SealFlag)
w.writeInt32(int(e.EnchantIndex))
w.writeByte(e.EnchantUpgrade)
w.writeByte(e.AmplifyType)
w.writeUint16(e.AmplifyValue)
w.writeUint32(0)
}
return w.bytes()
}
func (s *Service) sendTitleBookPutAck(session *gameSession, itemSpace int32, slot int16, category, bookIndex int32) error {
var w packetWriter
w.writeInt32(int(itemSpace))
w.writeInt32(int(slot))
w.writeInt32(int(category))
w.writeInt32(int(bookIndex))
return s.sendGameUpperSuccess(session, currentTitleBookPutMsgID, w.bytes())
}
func (s *Service) sendTitleBookGetAck(session *gameSession, itemSpace int32, slot int16, category, bookIndex int32) error {
var w packetWriter
w.writeInt32(int(itemSpace))
w.writeInt32(int(slot))
w.writeInt32(int(category))
w.writeInt32(int(bookIndex))
return s.sendGameUpperSuccess(session, currentTitleBookGetMsgID, w.bytes())
}
func (s *Service) sendTitleBookFailure(session *gameSession, msgID uint16, errorCode byte, itemSpace int32, category int32) error {
var w packetWriter
w.writeByte(0) // failure
w.writeByte(errorCode)
w.writeInt32(int(itemSpace))
w.writeInt32(int(category))
return s.sendGameUpperRaw(session, msgID, w.bytes())
}
// isCurrentTitleBookRequest returns true if the opcode is a title book operation
// that should be handled by the dnfbridge title book handler instead of the
// aligned command system.
func isCurrentTitleBookRequest(opcode uint16) bool {
return opcode == uint16(dnfenum.CmdPacketTitleBookPut) || opcode == uint16(dnfenum.CmdPacketTitleBookGet)
}
// --- Phase 2: Achievement progress (CMD 0x01A1 / 417) ---
const (
currentAchievementTriggerMsgID uint16 = uint16(dnfenum.CmdPacketAchievementTrigger)
)
// resolveAchievementDefinition joins the titlebook.etc placement mapping with
// the same quest PVF catalog used by normal quest logic.
func (s *Service) resolveAchievementDefinition(ctx context.Context, questID int32) (dnfachievement.Definition, error) {
mapping := s.currentTitleBookMapping()
if mapping == nil {
return dnfachievement.Definition{}, dnfachievement.ErrDefinitionNotFound
}
entry, ok := mapping[questID]
if !ok {
return dnfachievement.Definition{}, fmt.Errorf("%w: quest=%d", dnfachievement.ErrDefinitionNotFound, questID)
}
catalog, err := s.loadQuestCatalog(ctx)
if err != nil {
return dnfachievement.Definition{}, err
}
questDefinition, ok := catalog.Find(int64(questID))
if !ok {
return dnfachievement.Definition{}, fmt.Errorf("%w: quest=%d pvf", dnfachievement.ErrDefinitionNotFound, questID)
}
targets := currentAchievementTargets(questDefinition)
return dnfachievement.Definition{
QuestID: questID,
Target1: targets[0],
Target2: targets[1],
Target3: targets[2],
Reward: dnfachievement.TitleReward{
ItemID: entry.titleItemID,
Category: entry.category,
BookIndex: entry.bookIndex,
},
}, nil
}
type titleBookMappingEntry struct {
titleItemID int32
category int32
bookIndex int32
rewardCount int32
}
// currentTitleBookMapping returns the cached quest_id → title mapping from titlebook.etc.
func (s *Service) currentTitleBookMapping() map[int32]titleBookMappingEntry {
s.titleBookMappingMu.Lock()
defer s.titleBookMappingMu.Unlock()
if s.titleBookMappingCache != nil {
return s.titleBookMappingCache
}
s.initialEquipmentMu.Lock()
archive, err := s.initialEquipmentArchiveLocked()
s.initialEquipmentMu.Unlock()
if err != nil {
return nil
}
text, err := archive.ReadText("etc/titlebook.etc")
if err != nil {
s.logPacketEvent("game-titlebook-etc-read-failed", "error", err)
return nil
}
mapping := parseTitleBookEtc(text)
s.titleBookMappingCache = mapping
s.logPacketEvent("game-titlebook-mapping-loaded", "entries", len(mapping))
return mapping
}
// parseTitleBookEtc parses the current PVF's repeated
// [title collection info] sections. Each section starts with category name and
// max slots, then uses: book_index, marker, quest_id, reward_count, title_id.
// marker -1 denotes an empty slot and therefore has no following triple.
func parseTitleBookEtc(text string) map[int32]titleBookMappingEntry {
mapping := make(map[int32]titleBookMappingEntry, 256)
document, err := dnfpvf.Parse("etc/titlebook.etc", text)
if err != nil {
return mapping
}
category := int32(0)
for _, section := range document.Sections {
if !strings.EqualFold(strings.TrimSpace(section.Name), "title collection info") {
continue
}
if section.Start < 0 || section.End > len(document.Tokens) || section.Start >= section.End {
category++
continue
}
tokens := document.Tokens[section.Start:section.End]
cursor := 2 // category name + max slots
for cursor+1 < len(tokens) {
if tokens[cursor].Kind != dnfpvf.TokenInt || tokens[cursor+1].Kind != dnfpvf.TokenInt {
cursor++
continue
}
bookIndex := tokens[cursor].Int
marker := tokens[cursor+1].Int
cursor += 2
if marker == -1 {
continue
}
if cursor+2 >= len(tokens) ||
tokens[cursor].Kind != dnfpvf.TokenInt ||
tokens[cursor+1].Kind != dnfpvf.TokenInt ||
tokens[cursor+2].Kind != dnfpvf.TokenInt {
break
}
questID := tokens[cursor].Int
rewardCount := tokens[cursor+1].Int
titleItemID := tokens[cursor+2].Int
cursor += 3
if questID <= 0 || questID > int64(^uint32(0)>>1) ||
bookIndex < 0 || bookIndex >= int64(currentTitleBookMaxPerCategory) ||
titleItemID <= 0 || titleItemID > int64(^uint32(0)>>1) {
continue
}
mapping[int32(questID)] = titleBookMappingEntry{
titleItemID: int32(titleItemID),
category: category,
bookIndex: int32(bookIndex),
rewardCount: int32(rewardCount),
}
}
category++
}
return mapping
}
func currentAchievementTargets(definition dnfquest.Definition) [3]uint16 {
var targets [3]uint16
for index, value := range definition.CheckCount {
if index >= len(targets) {
break
}
targets[index] = currentAchievementTargetValue(value)
}
if targets[0] != 0 || targets[1] != 0 || targets[2] != 0 {
return targets
}
if strings.EqualFold(strings.TrimSpace(definition.Type), "[seeking]") ||
strings.EqualFold(strings.TrimSpace(definition.Type), "seeking") {
for index := 0; index < len(targets) && index*2+1 < len(definition.IntData); index++ {
targets[index] = currentAchievementTargetValue(definition.IntData[index*2+1])
}
if targets[0] != 0 || targets[1] != 0 || targets[2] != 0 {
return targets
}
}
targets[0] = 1
return targets
}
func currentAchievementTargetValue(value int64) uint16 {
if value <= 0 {
return 0
}
if value > int64(^uint16(0)) {
return ^uint16(0)
}
return uint16(value)
}