Files
90CNServer/server/internal/services/dnfbridge/userinfo_current_equipment_rules.go
T
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

283 lines
11 KiB
Go
Raw 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.
package dnfbridge
import (
"strconv"
"strings"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
func (r *csharpLegacyUserInfoReader) currentMode1EquipmentType(equipped dnfrepo.EquipmentEntry) (uint64, bool) {
return currentEXEActorEquipmentSlot(equipped)
}
func currentEXEActorEquipmentSlot(equipped dnfrepo.EquipmentEntry) (uint64, bool) {
// Builds before the dual-weapon metadata fix changed SlotIndex/map keys but
// left current_exe_equipment_type on the opposite endpoint. Prefer the
// authoritative location for this exact runtime 12<->24 mismatch so already
// affected Vagabonds recover on their next character selection.
if currentEXEDualWeaponSlotOverridesStaleMarker(equipped) {
return uint64(equipped.SlotIndex), true
}
if value, ok := sceneInventoryExtraUint(equipped.Extra, "current_exe_equipment_type"); ok {
if !isPVFInitialEquipment(equipped) || sceneInventoryExtraByte(equipped.Extra, "current_exe_runtime_move") != 0 {
return value, true
}
}
// Current runtime equipment records use the same direct base appearance
// slots 0..13 as mode0. Only untouched PVF starter rows retain the legacy
// worn-slot numbering translated below.
if !isPVFInitialEquipment(equipped) &&
equipped.SlotIndex >= 0 &&
equipped.SlotIndex < currentActorMode0AppearanceSlotCount {
return uint64(equipped.SlotIndex), true
}
// Legacy equipped-creature rows predate current_exe_runtime_move but carry
// the same current slot plus a concrete creature identity. This is enough
// to preserve slot 26 in a complete op342 group without treating arbitrary
// 26..29 records as ordinary equipment.
if equipped.SlotIndex == 26 {
equipmentSlot, hasEquipmentSlot := sceneInventoryExtraUint(
equipped.Extra,
"equipment_slot",
"equipped_slot",
)
creatureKey, hasCreatureKey := sceneInventoryExtraUint(
equipped.Extra,
"creature_key",
"creature_serial_or_handle",
)
if hasEquipmentSlot && equipmentSlot == 26 &&
hasCreatureKey && creatureKey > 0 {
return 26, true
}
}
return currentEXEActorEquipmentSlotForWornSlot(equipped.SlotIndex)
}
func currentEXEDualWeaponSlotOverridesStaleMarker(equipped dnfrepo.EquipmentEntry) bool {
if sceneInventoryExtraByte(equipped.Extra, "current_exe_runtime_move") == 0 {
return false
}
marker, ok := sceneInventoryExtraUint(equipped.Extra, "current_exe_equipment_type")
if !ok {
return false
}
return (equipped.SlotIndex == 12 && marker == 24) ||
(equipped.SlotIndex == 24 && marker == 12)
}
func currentEXEActorEquipmentSlotForWornSlot(slot int16) (uint64, bool) {
switch {
case slot == 11:
return 12, true
case slot >= 13 && slot <= 22:
return uint64(slot + 1), true
case slot == 23:
return 25, true
case slot == 32:
return 32, true
default:
return 0, false
}
}
func currentMode1EquipmentCreateRows(rows []currentMode1EquipmentObjectRow) []currentMode1EquipmentObjectRow {
if len(rows) == 0 {
return nil
}
createRows := make([]currentMode1EquipmentObjectRow, 0, len(rows))
for _, row := range rows {
if row.createEnabled && currentMode1EquipmentCreateRowHasVerifiedState(row) {
createRows = append(createRows, row)
}
}
return createRows
}
func currentMode1EquipmentDeferredUpdateRows(rows []currentMode1EquipmentObjectRow) []currentMode1EquipmentObjectRow {
if len(rows) == 0 {
return nil
}
updateRows := make([]currentMode1EquipmentObjectRow, 0, len(rows))
for _, row := range rows {
if row.createEnabled && currentMode1EquipmentCreateRowHasVerifiedState(row) &&
!currentMode1EquipmentRowNeedsDeferredStateRow(row) {
continue
}
updateRows = append(updateRows, row)
}
return updateRows
}
// currentMode1EquipmentRowNeedsDeferredStateRow 报告这一行除了 create 行之外,还必须
// 额外发一条 mode1 的延时更新行(sub_2007B00,逐字节携带完整 0x77 物品行)。
//
// 为什么必须有它(2026-09-25 现场:称号附魔进图就消失):
//
// writeCurrentMode1EquipmentCreateRow 只从 0x77 行里读三段——
// [0x0A,0x16)、[0x38,0x58)、[0x72,0x77);随机称号附魔的 +0x61..+0x65 落在它不读的
// [0x58,0x72) 里。角色身上装备对象的建立只有 mode1/mode3 的 create 行这一条路(本部署
// 的穿戴栏 list-3 行从不单独下发,见 packet_log 全量统计),所以每次 actor 重建
// (进图、回城、选角)附魔都会被重建掉:客户端上附魔显示消失,它带来的技能等级同时失效。
//
// 邻居已经因为同一个洞补过一次:守护者宝石 socket 字在 +0x65..+0x6C(同样落在
// [0x58,0x72)),走的是"mode1 之后补一条 list-3 op14"(guardian_gem_refresh.go 与
// scene_post_start_map.go 的注释写明了 "creation rows … do not carry generic raw 0x77
// offsets")。称号附魔以前谁都没补。
//
// 本函数走的是同一问题的另一条已验证通路:mode1 包里本来就有一个延时更新行区
// (sub_2007B00),它的每一行就是完整 0x77 行。这里**只做加法**——命中的行照旧进 create
// 列表(对象必须照原样被建立,绝不能因为改判定而让装备凭空消失),只是额外再进一次
// update 列表。因此 create 列表、其它任何行、任何其它流程都不受影响。
func currentMode1EquipmentRowNeedsDeferredStateRow(row currentMode1EquipmentObjectRow) bool {
if !row.currentItemStateDerived {
return false
}
return currentItemListEntryCarriesRandomTitleEnchant(row.update)
}
func currentMode1EquipmentCreateRowHasVerifiedState(row currentMode1EquipmentObjectRow) bool {
if _, ok := currentMode1EquipmentActorSlot(row); !ok || !row.durabilityKnown || row.itemID == 0 {
return false
}
// The current actor slot also selects the item-table conditional readers.
if _, ok := currentMode1EquipmentRuntimeType(row); !ok {
return false
}
if row.indexedState1C61F40.currentItemMismatch {
return false
}
// Current op25 and sub_21792A0 prove that the plain mode1 detail fields are
// the same values carried by the current 0x77 item record. This branch is
// intentionally limited to a detail state that can be reproduced exactly.
if row.currentItemStateDerived && !row.readsRawBlocks && !row.readsDurabilityOverrideU32 &&
row.linkedItemID == 0 && row.auxValue == 0 && row.auxByte == 0 && row.bindFlag == 0 && row.marker16 == 0 &&
len(row.vector1D6E020) == 0 && row.state112 == 0 && len(row.vector225CCA0) == 0 &&
row.state120 == 0 && row.state160 == 0 && row.state128 == 0 && row.state140 == 0 &&
row.state144 == 0 && row.resourceByte == 0 && row.state168 == 0 && row.state169 == 0 &&
row.state170 == 0 && len(row.lateRawC) == 0 && len(row.lateRawD) == 0 &&
row.state183 == 0 && len(row.lateRawE) == 0 {
return true
}
if (row.readsRawBlocks && (len(row.rawA) > 0 || len(row.rawB) > 0)) ||
(row.linkedItemID != 0 && row.slot == 9 && len(row.linkedRaw) > 0) ||
len(row.vector1D6E020) > 0 || len(row.vector225CCA0) > 0 ||
len(row.lateRawC) > 0 || len(row.lateRawD) > 0 || len(row.lateRawE) > 0 {
return true
}
return row.linkedItemID != 0 || row.auxValue != 0 || row.auxByte != 0 ||
row.state112 != 0xffffffff ||
row.state120 != 0 || row.state160 != 0 || row.state128 != 0 ||
row.state140 != 0 || row.state144 != 0 || row.resourceByte != 0 ||
row.state168 != 0 || row.state169 != 0 || row.state170 != 0 || row.state183 != 0
}
func currentMode1EquipmentCreateEnabled(equipped dnfrepo.EquipmentEntry) bool {
extra := equipped.Extra
for _, key := range []string{"mode1_create_enabled", "mode1_create_row_verified", "current_exe_mode1_create_verified"} {
raw := strings.TrimSpace(strings.ToLower(extra[key]))
switch raw {
case "1", "true", "yes", "on":
return true
case "0", "false", "no", "off":
return false
}
}
// The working Python implementation creates every equipped row in mode1.
// Go previously defaulted this to untouched legacy starter slots only.
// After op19 normalized those rows, or for runtime/rental equipment, mode1
// emitted update rows without first creating actor equipment objects.
// Preserve explicit overrides and the verified-state gate, but enable every
// row whose current actor slot is known.
_, ok := currentEXEActorEquipmentSlot(equipped)
return ok
}
func currentMode1EquipmentReadsRawBlocks(extra map[string]string, equipmentType uint64, equipmentTypeKnown bool) bool {
for _, key := range []string{"mode1_reads_raw_blocks", "current_exe_reads_equipment_raw_blocks"} {
raw := strings.TrimSpace(strings.ToLower(extra[key]))
switch raw {
case "1", "true", "yes", "on":
return true
case "0", "false", "no", "off":
return false
}
}
if equipmentTypeKnown {
return equipmentType <= 0x0b
}
return false
}
func currentMode1EquipmentUnknownTypeCount(rows []currentMode1EquipmentObjectRow) int {
count := 0
for _, row := range rows {
if !row.equipmentTypeKnown {
count++
}
}
return count
}
func currentMode1EquipmentRowSummary(rows []currentMode1EquipmentObjectRow) string {
if len(rows) == 0 {
return ""
}
parts := make([]string, 0, len(rows))
for _, row := range rows {
typePart := "unknown"
if row.equipmentTypeKnown {
typePart = strconv.FormatUint(row.equipmentType, 10)
}
parts = append(parts, strconv.Itoa(int(row.slot))+":"+strconv.FormatUint(uint64(row.itemID), 10)+":type="+typePart+":durability="+strconv.Itoa(int(row.durability))+":derived="+boolDigit(row.currentItemStateDerived)+":raw="+boolDigit(row.readsRawBlocks)+":create="+boolDigit(row.createEnabled))
}
return strings.Join(parts, ";")
}
func boolDigit(value bool) string {
if value {
return "1"
}
return "0"
}
func currentMode1EquipmentReadsDurabilityOverrideU32(extra map[string]string, equipmentType uint64, equipmentTypeKnown bool) bool {
for _, key := range []string{"mode1_reads_durability_override_u32", "current_exe_reads_type26_u32"} {
raw := strings.TrimSpace(strings.ToLower(extra[key]))
switch raw {
case "1", "true", "yes", "on":
return true
case "0", "false", "no", "off":
return false
}
}
if equipmentTypeKnown {
return equipmentType == 26
}
return false
}
func currentMode1EquipmentGradeOrInstance(equipped dnfrepo.EquipmentEntry) uint32 {
if value, ok := sceneInventoryExtraUint(equipped.Extra, "mode1_v74", "mode1_grade_or_instance"); ok {
return sceneInventoryClampUint32(value)
}
if value := currentEquippedCreatureInstance(equipped); value != 0 {
return value
}
if isPVFInitialEquipment(equipped) {
return 0
}
if seed := sceneInventoryExtraUint32(equipped.Extra, "quality_seed"); validCurrentEquipmentQualitySeed(seed) {
return seed
}
if !currentEquipmentEntryHasCurrentRaw77(equipped) {
return 0
}
value := currentItemListEquipmentInstance(equipped)
if !validCurrentEquipmentQualitySeed(value) {
return 0
}
return value
}