服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
109 lines
4.9 KiB
Go
109 lines
4.9 KiB
Go
package dnfbridge
|
||
|
||
import (
|
||
"context"
|
||
"strconv"
|
||
|
||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||
)
|
||
|
||
// 本文件补上「穿戴中的称号随机附魔」的权威行重放,修的是 2026-09-25 现场
|
||
// 「称号附魔后一进图附魔与技能等级一起消失,小退重上仍然看不见」。
|
||
//
|
||
// 取证链(全部有据可查,不是猜测):
|
||
//
|
||
// 1. 附魔在库里一直是好的:dnf_equipment_entries 槽 13 的 raw_entry +0x61..+0x65 =
|
||
// config/row,extras 里 random_title_enchant_config_id / _row_id 同时存在。
|
||
// 2. mode1/mode3 的**创建行**不携带那一段:writeCurrentMode1EquipmentCreateRow 只读
|
||
// [0x0A,0x16)、[0x38,0x58)、[0x72,0x77),而 +0x61..+0x65 落在 [0x58,0x72)。详见
|
||
// currentMode1EquipmentRowNeedsDeferredStateRow 的注释。
|
||
// 3. mode1 的延时更新行(sub_2007B00)**实测客户端不消费**:2026-09-25 11:00 之后每个
|
||
// mode1 的装备投影都已 deferred_update_count=1,报体里也确实出现了
|
||
// "00 03 0D 00" + 11 27 25 00(config 10001 / row 37),客户端依旧不显示附魔。
|
||
// 所以那条通路对这个问题无效,不能只靠它。
|
||
// 4. 本部署从不单独下发穿戴栏 list-3 行(全量 packet_log 里
|
||
// game-aligned-command-equipment-slot-refresh-send /
|
||
// game-upper-current-equipment-slot-items-refresh-send /
|
||
// game-upper-current-equipment-item-update-send 三个事件各 0 次),所以客户端手上那件
|
||
// 穿戴称号永远拿不到 +0x61..+0x65。
|
||
//
|
||
// 于是这里用**守护者宝石**早就落地并写明"客户端已验证会应用"的那条通路补包:在 mode1
|
||
// 之后、actor 已经建立之后,把这条穿戴行的完整 0x77 行用 list-3 op14 单独重放一次。
|
||
// 宝石的 socket 字在 +0x65..+0x6C,与称号附魔紧挨着、落在同一个缺口里,它的注释原文是
|
||
// "creation rows … do not carry generic raw 0x77 offsets" 与 "the current client has already
|
||
// proved it applies a following list-3 op14 row to the worn medal";称号附魔以前谁都没补。
|
||
//
|
||
// 选行标准只有一条:这条**投影出来的 0x77 行**是否带着完整的随机称号附魔
|
||
// (config 与 row 都非零,见 currentItemListEntryCarriesRandomTitleEnchant)。因此没有附魔
|
||
// 的称号与其它任何装备都不会被这条通路碰到——全库今天只有 1 行命中。
|
||
//
|
||
// 与 mode1 延时更新行的关系:那条保留(数据正确、对会读它的客户端有用),是"同一份正确
|
||
// 数据多一条通路";本文件这条是实测客户端会应用的那条。两条都可以单独回退。
|
||
|
||
// currentWornTitleEnchantActorSlot 报告这条穿戴行是否需要在 mode1 之后重放权威行,
|
||
// 需要时返回它在该客户端 actor 上的槽位号。
|
||
func currentWornTitleEnchantActorSlot(equipped dnfrepo.EquipmentEntry) (int16, bool) {
|
||
entry, ok := currentItemListEntryFromEquipment(equipped)
|
||
if !ok || !currentItemListEntryCarriesRandomTitleEnchant(entry) {
|
||
return 0, false
|
||
}
|
||
actorSlot, ok := currentEXEActorEquipmentSlot(equipped)
|
||
if !ok || actorSlot > 0xff {
|
||
return 0, false
|
||
}
|
||
return int16(actorSlot), true
|
||
}
|
||
|
||
// sendSelectedWornTitleEnchantRowRefresh 在 actor 生成边界(mode1 之后)把带随机称号附魔的
|
||
// 穿戴行用 list-3 op14 重放一次。没有命中的槽位时一个包都不发。
|
||
func (s *Service) sendSelectedWornTitleEnchantRowRefresh(session *gameSession, source string) error {
|
||
if session == nil || session.selectedCharacterID == 0 {
|
||
return nil
|
||
}
|
||
repositories, ok := s.repositoryGroup()
|
||
if !ok || repositories.Equipment == nil {
|
||
return nil
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||
defer cancel()
|
||
characterID := strconv.Itoa(int(session.selectedCharacterID))
|
||
equipment, found, err := repositories.Equipment.Load(ctx, characterID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !found {
|
||
return nil
|
||
}
|
||
for _, equipped := range equipment.Entries {
|
||
actorSlot, needsReplay := currentWornTitleEnchantActorSlot(equipped)
|
||
if !needsReplay {
|
||
continue
|
||
}
|
||
entry, ok := currentItemListEntryFromEquipment(equipped)
|
||
if !ok {
|
||
continue
|
||
}
|
||
// 与宝石重放同形:body = [list_type][u16][119 字节 0x77 行],走 op14 载体。
|
||
entry.setUint16(0, uint16(actorSlot))
|
||
body := buildCurrentItemUpdateBody(currentSocketListEquipment, []currentItemListEntry{entry})
|
||
s.logGameEvent(session, "game-current-worn-title-enchant-row-refresh",
|
||
"source", source,
|
||
"msg_id", uint16(dnfenum.CmdPacketWalkoutPartyMember),
|
||
"list_type", currentSocketListEquipment,
|
||
"actor_slot", actorSlot,
|
||
"item_id", equipped.ItemID,
|
||
"body_len", len(body),
|
||
"reason", "mode1_create_rows_do_not_carry_the_raw_title_enchant")
|
||
if err := s.sendGameUpperRawClass(
|
||
session,
|
||
uint16(dnfenum.CmdPacketWalkoutPartyMember),
|
||
body,
|
||
0,
|
||
); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|