Files
Flechazo d8771caef2 feat(all): add hunting and events, commerce and rewards, binary calendars, GameData caching and login diagnostics
Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling.

Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata.

Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks.

Improve automatic login and recovery diagnostics; document configuration and release flows.
2026-10-05 11:50:31 +08:00
..

离线开发工具

这里的程序用于查表、转换种子和维护测试存档。它们不属于游戏服务端运行时,不会进入 bd2server.exe。

Python 版本为 3.11 以上;GameData 分页 AES 解密需要 pycryptodome:

python -m pip install pycryptodome

客户端源码镜像与 Proto 查看

客户端反编译源码、混淆翻译表和官方会话抓包是开发取证输入;这些工具只读取客户端目录,绝不原地改写。两个输出目录都必须位于 --source 之外。为避免清空人工文件,输出已存在时只有带本工具清单的目录才能被重新生成。

# 用 ObfuscationTranslation 的 “混淆名⇨含义” 映射生成可检索 C# 镜像。
# 替换只作用于代码中的 C# 标识符;注释、字符串和字符字面量原样保留。
python .\tools\python\deobfuscate_client_source.py `
  --source "<Assembly-CSharp目录>" `
  --mapping "<ObfuscationTranslation.obfuscate>" `
  --output .\tmp\client-source-readable

# 从 *Reflection.cs 内嵌的 FileDescriptorProto 重建真正的 .proto。
python .\tools\python\extract_client_proto.py `
  --source "<Assembly-CSharp目录>" `
  --output .\tmp\client-proto

反混淆输出根下的 .bd2-deobfuscate-manifest.json 记录有效映射、冲突改名、未处理警告和替换统计。映射含命名空间或路径时,工具取末段并规范为一个合法标识符;发生同名时加入稳定后缀。反编译器在源码里留下的派生局部名也会回推:映射名 + 数字后缀 按最长映射前缀还原,camelCase(映射名)(首段大写字母整体小写)按来源唯一的逆变换还原;两者计入 derived_identifier_replacements。注释与字面量始终保持原样(其中的名字须回查映射表)。该镜像用于阅读与检索,并不承诺可编译。

Proto 输出根按 descriptor 原始文件名保存可读 .proto;client-descriptors.pb 是无损 FileDescriptorSet,用于保留文本渲染器暂未展开的复杂 options。.bd2-proto-extract-manifest.json 记录 package、依赖、源 Reflection、descriptor/.proto SHA-256 和渲染警告。

查询 GameData

无需运行 go test 或构建服务端:

# 列出任务库中的表
python .\tools\python\gamedata_db.py tables --root "<GameData目录>" --version "<版本>" --db quest --match QuestTable

# 查看表结构
python .\tools\python\gamedata_db.py schema --root "<GameData目录>" --version "<版本>" --db quest --table QuestTable22

# 展开一行 protobuf 的指定字段
python .\tools\python\gamedata_db.py row --root "<GameData目录>" --version "<版本>" --db quest --table PackTable --id 21 --fields 45

# 查看任务真实奖励(五个难度槽分别显示)
python .\tools\python\gamedata_db.py rewards --root "<GameData目录>" --version "<版本>" --pack 21 --quest 38

# 沿真实 PackTable.NextPackId 查看故事包链
python .\tools\python\gamedata_db.py chain --root "<GameData目录>" --version "<版本>" --start-pack 21

# 任意只读 SQL;参数用重复的 --param 传入
python .\tools\python\gamedata_db.py sql --root "<GameData目录>" --version "<版本>" --db pack21 `
  --query "SELECT id,ProtoBuf FROM BattleDeckTable WHERE id=?" --param 1

# 必要时导出普通 SQLite,默认拒绝覆盖
python .\tools\python\gamedata_db.py extract --root "<GameData目录>" --version "<版本>" --db quest --output .\tmp\quest.db

GameData 根目录和版本没有机器相关默认值,所有查询都必须显式传入 --root 和 --version。

种子转换

输入必须是已经解密、解包后的原始 protobuf,不接受 HTTP 信封或 AES 密文:

python .\tools\python\import_seed.py login .\decoded\LoginUser.pb `
  --packet-code 11 --output .\go\seed\v2_35_10\login_user.json

python .\tools\python\import_seed.py starter `
  --items .\decoded\ItemInfo.pb `
  --costumes .\decoded\CostumeInfo.pb `
  --characters .\decoded\CharInfo.pb `
  --output .\go\seed\v2_35_10\starter_player.json

python .\tools\python\import_seed.py mail .\decoded\MailInfo.pb `
  --output .\go\seed\v2_35_10\mail.json

输出已存在时默认拒绝覆盖;审阅输入和预期差异后才传 --force。

readonly 子命令接收一个 JSON 清单,键是接口路径,值含 packet_code 和相对于清单的 protobuf 文件路径:

{
  "/SkyWayScheduleInfo": {
    "packet_code": 162,
    "protobuf": "SkyWayScheduleInfo.pb"
  }
}

BD2 开发工具

dev_mail_grant.py 是独立的、仅监听环回地址的开发期浏览器工具。它只读取指定版本的 GameData,列出已验证、可走 ItemDBInfo 领取路径的有名道具:FoodTable(类型 5)、CookingTable(7)、ResourceTable(8)、QuestItemTable(13)、UseItemTable(14)、CollectionTable(17)、MyRoomItemTable(27)和 InstantUseItemTable(29)。普通物品的 ItemNameTextId 按客户端行为从 NameTextTable 解析;随机箱使用独立的 RandomBoxTextTable,现金商品搜索别名仍从其声明的 LocalTextTable 解析,三者不会依靠碰巧相同的数字文本 ID 串表。确定性随机箱使用真实名称作为内容物搜索别名,因此不依赖固定物品 ID。ResourceTable.Type=2 的场景/展示哨兵和无可用名称行不提供;固定内容随机箱只用于反查内容物的真实 ID,所有 type 9 随机箱均不作为邮件选项。金币和天赋神药等服务端已实现的账户货币从当前 CurrencyTable 发现,以各自元素类型和 id0 直接入账;名称与物品 ID 均不硬编码。工具本身既不属于 bd2server.exe,也不修改 data/state 的九份账号状态。

无需 GameData 或第三方 Python 包的 grant 子命令可追加一封含多个附件的动态邮件。服务端使用 --mail-grant-spool 读取该 JSON,每次 /MailInfo 导入未发放的 identity;已发放 identity 在领取和重启后仍保持幂等。CLI 允许付费钻石 2:0:数量、免费钻石 3:0:数量、金币 4:0:数量、天赋神药 12:0:数量、金线 20:0:数量,以及当前 2.35.10 已核实的抽抽乐券 8:1000:数量 和 UR 专用装备抽抽乐券 8:1104:数量,每个附件数量均限制为 1..2147483647。例如 python tools/python/dev_mail_grant.py grant --output data/dev/currency-grants.json --identity test-paid-and-tickets-1 --attachment 2:0:100000000 --attachment 8:1000:100000000 --attachment 8:1104:100000000 --attachment 4:0:1000000000。两种券通过 ItemDBInfo 写入背包,钻石和金币直接叠加账户余额;发放前应检查同类券现有库存加附件后的总量仍在客户端 int32 范围内。

启动工具时,--output 是 version=1 的动态邮件 grant spool;网页只追加带稳定 identity 的待发放请求,实际 mail_id 由服务端统一分配,因此不会与任务补偿等动态邮件撞号。--settings-output 保存与账号存档分离的开发选项。页面会在一次提交完成前禁用按钮,避免浏览器双击生成重复请求。工具启动后在浏览器打开 http://127.0.0.1:8765/:

python .\tools\python\dev_mail_grant.py serve `
  --game-data "E:\bd2\dl\GameData" `
  --game-data-version "20260923193640" `
  --output .\data\dev\mail-grants-spool.json `
  --settings-output .\data\dev\dev-tools.json

工具首次提交及后续每次发放都会原子更新 --output。bd2server 的邮件服务会在下一次正常 /MailInfo 请求检查它:网页发放后重新打开或刷新游戏邮箱即可看到新邮件,不需要每次重启服务器。服务启动时加入:

--mail-grant-spool ".\data\dev\mail-grants-spool.json"
--dev-tools-config ".\data\dev\dev-tools.json"

页面下方的“无限背包容量”不会写入账号状态,也不会使用任意巨大容量;它从当前 GameData 读取客户端安全上限(2.35.10 当前为普通道具 500、装备 2000)。切换后无需重启服务端,但客户端必须重新登录以重新取得 UserDBInfo。普通背包、普通仓库、装备背包和装备仓库的正式扩充接口仍按 GameDefaultTable 的逐格阶梯价格扣除金币并持久化;开发开关不覆盖或删除已购买容量。

客户端 MailDBInfo.ItemType、ItemId 和 ItemCount 均为 int32,所以该工具把单附件数量限制为 1..2147483647。当前官方样本中单封最多观察到 5 个附件,但没有证据证明这是协议上限,因此工具不据此宣称或实施“5 件”上限。服务端统一分配动态邮件 ID,并持久化 spool identity、领取状态和领取历史;同一邮件重试不会重复发奖。

这不是“所有 GameData 表都可发放”的虚假承诺:角色(元素类型 6)、装备(10)、服装(11)和我的房间奖杯(28)在客户端 RewardDBInfoBundle 中分别必须使用 CharDBInfo、EquipDBInfo、CostumeDBInfo、MyRoomTrophyDBInfo,而当前本地邮件服务尚未连接相应领域存档,工具不会提供它们;直接伪装成 ItemDBInfo 会造成客户端状态错误。付费/普通货币之外的特殊货币亦不在当前本地钱包实现范围内。ContentTicket(19)仅为已核实的满月一次性专用券开放 19:450030:1:客户端 CommonPacket.AddItemInfo 将其交给 AddContentTicketItem,服务端按实例存入物品存档并沿用邮件领取去重;其他内容券 ID 和其他数量均拒绝。浏览器道具列表不开放内容券,grant 子命令可使用这一严格限定的附件。LobbySettingItem(25)仍未提供;GetItemInfo 的显示分支不足以证明可安全存储。若要补齐其余类型,需要先实现对应的服务端存储、去重及正确 reward-bundle 字段,不需要客户端 patch。

热载只接受经过 mail.Starter.Validate 校验的完整 JSON 种子:文件未变化时不会重新读取;被检测到的坏替换会保留上一次已验证邮箱,并使该次 /MailInfo 请求失败而不会部分加载。工具本身总是完整写临时文件、fsync 后原子替换,正常发放不会让服务器看到半文件。

工具测试

python -m unittest discover -s .\tools\python\tests -p 'test_*.py' -v