服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
283 lines
11 KiB
Go
283 lines
11 KiB
Go
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
|
||
}
|