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,339 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const currentMode1EquipmentCreateRowBaseWireSize = 56
|
||||
const currentMode1EquipmentRawBlockHeaderSize = 4
|
||||
const currentMode1EquipmentRawBlockMaxLen = 1024
|
||||
const currentMode1EquipmentVectorRecordSize = 8
|
||||
const currentMode1EquipmentVectorRecordMaxCount = 2
|
||||
const currentMode1EquipmentDwordVectorMaxCount = 2
|
||||
|
||||
const currentMode1EquipmentIndexedStateMaxRecords = 3
|
||||
|
||||
type currentMode1EquipmentIndexedStateRecord struct {
|
||||
value0 byte
|
||||
value1 byte
|
||||
value2 byte
|
||||
}
|
||||
|
||||
type currentMode1EquipmentIndexedState struct {
|
||||
records []currentMode1EquipmentIndexedStateRecord
|
||||
activeValue byte
|
||||
selectedIndex byte
|
||||
selectedValue byte
|
||||
selectedRecord currentMode1EquipmentIndexedStateRecord
|
||||
hasSelectedRecord bool
|
||||
currentItemMismatch bool
|
||||
}
|
||||
|
||||
type currentMode1EquipmentObjectRow struct {
|
||||
createEnabled bool
|
||||
currentItemStateDerived bool
|
||||
slot byte
|
||||
itemID uint32
|
||||
equipmentType uint64
|
||||
equipmentTypeKnown bool
|
||||
instance uint32
|
||||
extData byte
|
||||
durability uint16
|
||||
durabilityKnown bool
|
||||
linkedItemID uint32
|
||||
auxValue uint32
|
||||
auxByte byte
|
||||
bindFlag byte
|
||||
marker16 uint16
|
||||
linkedRaw []byte
|
||||
readsRawBlocks bool
|
||||
rawA []byte
|
||||
rawB []byte
|
||||
readsDurabilityOverrideU32 bool
|
||||
durabilityOverride uint32
|
||||
vector1D6E020 []byte
|
||||
state112 uint32
|
||||
vector225CCA0 []byte
|
||||
state1E636E0 uint16
|
||||
indexedState1C61F40 currentMode1EquipmentIndexedState
|
||||
state120 byte
|
||||
state160 byte
|
||||
state128 byte
|
||||
state140 uint16
|
||||
state144 byte
|
||||
resourceByte byte
|
||||
state168 byte
|
||||
state169 byte
|
||||
state170 byte
|
||||
lateRawC []byte
|
||||
lateRawD []byte
|
||||
state183 byte
|
||||
lateRawE []byte
|
||||
update currentItemListEntry
|
||||
}
|
||||
|
||||
func (r *csharpLegacyUserInfoReader) currentMode1EquipmentObjectRows() []currentMode1EquipmentObjectRow {
|
||||
if r == nil || r.service == nil || strings.TrimSpace(r.characterID) == "" {
|
||||
return nil
|
||||
}
|
||||
repos, ok := r.service.projectionRepositories(r.ctx)
|
||||
if !ok || repos.Equipment == nil {
|
||||
return nil
|
||||
}
|
||||
record, found, err := repos.Equipment.Load(r.ctx, r.characterID)
|
||||
if err != nil {
|
||||
if r.session != nil {
|
||||
r.service.logGameEvent(r.session, "game-upper-selected-userinfo-mode1-equipment-load-failed", "character_id", r.characterID, "error", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !found || len(record.Entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
var petRecord dnfrepo.PetRecord
|
||||
if repos.Pet != nil {
|
||||
if loaded, petFound, petErr := repos.Pet.Load(r.ctx, r.characterID); petErr == nil && petFound {
|
||||
petRecord = loaded
|
||||
}
|
||||
}
|
||||
// 转职光环的 USERINFO 关联块要带**被覆盖光环实例**的镶嵌字节,所以这里必须
|
||||
// 读一次背包:穿戴时快照会在"穿好之后才打徽章"的场景下过期。缺记录/读失败都
|
||||
// 只意味着"没有可用的关联实例",投影照常发出(不猜、不阻塞)。
|
||||
var inventorySlots map[string]dnfrepo.ItemStack
|
||||
if repos.Inventory != nil {
|
||||
if inv, invFound, invErr := repos.Inventory.Load(r.ctx, r.characterID); invErr == nil && invFound {
|
||||
inventorySlots = inv.Slots
|
||||
}
|
||||
}
|
||||
rows := make([]currentMode1EquipmentObjectRow, 0, len(record.Entries))
|
||||
for _, equipped := range record.Entries {
|
||||
if equipped.SlotIndex < 0 || equipped.SlotIndex > 0xff || equipped.ItemID <= 0 {
|
||||
continue
|
||||
}
|
||||
mergeCurrentPetEnchantFromRecord(&equipped, petRecord)
|
||||
entry, entryOK := currentItemListEntryFromEquipment(equipped)
|
||||
if !entryOK {
|
||||
continue
|
||||
}
|
||||
equipmentType, equipmentTypeKnown := r.currentMode1EquipmentType(equipped)
|
||||
durability, durabilityKnown := currentMode1EquipmentDurabilityWord(equipped, entry)
|
||||
indexedState := currentMode1EquipmentIndexedStateFromCurrentItem(entry)
|
||||
// Slot 8 is the sole skin-avatar slot. Historical rows often lack the
|
||||
// optional PVF type label, but their mode1 reader still requires the
|
||||
// avatar detail blocks to materialize the full-body render.
|
||||
readsRawBlocks := currentMode1EquipmentReadsRawBlocks(equipped.Extra, equipmentType, equipmentTypeKnown) ||
|
||||
currentMode1EquipmentIsSkinAvatar(equipped)
|
||||
rawA, rawB := currentMode1EquipmentAvatarRawBlocks(equipped, entry, readsRawBlocks)
|
||||
rows = append(rows, currentMode1EquipmentObjectRow{
|
||||
createEnabled: currentMode1EquipmentCreateEnabled(equipped),
|
||||
currentItemStateDerived: true,
|
||||
slot: byte(equipped.SlotIndex),
|
||||
itemID: sceneInventoryUint32FromInt64(equipped.ItemID),
|
||||
equipmentType: equipmentType,
|
||||
equipmentTypeKnown: equipmentTypeKnown,
|
||||
// sub_1D77560's create-row v74 field is the equipped object's
|
||||
// grade/instance value. A zero default happens to render as top
|
||||
// quality, so rebuilding an already-owned actor after op19 must
|
||||
// carry the same equipment quality seed as the authoritative
|
||||
// 0x77 item row. Keep the historical PVF starter projection at
|
||||
// zero because its create scalar is an initialization marker,
|
||||
// not a durable quality seed.
|
||||
instance: currentMode1EquipmentGradeOrInstance(equipped),
|
||||
extData: entry.data[0x0a],
|
||||
durability: durability,
|
||||
durabilityKnown: durabilityKnown,
|
||||
// The link field (+12) carries whichever template this row must be
|
||||
// read against: a 转职光环 records the aura it covered, a 透明克隆
|
||||
// 时装 records the template it clones. The two keys never coexist on
|
||||
// one row, so the order only decides which is consulted first.
|
||||
linkedItemID: sceneInventoryExtraUint32(equipped.Extra, dnfrepo.AuraLinkedTemplateExtraKey, dnfrepo.CloneAvatarAppearanceExtraKey, "mode1_linked_item_id", "mode1_clone_or_linked_id", "clone_or_linked_id"),
|
||||
auxValue: currentMode1EquipmentExtraUint32Or(equipped.Extra, binary.LittleEndian.Uint32(entry.data[0x0e:0x12]), "mode1_aux_value", "mode1_state_24", "state_24"),
|
||||
auxByte: currentMode1EquipmentExtraByteOr(equipped.Extra, entry.data[0x12], "mode1_aux_byte", "mode1_state_32", "state_32"),
|
||||
bindFlag: currentMode1EquipmentExtraByteOr(equipped.Extra, entry.data[0x13], "mode1_state_56", "state_56", "value_c"),
|
||||
marker16: currentMode1EquipmentExtraUint16Or(equipped.Extra, binary.LittleEndian.Uint16(entry.data[0x14:0x16]), "marker_16", "marker16", "value_d"),
|
||||
linkedRaw: currentMode1EquipmentLinkedRaw(equipped.Extra, inventorySlots),
|
||||
readsRawBlocks: readsRawBlocks,
|
||||
rawA: rawA,
|
||||
rawB: rawB,
|
||||
readsDurabilityOverrideU32: currentMode1EquipmentReadsDurabilityOverrideU32(equipped.Extra, equipmentType, equipmentTypeKnown),
|
||||
durabilityOverride: currentMode1EquipmentDurabilityOverride(equipped),
|
||||
vector1D6E020: currentMode1EquipmentVectorRecords(equipped.Extra, "mode1_sub_1d6e020_records_hex", "mode1_vector_records_hex"),
|
||||
state112: currentMode1EquipmentState112(equipped, entry),
|
||||
// sub_1D77560 reads this through sub_225CCA0 and applies it to
|
||||
// ordinary equipment through the same vtable +280 method used
|
||||
// after op14. Derive it from the durable raw+0x3C vector so a
|
||||
// cold-login create row restores sockets without a wear cycle.
|
||||
vector225CCA0: currentMode1EquipmentDwordVectorFromCurrentItem(
|
||||
entry,
|
||||
equipped.Extra,
|
||||
"mode1_sub_225cca0_dwords_hex",
|
||||
"mode1_dword_vector_hex",
|
||||
),
|
||||
state1E636E0: binary.LittleEndian.Uint16(entry.data[0x45:0x47]),
|
||||
indexedState1C61F40: indexedState,
|
||||
state120: currentMode1EquipmentForgingLevel(equipped, entry),
|
||||
state160: sceneInventoryExtraByte(equipped.Extra, "mode1_state_160", "state_160"),
|
||||
state128: sceneInventoryExtraByte(equipped.Extra, "mode1_state_128", "state_128"),
|
||||
state140: sceneInventoryExtraUint16(equipped.Extra, "mode1_state_140", "state_140"),
|
||||
state144: sceneInventoryExtraByte(equipped.Extra, "mode1_state_144", "state_144"),
|
||||
resourceByte: sceneInventoryExtraByte(equipped.Extra, "mode1_resource_byte", "resource_byte"),
|
||||
state168: sceneInventoryExtraByte(equipped.Extra, "mode1_state_168", "state_168"),
|
||||
state169: sceneInventoryExtraByte(equipped.Extra, "mode1_state_169", "state_169"),
|
||||
state170: sceneInventoryExtraByte(equipped.Extra, "mode1_state_170", "state_170"),
|
||||
lateRawC: currentMode1EquipmentExtraHex(equipped.Extra, currentMode1EquipmentRawBlockMaxLen, "mode1_raw_c_hex", "mode1_state_171_raw_hex"),
|
||||
lateRawD: currentMode1EquipmentExtraHex(equipped.Extra, currentMode1EquipmentRawBlockMaxLen, "mode1_raw_d_hex", "mode1_state_175_179_raw_hex"),
|
||||
state183: sceneInventoryExtraByte(equipped.Extra, "mode1_state_183", "state_183"),
|
||||
// The current EXE copies this final five-byte block into equipment
|
||||
// state +188..+192. Restore authoritative raw+0x72..0x76 on cold
|
||||
// login when no explicit mode1 alias exists.
|
||||
lateRawE: currentMode1EquipmentRawBlockOrCurrentItem(
|
||||
equipped.Extra,
|
||||
entry,
|
||||
currentEpicProductionAbilityTailOffset,
|
||||
currentEpicProductionAbilityTailOffset+currentEpicProductionAbilityTailSize,
|
||||
"mode1_raw_e_hex",
|
||||
"mode1_state_188_192_raw_hex",
|
||||
),
|
||||
update: entry,
|
||||
})
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
return rows[i].slot < rows[j].slot
|
||||
})
|
||||
if len(rows) > 0xff {
|
||||
rows = rows[:0xff]
|
||||
}
|
||||
if len(rows) > 0 && r.session != nil {
|
||||
r.service.logGameEvent(r.session, "game-upper-selected-userinfo-mode1-equipment-create-rows",
|
||||
"character_id", r.characterID,
|
||||
"entry_count", len(rows),
|
||||
"unknown_type_count", currentMode1EquipmentUnknownTypeCount(rows),
|
||||
"row_summary", currentMode1EquipmentRowSummary(rows),
|
||||
"mcp_handler", "sub_1D77560")
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// currentMode1EquipmentLinkedRaw returns the payload written after the link
|
||||
// field (+12) in a create row, i.e. the block the client reads via sub_3457C50.
|
||||
//
|
||||
// An explicit captured alias always wins. Otherwise a worn 转职光环 resolves the
|
||||
// covered aura's socket bytes from the live inventory row the equip recorded, so
|
||||
// emblems socketed AFTER the 转职光环 was worn are still carried. Everything else
|
||||
// (clone avatars included) keeps the previous behaviour of emitting no block.
|
||||
func currentMode1EquipmentLinkedRaw(extra map[string]string, slots map[string]dnfrepo.ItemStack) []byte {
|
||||
if raw := currentMode1EquipmentExtraHex(extra, currentMode1EquipmentRawBlockMaxLen, "mode1_linked_raw_hex", "mode1_clone_raw_hex"); len(raw) != 0 {
|
||||
return raw
|
||||
}
|
||||
return currentAuraLinkedSocketBlock(extra, slots)
|
||||
}
|
||||
|
||||
// currentAuraLinkedSocketBlock resolves the covered aura INSTANCE's socket block
|
||||
// for a worn 转职光环. The link field only carries the covered aura's template id,
|
||||
// so the instance has to be looked up; a row that is gone, moved, or now holds a
|
||||
// different template means the binding is dead and nothing is emitted. Never
|
||||
// substitute another aura from the bag or a template guess.
|
||||
func currentAuraLinkedSocketBlock(extra map[string]string, slots map[string]dnfrepo.ItemStack) []byte {
|
||||
templateID := sceneInventoryExtraUint32(extra, dnfrepo.AuraLinkedTemplateExtraKey)
|
||||
if templateID == 0 {
|
||||
return nil
|
||||
}
|
||||
sourceKey := strings.TrimSpace(extra[dnfrepo.AuraLinkedSourceExtraKey])
|
||||
if sourceKey == "" || len(slots) == 0 {
|
||||
return nil
|
||||
}
|
||||
stack, found := slots[sourceKey]
|
||||
if !found || stack.ItemID != int64(templateID) {
|
||||
return nil
|
||||
}
|
||||
return currentItemListAvatarSocketData(stack.Extra)
|
||||
}
|
||||
|
||||
func currentMode1EquipmentRawBlockOrCurrentItem(
|
||||
extra map[string]string,
|
||||
entry currentItemListEntry,
|
||||
start int,
|
||||
end int,
|
||||
keys ...string,
|
||||
) []byte {
|
||||
if data := currentMode1EquipmentExtraHex(extra, currentMode1EquipmentRawBlockMaxLen, keys...); len(data) != 0 {
|
||||
return data
|
||||
}
|
||||
if start < 0 || end <= start || end > len(entry.data) {
|
||||
return nil
|
||||
}
|
||||
for _, value := range entry.data[start:end] {
|
||||
if value != 0 {
|
||||
return append([]byte(nil), entry.data[start:end]...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentMode1EquipmentForgingLevel(equipped dnfrepo.EquipmentEntry, entry currentItemListEntry) byte {
|
||||
// Current NoPack sub_1D77560 passes this byte as the forgingLevel argument
|
||||
// to ConstructEquipmentState_FromProjectionRow. That constructor stores it
|
||||
// in the encoded equipment-property field at +0x78, which is exactly what
|
||||
// AppendWeaponForgingTooltipLines reads after a character reselect.
|
||||
return sceneInventoryExtraByteDefault(
|
||||
equipped.Extra,
|
||||
entry.data[currentItemListForgingLevelOffset],
|
||||
"forging_level", "byte_57", "value_57", "mode1_state_120", "state_120",
|
||||
)
|
||||
}
|
||||
|
||||
const currentMode1SkinAvatarColorStateBytes = 4
|
||||
|
||||
func currentMode1EquipmentAvatarRawBlocks(equipped dnfrepo.EquipmentEntry, entry currentItemListEntry, readsRawBlocks bool) ([]byte, []byte) {
|
||||
extra := equipped.Extra
|
||||
rawA := currentMode1EquipmentExtraHex(extra, currentMode1EquipmentRawBlockMaxLen, "mode1_raw_a_hex", "mode1_sub_225c960_raw_hex", "mode1_raw_block_a_hex")
|
||||
rawB := currentMode1EquipmentExtraHex(extra, currentMode1EquipmentRawBlockMaxLen, "mode1_raw_b_hex", "mode1_sub_225c9b0_raw_hex", "mode1_raw_block_b_hex")
|
||||
if !readsRawBlocks {
|
||||
return rawA, rawB
|
||||
}
|
||||
// Current EXE sub_1D77560 uses the same two type<=11 extension readers as
|
||||
// list-1/op13 and the avatar branch of op14: socket state first, color state
|
||||
// second. Socket mutations persist those canonical blobs on the item row,
|
||||
// so use them as the mode1 create-state fallback instead of requiring a
|
||||
// second set of mode1-only aliases. Explicit captured mode1 blocks retain
|
||||
// precedence when present.
|
||||
if len(rawA) == 0 {
|
||||
rawA = append([]byte(nil), entry.avatarSocketData...)
|
||||
}
|
||||
if len(rawB) == 0 {
|
||||
rawB = append([]byte(nil), entry.avatarColorData...)
|
||||
}
|
||||
if currentMode1EquipmentIsSkinAvatar(equipped) {
|
||||
// A skin avatar is a complete character render rather than an
|
||||
// independent paper-doll part. The current actor's mode-1 reader
|
||||
// distinguishes a materialized avatar detail from an absent one by the
|
||||
// two length-prefixed detail blocks. A zero-length pair leaves slot 8
|
||||
// registered as an ordinary item and the renderer falls back to the
|
||||
// visible 0..7 avatar parts. The roster/card path does not consume
|
||||
// these blocks, which is why it can still show the skin correctly.
|
||||
//
|
||||
// A newly granted skin has no emblem or dye data, but still needs its
|
||||
// canonical empty detail shape: five empty six-byte sockets and two
|
||||
// zero colour words. Explicit captured/persisted mode-1 data above
|
||||
// always takes precedence.
|
||||
if len(rawA) == 0 {
|
||||
rawA = make([]byte, currentAvatarSocketBytes)
|
||||
}
|
||||
if len(rawB) == 0 {
|
||||
rawB = make([]byte, currentMode1SkinAvatarColorStateBytes)
|
||||
}
|
||||
}
|
||||
return rawA, rawB
|
||||
}
|
||||
|
||||
func currentMode1EquipmentIsSkinAvatar(equipped dnfrepo.EquipmentEntry) bool {
|
||||
// The current actor's equipment table reserves slot 8 exclusively for the
|
||||
// skin-avatar render. The PVF type string is helpful on newly granted
|
||||
// items but is absent on old/equipped rows imported before that metadata was
|
||||
// persisted, so it must not gate the wire shape.
|
||||
return equipped.SlotIndex == 8
|
||||
}
|
||||
Reference in new issue
Block a user