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,70 @@
package dnfbridge
import "longheng.io/server/internal/modules/dnf/adventuregroup"
// 冒险团商店每个页签的"当前窗口进度"。
//
// 现场取证(2026-09-23):商店点数 8 字节条目 = `{数量, 当前窗口进度}`,
// 而且条目**按位置**对应页签(不是按字段匹配)——
//
// - 数量(第 0 个 dword)决定页签头部的 "X / 250 个";
// - 进度(第 2 个 dword)决定那条"获得1个结晶"进度条的百分比,
// 分母是 PVF 的 `[exp to get shop point]`(1500 万);
// 成长胶囊那一格的分母是 `[capsule max exp]`。
//
// 证据:往第 0 条(骁勇结晶)的第 2 个 dword 写 1,500,000,商店里那条进度条
// 正好显示 10%;同一轮往报文 raw+24 / raw+32 写的探针值完全不显示,
// 说明进度既不在报文头部也不在别处,就在这个字段里。
//
// 以前这里写的是"分类号"(0/1/2/3/4),于是骁勇结晶那一格恒为 0 → 恒 0.00%。
func currentAdventureInfoShopPointProgress(
index int,
projection adventuregroup.Projection,
) uint32 {
switch index {
case int(adventuregroup.ShopPointBrave):
// 距下一个骁勇结晶还差多少经验(PVF 1500 万一个,余数即进度)。
return clampCurrentAdventureProgress(projection.Runtime.BraveExperience)
case currentAdventureInfoCapsuleProgressEntry:
// 成长胶囊页签(4 个页签里的第 4 个):当前这一格的进度。
// 胶囊可以攒 10 格,所以必须取"累计值 mod 单格",否则攒过 100%
// 之后客户端会算出 100% 以上、看起来卡住不动。
gauge := projection.CapsuleGauge
if gauge == 0 {
return 0
}
return clampCurrentAdventureProgress(projection.Runtime.GrowthExperience % gauge)
default:
// 荣耀结晶(远征队奖励)与纯粹结晶(帝国竞技场)没有经验窗口。
return 0
}
}
// currentAdventureInfoCapsuleClaimCount 返回成长胶囊页签"可获得的经验值"那一栏
// 要发的值 = 现在可以领取几次(每次 = 10% 档 = 1,000,000 点经验值)。
//
// 取证(2026-09-23):
// - 客户端解析 sub_C7DAF0 时把报文 raw+7416 存进 manager+0x310,
// 0x1DE1F30 就是 `mov eax,[ecx+0x310]; ret`;
// - 0x13F0FA0 拿到这个值后 `for (i = 0; i < count; i++)` 逐个拼字符串建列表
// —— 它是**次数**,不是经验值总量。第一版当成经验值发(1,100 万)会让
// 客户端建上千万个字符串而卡死,所以这里只用 adventuregroup 算出来的、
// 上限为"10 格 × 每格 10 次 = 100"的次数;
// - 同一个 manager 上的 0x308 是"领取请求已发出"标志,0xC79730 发 op1491 时
// **不带任何字段**,和服务端的空 body 校验一致;
// - 页签上那句说明文字自己写着"累积10%以上时可以领取经验值"、
// "经验值按照10% : 1,000,000点的比例换算"、"角色Lv : Lv.50 ~ 86"。
func currentAdventureInfoCapsuleClaimCount(projection adventuregroup.Projection) uint32 {
return projection.CapsuleClaimCount
}
// currentAdventureInfoCapsuleProgressEntry 是成长胶囊页签在商店点数数组里的
// 位置:数组 5 条 = 3 个结晶分类 + 成长胶囊 + 1 条备用,页签顺序与 XUI 一致。
const currentAdventureInfoCapsuleProgressEntry = 3
func clampCurrentAdventureProgress(value uint64) uint32 {
if value > uint64(^uint32(0)) {
return ^uint32(0)
}
return uint32(value)
}