Skip to content

引擎数据契约与数据驱动边界(互操作 / schema 级)

本篇回答一个常被问的问题:引擎 .so 里到底有没有"硬编码关卡数据"?游戏数据到底住在哪、 长什么样? 结论会顺带厘清一条对 Web 化可行性 至关重要的分界线—— 哪些是"纯原生运行时",哪些是"数据驱动"

来源与边界(务必先读)

  • 全文只到 结构 / schema / 规模 级:ELF 段大小、导出符号计数、JSON 的字段名与类型
  • 不含任何值内容:不导出反汇编、不导出角色名/台词/数值等版权数据。字段名是 schema/接口事实(等同于描述一份数据库表结构),不是受保护的表达。
  • 素材来自本地只读分析仓库内既有文件(lib/*.soassets/** 皆随反编译 APK 附带) 与项目自有代码,无反编译产物入库

一句话结论

"关卡数据硬编码在 .so 里"基本是把位置搞混了。 .so客户端运行时代码本体 (渲染 / 音频 / 战斗演出 / 网络 / 下载状态机 + 静态中间件),关卡本身的数据不在里面; 真正的游戏数据分布在下载资产包、bundled 资产、服务器响应三处,且大多是干净的 JSON。 所谓"能在 .so 里找到关卡数据",实为一个 bundled 的测试关卡 JSONassets/…/quest/testStageData1.json,17 KB)——是资产文件、非二进制内嵌

.so 是什么:运行时代码,不是数据库

lib/arm64-v8a/libmadomagi_native.so(30.5 MB,aarch64,stripped)的结构实测:

大小说明
.text(代码)~14.4 MB引擎 + 游戏逻辑 + 静态链入的全部中间件
.rodata(只读常量)~2.0 MB格式串、字段名、资产 key、少量 ID/URL——是代码常量,不是数据库
.data.rel.ro / .data~1.0 MB重定位只读 / 可写数据
.bss~33 MB运行时零初始化,文件里无内容
  • DT_NEEDED 只有系统库libGLESv2/libEGL 渲染、libOpenSLES 音频、libc/m/stdc++) → Cocos2d-x + CRI ADX2 + Live2D 等中间件全部静态链入这一颗 .so,无独立中间件库。
  • stripped,但仍有 ~59,000 个 C++ 动态符号可见(这是项目能"按 mangled 名 hook"的前提; 内部静态函数名已抹除)。按子系统的导出符号计数(近似规模):
子系统符号数子系统符号数
CRI ADX2 音频~3700任务 Quest~815
场景 Scene~2350剧情 / CG~730
Live2D Cubism~1350扭蛋 Gacha~500
http2 网络栈~1150下载 / 资源~200
战斗 Battle~160
  • 只读串里:JSON 字段名 ~1490、资产 key ~520、硬编码 URL 仅 4 个/magica/ 路径 17 个、 以及大量 debug/test 脚手架(含 test/debug/sample 的串 ~3350)。少量写死标识符 (如序章 OP020)。这些是解析数据的代码常量,不是数据本身。

数据驱动边界(对 Web 化最关键)

把上面的符号规模与资产格局对齐,可以画出这条线:

面向归属例子
纯原生运行时(Web 化的真正壁垒)代码,在 .soGLES 渲染、Cocos2d-x 场景框架、Live2D 播放器CRI 音频引擎战斗演出/播放器、http2 客户端、资源下载状态机
数据驱动(可绕开二进制理解)JSON / 资产 / 服务器关卡/任务定义战斗结算、扭蛋结果、编成/账号状态、剧情脚本、master-data
数据资产(原生播放器消费的素材)资产包Live2D 模型(moc3)、CRI 音频(hca/acb)、图集(plist+png)、动画(ExportJson)、着色器(vsh/fsh)

要点:原生那颗是"播放器 / 渲染器 / 网络与下载机",而"玩什么"(关卡、结算、剧情、编成) 是数据——大多还是服务端下发的结构化 JSON。这意味着理解游戏内容完全可以绕开 二进制,只从数据契约走(净室友好)。

"数据驱动"不等于"服务端算"

本节说的是内容定义(打什么)是数据;它不保证结算逻辑(怎么算)也在服务端。 真机流量已证实战斗结算在客户端——见下文 webData 一节的更正。

Bundled 资产格式地图

assets/ 里随包附带的基础集(下载包之外的启动/兜底/测试资产):

  • assets/package/ 顶层分区:quest · story · movie · memoria · evolution · top · web · window · loading · startup · tap · selectURL · download · debug · live2dViewer · anotherQuest · shaders · spacer —— 一眼可见"每个游戏子系统一个目录",含 debug / live2dViewer开发/调试分区。
  • assets/resource/image_native(图集/场景图)、scenario(剧情脚本)、sound_native(BGM/SE,HCA/ACB)。

按扩展名的形态(bundled 部分):plist(Cocos 图集) ·png/jpg ·ExportJson(Cocos Studio 动画) · json(UI/数据) ·vsh/fsh(GLSL 着色器) ·js/html(WebView 前端片段)。这些格式本身多为公开 格式,可自由据其结构解析;游戏专有的取值内容留在原资产、不复制。

战斗关卡数据契约(关键实证)

那份 bundled 测试关卡 testStageData1.json(17 KB)的 schema仅字段名/类型,无任何值):

text
根 object(13 键)
├─ battleType            : str          # 战斗种类
├─ scenario              : object       # 关卡元数据
│    keys: auto, bgm, bgmBoss, cost, difficulty, missionList,
│          questType, sheetType, title, titleExtend
├─ playerList            : array        # 我方编成(每个元素 ~数十字段)
│    elem keys: ai, align, attack, defence, blast, charge,
│               charId, cardId, diskId, connectId, doppelId,
│               discType1..5, endMessageId, friend, helper, …
├─ waveList              : array        # 敌人波次
│    elem keys: boss, effect, enemyList, field
├─ artList               : array        # "art"=光碟/技能效果
│    elem keys: artId, code, growPoint, rate, sub, target, turn
├─ memoriaList           : array        # 记忆(装备)
│    elem keys: artList, cost, description, displayType, icon,
│               level, memoriaId, name, type, voice
├─ magiaList             : array        # 魔法(必杀)
│    elem keys: artList, description, icon, level, magiaId, name
├─ connectList           : array        # connect 技
├─ doppelList            : array        # doppel(变身)
├─ webData               : object       # ★ 见下
│    keys: gameUser, resultCode, userItemList,
│          userQuestBattleResultList, userStatusList
├─ isHalfSkill / continuable : bool
└─ attackAlignmentRateTable  : array    # 属性克制表

★ 关于 webData 的推测已被证伪(更正)

webData 的键是 gameUser / resultCode / userQuestBattleResultList / userStatusList—— 这是一份服务器 API 响应的形状(战斗结算 + 玩家状态)。本篇早期版本据此推测 "战斗 = 服务端算好、客户端回放"。这个推测是错的。

真机流量的结论是战斗结算完全在客户端:服务端只在 quest/start 建档(计数器全零)、 在 quest/native/get 下发战斗定义,然后由客户端把算好的结果通过 quest/native/result/send 上报;全程无随机种子无逐回合轨迹, 服务端无从复现或校验。

为什么形状会骗人:这些结算类字段确实存在于响应中,但它们是 "为客户端上报的结果记账",不是"服务端算出来的战斗"。判断权威性 不能只看响应里有没有结算字段,必须看方向与时序。完整证据链、 正确判据与由此修订的路线权衡见 Web 化可行性评估 · Phase 0 结论

WebView ↔ 引擎命令桥(98 个命令,实证)

元游戏前端(/magica/* 那套 WebView 应用)不直接操作引擎,而是通过一条命令桥 让 native 干活:播 BGM、推场景、加 Live2D、放过场动画、发本地通知……

这条桥的完整命令表就在底包里——assets/package/web/test/config.js,98 个命令码, 是 f4samurai 留下的开发测试页的一部分。旁边的 command.html / touch2.html / command.js 是能跑的测试台,逐个演示这些命令怎么调。

为什么这对 Web 化很关键

它等于一份**「要把元游戏跑进浏览器,需要用 Web 技术顶替掉的 native 能力清单」**, 而且一条不落、带官方命名。更重要的是:它是静态的、已经在我们仓库里, 不依赖任何还活着的服务器——与前端应用本身(只能从活服务器抓)性质完全不同。

线格式

JS 侧的封装(command.js 原样):

js
function command(id, pram) {
  var message = "game:" + id + "," + pram;   // 载荷格式:game:<命令码>,<参数>
  try {
    webkit.messageHandlers.gameCommand.postMessage(message);   // iOS 传输
  } catch (e) {
    alert(message);
  }
}

载荷是 game:<命令码>,<参数>,两端一致;传输方式两个平台不同——所以测试页里 那个 try/catch 不是防御性编程,是因为它写的是 iOS 分支。Android 侧从 smali 可读出:

环节Android 实现
JS 可见的桥对象androidCommandWebViewImpl 构造里 addJavascriptInterface(new Javascript(), "androidCommand")
唯一的 @JavascriptInterface 方法jsCallback(String),内部 startsWith("game") 过滤
另一条传输mJsScheme = "game",即导航到 game: URL 由 shouldOverrideUrlLoading 拦截
过 JNIJava_jp_f4samurai_web_WebViewHelper_onJsCallback
C++ 落点web::WebViewImpl::onJsCallback(std::string)web::WebViewManager::jsCallbackweb::WebView::setOnJSCallback 注册的处理器

反向(引擎 → JS) 靠在页面里求值全局函数,native 库里能直接 strings 出调用点:

nativeCallback("GachaResult");
nativeCallback("SCENE_PUSH_EVENT_RAID");
nativeCallback("SCENE_SHOW_REWARD_EVENT_RAID");
nativeCallback("purchasePopup");
getBaseData(<json>)      // 批量回传 base64 图片,供页面直接贴成背景

我们的 CnvBridge 与游戏的桥是两套,不要混

补丁在游戏注册完 androidCommand 之后紧接着插入 WebViewInterceptor.installJsBridge(...),把自己的 CnvBridge 注册上去。 两者对象名不同、互不干扰。CnvBridge 的能力与来源闸见 WebView 拦截与状态重放

命令表

命令码按功能分段,段内留有空洞(官方预留)。以下为 config.js 原样:

数据与设备 <100(24 个)

名称名称
1DATA_CLEAR_WEB_CACHE25DATA_CLOSE_APP
2DATA_REMOVE_ASSET30DATA_GET_FONT
5DATA_CALL_TOUCHES_BEGIN40DATA_GET_QUEST_RESULT_JSON
6DATA_CALL_TOUCHES_MOVE50DATA_OPEN_URL
7DATA_CALL_TOUCHES_END60DATA_GET_BASE64
10DATA_AWAKE_PURCHASE62DATA_SET_CLIPBOARD
11DATA_PURCHASE_ITEM70DATA_GET_REWARD
12DATA_GET_PURCHASE_STATE71DATA_DELETE_REWARD
20DATA_GET_SNS_USER_ID90DATA_OPEN_EDIT_BOX
21DATA_GET_APP_VERSION95DATA_GET_QUEST_REPLAY_DATA_ID
22DATA_GET_DOWNLOAD_CONFIG96DATA_DELETE_QUEST_REPLAY_DATA
23DATA_GET_DEVICE_INFO97DATA_GET_QUEST_REPLAY_VERSION

音效 100–199(14 个)

名称名称
100SOUND_BGM_PLAY111SOUND_SE_STOP
101SOUND_BGM_STOP114SOUND_SE_SET_VOL
102SOUND_BGM_RESUME115SOUND_SE_GET_VOL
103SOUND_BGM_PAUSE120SOUND_VO_PLAY
104SOUND_BGM_SET_VOL121SOUND_VO_STOP
105SOUND_BGM_GET_VOL124SOUND_VO_SET_VOL
110SOUND_SE_PLAY125SOUND_VO_GET_VO

场景切换 200–399(24 个)

名称名称
201SCENE_PUSH_WEBVIEW281SCENE_PUSH_ARENA
202SCENE_POP_WEBVIEW282SCENE_POP_ARENA
211SCENE_PUSH_LOADING291SCENE_PUSH_CHAT
221SCENE_PUSH_DOWNLOAD292SCENE_POP_CHAT
231SCENE_PUSH_GACHA301SCENE_PUSH_TOP
241SCENE_PUSH_EVOLUTION302SCENE_POP_TOP
251SCENE_PUSH_MEMORIA_COMPOSE341SCENE_PUSH_ANOTHER_QUEST
261SCENE_PUSH_STORY342SCENE_POP_ANOTHER_QUEST
271SCENE_PUSH_QUEST343SCENE_PLAY_ANOTHER_QUEST
272SCENE_POP_QUEST351SCENE_PUSH_MOVIE
275SCENE_PUSH_QUEST_REPLAY361SCENE_PUSH_MOVIE_CHAR
276SCENE_SEND_QUEST_REPLAY_DATA371SCENE_PUSH_EVENT_TEST

显示与演出 400–499(20 个)

名称名称
400DISPLAY_SET_WEBVIEW_VISIBLE440DISPLAY_ADD_MOVIE
410DISPLAY_CHANGE_BG450DISPLAY_PLAY_COMPOSE_EFFECT
420DISPLAY_ADD_L2D451DISPLAY_SHOW_COMPOSE_RESULT
421DISPLAY_REMOVE_L2D452DISPLAY_HIDE_COMPOSE
422DISPLAY_PALY_L2D_MOTION460DISPLAY_PLAY_COMPOSE_MAGIA
430DISPLAY_ADD_MINI465DISPLAY_PLAY_AWAKE_ABILITY
431DISPLAY_REMOVE_MINI470DISPLAY_PLAY_NORMAL_GACHA_TOP
432DISPLAY_PLAY_MINI_MOTION471DISPLAY_STOP_NORMAL_GACHA_TOP
433DISPLAY_PLAY_MINI_EFFECT490DISPLAY_PLAY_MEMORIA_TOP
434DISPLAY_STOP_MINI_EFFECT491DISPLAY_STOP_MEMORIA_TOP

本地通知 500–599(10 个)

名称名称
500NOTI_GET_CONF_PNOTE511NOTI_TURN_ON_WEEKLY_QUEST
501NOTI_AWAKE_PNOTE512NOTI_TURN_OFF_WEEKLY_QUEST
502NOTI_TURN_ON_PNOTE520NOTI_GET_CONF_AP_FULL
503NOTI_TURN_OFF_PNOTE521NOTI_TURN_ON_AP_FULL
510NOTI_GET_CONF_WEEKLY_QUEST522NOTI_TURN_OFF_AP_FULL

显示与演出(续) 600+(6 个)

名称名称
600DISPLAY_PLAY_FORMATION611DISPLAY_STOP_WEEKLY_QUEST_TOP
601DISPLAY_STOP_FORMATION620DISPLAY_PLAY_FORMATION_ENEMY
610DISPLAY_PLAY_WEEKLY_QUEST_TOP621DISPLAY_STOP_FORMATION_ENEMY

表是底包版本的快照

这份表来自当前底包(totentanz 1.2.0_r128 / 游戏 3.1.9)的 config.js。 换底包时应重新提取比对——命令码是引擎与前端的私有约定,官方没有兼容性承诺。 提取方式:

bash
grep -oE '^\s*[A-Z_0-9]+\s*:\s*[0-9]+' assets/package/web/test/config.js

综合结论

  1. .so = 运行时代码(渲染/音频/演出/网络/下载 + 静态中间件),不是游戏数据库
  2. 游戏数据是数据驱动的干净 JSON/资产,分布在下载包、bundled 资产、服务器响应三处;
  3. 数据驱动边界清晰:原生是"播放器",内容是"数据"——理解内容无需碰二进制
  4. 战斗结算在客户端(真机流量证实,非本篇早期推测的"服务端权威回放"):服务端下发 定义、客户端算完上报结果。因此纯 Web 化除表现层外,还需自行重写结算引擎—— 范围有界但工作量实在,详见 Web 化可行性评估

合规再强调

本篇只刻画结构/schema/规模。任何进一步实现都应延续净室口径:可据公开格式自采数据契约 自行实现解析/渲染,但不得照搬引擎反编译产物,也不得再分发提取出的游戏美术/音频/文本内容。

交叉链接

文档正文以 CC BY-NC-SA 4.0 授权 · 代码部分以 GPLv3 开源 · 本项目仅作学习研究使用,与版权方无任何关联