目录
由于众所周知的咕咕咕的原因,持苹果设备的小鸽子们无法同步存档,这给我们带来了诸多不便,特别是要跨系统迁移时。而现在,我面临着要将 iOS 端存档迁到安卓端的挑战。
原理
- 现代的移动设备操作系统,没有 root 或越狱情况下,都不允许直接访问属于应用内部的数据。Phi 的存档显然属于这一类。然而还是有一种办法能让我们查看与修改这些数据,那就是备份与恢复(对整个设备或对个别应用的)。
- 跨平台的游戏,大概没有必要对不同系统使用不同数据储存格式。(事实上,Phigros 使用 Unity 开发,更保证了这一点)大胆猜想存档数据在不同平台内部的储存格式是一样的。
- 上网搜寻,找到这篇 B 站文章,虽然彼的迁移方向与我相反,但说明了迁移是可行的,ta 给的 Tip 印证了上述猜想,只是表明格式有些细微差异。
这样思路就很清晰了。先获得两个设备的备份,然后从备份文件中找到关心的游戏数据,设法偷梁换柱,最后将修改了的备份恢复到安卓设备上。
备份 iOS 设备
有很多种方法。这里我选用 libimobiledevice 提供的命令行工具:
idevicebackup2 backup ./ios0624
一开始还报 protocol version
不对,不支持我这么高的 iOS 版本。然后发现要从源码编译最新版,遂 AUR 了事。
等跑完,备份出来的文件夹长这样:(示意)
ios0624└── 00XXXX10-0016XXXXXXXXXX1E ├── 00 │ ├── 000117d06d7515f2a2965574bca1fbc9e48e0ddd │ ├── 00019983bebec182aef65f21253ef6b4fc919f7f │ ├── …… │ └── 00ff010b251ec8c21782af2a1b085f9dcd9b290b ├── 01 │ └── …… ├── …… ├── ff │ ├── …… │ └── ffffc1329062b055698bb15bd7020b4e134f41c7 ├── Info.plist ├── Manifest.db ├── Manifest.db-shm ├── Manifest.db-wal ├── Manifest.plist └── Status.plist
258 directories, 158281 files
这是整机备份,还并不能马上看见我们关心的数据。有一堆 sha1 哈希前缀两字母分块的文件夹,但实际上不在那里,而在 Plist 文件中。这 plist 尚不是明文,还得借助工具解开。这里经过查询,我选择使用 Java 系的 iTunes Backup Explorer 工具。用它打开刚备份出的 Manifest,切换到 Files 标签页,选中 Applications
下的 AppDomain-games.Pigeon.Phigros
,并点〖Export selected domains〗保存下来。其文件夹结构如下:(示意)
AppDomain-games.Pigeon.Phigros├── Documents│ └── antiAddiction.json└── Library ├── Cookies │ └── Cookies.binarycookies ├── HTTPStorages ├── Preferences │ ├── com.apple.gamecenter.plist │ └── games.Pigeon.Phigros.plist └── WebKit └── WebsiteData └── ……
备份安卓设备
为了之后通过恢复写入数据,确实得知道备份文件的结构是什么样的。
需要注意的是,备份前 Phigros 不能是全新的,至少要开一首歌,不然后面会有问题,别问我怎么知道的。
备份安卓设备,也有很多方法,最通用的当然是用 adb. 先看看 Phigros 包名叫什么:
adb shell pm list packages
可以找到它叫 com.PigeonGames.Phigros
. 于是:
adb backup com.PigeonGames.Phigros
不知道为什么,在我的设备上,运行以上命令前得开着游戏,不然备份出来只有 47B 小的 .ab 文件,没有有效的内容。正常情况下,会产生一个大约 10 MiB 的 backup.ab 文件。
这是个二进制文件,想办法解包。有个 Android backup extractor 工具,支持解包和重新打包备份文件,非常的好。
abe unpack backup.ab backup.tar
再解压这个 tar 档案。得到如下文件夹结构:(示意)
apps└── com.PigeonGames.Phigros ├── ef │ └── il2cpp │ ├── etc │ │ └── mono │ │ ├── 2.0 │ │ ├── 4.0 │ │ ├── 4.5 │ │ ├── browscap.ini │ │ ├── config │ │ └── mconfig │ ├── Metadata │ │ └── game.dat │ ├── Resources │ │ ├── mscorlib.dll-resources.dat │ │ └── System.Data.dll-resources.dat │ └── unity.ver ├── _manifest └── sp ├── com.PigeonGames.Phigros.v2.playerprefs.xml ├── device_detail.xml ├── game_detail.xml ├── GUIDHelper.xml ├── [hex=088cf05dbc3f39xxxxxxxxxxxxxxxxbd]_timing_file_suffix.xml ├── np-saved-game-services-keys-data.xml └── taptap_sharepreference.xml
转移 Preference 数据
现在,比较两边备份解包的目录结构,观察它们的文件名和内容,不难猜到游戏存档数据应当分别存在 AppDomain-games.Pigeon.Phigros/Library/Preferences/games.Pigeon.Phigros.plist
和 apps/com.PigeonGames.Phigros/sp/com.PigeonGames.Phigros.v2.playerprefs.xml
中。后者是纯文本 XML,格式大概如下:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?><map> <string name="50p5P7iCOVKXpuayTWM25XVib4gREmZHVZxbIq4VKRo%3D">%2BfYF%2BTK0Jy8b25IgYEeerviDL8ylovfdLWWJAuPFmBY%3D</string> <int name="NeedUpdateSongsData" value="4" /> ……</map>
有很多很多加密的键值对,也有一些明文。这些加密的键值对应该就是游戏数据了,不信的话,打一首歌或改一个设置,再备份一遍看,就发现有个别键的值发生了变化。
而苹果这边的 games.Pigeon.Phigros.plist
还是二进制。用 libplist 提供的 plistutil
来转换一下:
cd AppDomain-games.Pigeon.Phigros/Library/Preferencesplistutil -i games.Pigeon.Phigros.plist -o games.Pigeon.Phigros.plist.xml
出来的 xml 格式是大致是这样的:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>50p5P7iCOVKXpuayTWM25XVib4gREmZHVZxbIq4VKRo=</key> <string>+fYF+TK0Jy8b25IgYEeerviDL8ylovfdLWWJAuPFmBY=</string> <key>NeedUpdateSongsData</key> <integer>4</integer> ……</dict></plist>
可见格式稍有不同,iOS 端键和值分在相邻两个 XML tag 中,而安卓端在一个 tag 的不同 attribute 中。好在我们在两边找到了一模一样的键名,只是安卓端多套了一个 URLEncode 而已——我们并没有破解游戏数据的意图,所以知道这些足矣。
为此,让 GPT 帮我们用 python 写一个转换脚本:
import xml.etree.ElementTree as ETimport urllib.parse
def convert_plist_to_playerprefs(plist_path, playerprefs_path): # Parse the plist XML file plist_tree = ET.parse(plist_path) plist_root = plist_tree.getroot() plist_dict = plist_root.find("dict")
# Create the root of the playerprefs XML structure playerprefs_root = ET.Element("map")
# Iterate over the elements in the plist file plist_elements = list(plist_dict) for i in range(0, len(plist_elements), 2): key_elem = plist_elements[i] value_elem = plist_elements[i + 1] key = key_elem.text value = value_elem.text value_type = value_elem.tag
# Add key-value pairs to the playerprefs map if value_type == "string": string_elem = ET.SubElement( playerprefs_root, "string", name=urllib.parse.quote(key, safe="") ) string_elem.text = urllib.parse.quote(value) elif value_type == "integer": int_elem = ET.SubElement( playerprefs_root, "int", name=urllib.parse.quote(key, safe="") ) int_elem.set("value", value) else: print( f"Warning: Unsupported data type '{value_type}' encountered for key '{key}'. This key-value pair will be skipped." )
# Write the playerprefs XML to file playerprefs_tree = ET.ElementTree(playerprefs_root) playerprefs_tree.write(playerprefs_path, encoding="utf-8", xml_declaration=True)
# Example usageconvert_plist_to_playerprefs( "./ios0624-extract/AppDomain-games.Pigeon.Phigros/Library/Preferences/games.Pigeon.Phigros.plist.xml", "com.PigeonGames.Phigros.v2.playerprefs-cvt-out-0624.xml",)
上面脚本最后几行指定的输入输出文件位置,执行后,用转换过的 xml 文件覆盖掉彼 playerprefs.xml 文件就行。事实上有一些未加密的键是只出现在一边平台的,但似乎对后续没影响。
恢复安卓设备
现在就是把改过的目录结构重新打包成安卓备份,即一步步将上面操作反过来。
首先是把文件夹打成 tar 档案。一开始直接用图形化界面的压缩工具,后面发现恢复不进去,一度非常怀疑天地,然后才知道安卓对这个 tar 包的格式(其中的文件顺序)非常挑剔,要这样打:
cd apps/..
# 先把原来的备份档案 tar 文件顺序列表导出来tar tf backup.tar | grep -F "com.PigeonGames.Phigros" > package.list# 再按这个 list 的顺序打包tar cf restore.tar -T package.list
再把 tar 转成安卓备份 ab 格式:
abe pack restore.tar restore.ab
最后,「恢复」一下:
adb restore restore.ab
打开屁股肉看一眼,是不是进度都回来啦?愉快地在新设备上推分吧!