跳转到正文
Zjl37's logo 动量星空

Phigros iOS 存档手动迁移到安卓设备

/ 9分钟

目录

由于众所周知的咕咕咕的原因,持苹果设备的小鸽子们无法同步存档,这给我们带来了诸多不便,特别是要跨系统迁移时。而现在,我面临着要将 iOS 端存档迁到安卓端的挑战。

原理

  • 现代的移动设备操作系统,没有 root 或越狱情况下,都不允许直接访问属于应用内部的数据。Phi 的存档显然属于这一类。然而还是有一种办法能让我们查看与修改这些数据,那就是备份与恢复(对整个设备或对个别应用的)。
  • 跨平台的游戏,大概没有必要对不同系统使用不同数据储存格式。(事实上,Phigros 使用 Unity 开发,更保证了这一点)大胆猜想存档数据在不同平台内部的储存格式是一样的。
  • 上网搜寻,找到这篇 B 站文章,虽然彼的迁移方向与我相反,但说明了迁移是可行的,ta 给的 Tip 印证了上述猜想,只是表明格式有些细微差异。

这样思路就很清晰了。先获得两个设备的备份,然后从备份文件中找到关心的游戏数据,设法偷梁换柱,最后将修改了的备份恢复到安卓设备上。

备份 iOS 设备

有很多种方法。这里我选用 libimobiledevice 提供的命令行工具:

Terminal window
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 包名叫什么:

Terminal window
adb shell pm list packages

可以找到它叫 com.PigeonGames.Phigros. 于是:

Terminal window
adb backup com.PigeonGames.Phigros

不知道为什么,在我的设备上,运行以上命令前得开着游戏,不然备份出来只有 47B 小的 .ab 文件,没有有效的内容。正常情况下,会产生一个大约 10 MiB 的 backup.ab 文件。

这是个二进制文件,想办法解包。有个 Android backup extractor 工具,支持解包和重新打包备份文件,非常的好。

Terminal window
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.plistapps/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 来转换一下:

Terminal window
cd AppDomain-games.Pigeon.Phigros/Library/Preferences
plistutil -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 ET
import 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 usage
convert_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 包的格式(其中的文件顺序)非常挑剔,要这样打:

Terminal window
cd apps/..
# 先把原来的备份档案 tar 文件顺序列表导出来
tar tf backup.tar | grep -F "com.PigeonGames.Phigros" > package.list
# 再按这个 list 的顺序打包
tar cf restore.tar -T package.list

再把 tar 转成安卓备份 ab 格式:

Terminal window
abe pack restore.tar restore.ab

最后,「恢复」一下:

Terminal window
adb restore restore.ab

打开屁股肉看一眼,是不是进度都回来啦?愉快地在新设备上推分吧!