JBなしでiPhone版ドラクエ8のセーブデータを解析・改造してみた
スマホ版ドラクエ8を発売日に購入してプレイしていますが、素晴らしい出来ですね。
Unityを使って作られているもようで、PS2版にも全然劣らないと思います。
さて、そんなドラクエ8のセーブデータを早速改造してみました。
そのうち対策されると思いますし、決して真似はしないでください。
セーブデータを取り出す
まずはiFunboxをつかってごにょごにょしますと、それっぽいデータを発見できます。
これはJailBreakなしでもたどり着けます。
詳しくは調べてないのでわかりませんが、savedata.binファイルはタイトル画面で設定できる音量設定の情報などが入っていました。
残りの0〜5が、冒険の書に該当すると思われるのですが、ひとまず1つめの冒険の書がsavedata0.binに該当することだけはわかりました。
冒険の書は3つまでなので、3〜5はなんでしょうね。おそらくは、今回セーブをしていない場合に中断箇所から再開できる機能があるので、それのための保持データではないかと思われます。
バイナリエディタで開いてみました。
macには使いやすいバイナリエディタがなかったので、これはWindowsのtsxbinというソフトです。
なんだか暗号化されてる感じでもないし、Assembly-CSharpという文字をみるとなんとなく・・・。
もしかすると、これはC#のBinaryFomatterでシリアライズ化したデータなのではないでしょうか。
解析してみる
というわけで実際にプログラムを組んでBinaryFomatterが読めるかを試してみました。
長くなるので結果だけから先にいうと、ダメでした。
ただ、構造はある程度解析することができました。
BinaryFomatterのデータ構造はこちらに詳しく書かれていました。
それを利用したAnalyzerプログラムを書いている人がいたので、そちらを参考にして、このファイルを読み込んでみたところ、歯抜けながらもデータ構造を復元することができました。
変数名などもBinaryFormatterから拾えたのですが、一部おかしいものがあったので、おそらくこんな感じというデータ構造が下記。
[Serializable]
public class CDqSaveData{
public Int32 m_saveMapNo;
public Single m_envTime;
public UInt16 m_fileAttr;
public UInt64 m_playTimeSec;
public Boolean m_empty;
public MAP_FLAG_INFO m_mapFlag;
public CScenario m_scenario;
public CUserData m_userData;
public CSaveBattleData m_battleData;
public CMonsterTeamData m_monsterTeam;
public CRenkinData m_renkinData;
public CASINO_SAVE_DATA m_casinoData;
public CFUSION_ITEM_SELL m_fusionShopData;
public CUiOption m_uiOption;
public CSuspendData m_suspendData;
public CUsedPresentCode m_usedPresentCode;
public UInt32[] m_bitFlag;
public Int16[] m_shortFlag;
public UInt32[] m_rulaFlag;
public UInt32[] m_rulaDisableFlag;
}
[Serializable]
public class MAP_FLAG_INFO{
public UInt32[] PartsOnOffFlag;
public UInt32[] drop_iflag;
public UInt32[] navi_map_flag;
public UInt32[] wmap_bit_data;
public UInt32[] padding_2;
public UInt32[] area_cord;
}
[Serializable]
public class CScenario{
public Int32 Progress;
public UInt16[] Chapter;
public UInt16[] MapCounter;
public UInt32[] BitFlag;
public UInt32[] KeyFlag;
public UInt32[] TreasureFlag;
public UInt16[] ShortCount;
}
[Serializable]
public class CUserData{
public string m_playerName;
public Int32 m_money;
public UInt16 m_partyBit;
public Int16[] m_partyOrder;
public Int32[] m_partyNpc;
public DQ_PERSON_STATUS_PARAM[] m_paramData;
public CPartyItemData m_partyItemData;
public SByte[] m_battleTactics;
public Int32 m_bankMoney;
public Single[] m_shipPosition;
public string m_shipBlock;
public Int32[] m_remainSkillPoint;
public CPersistItem m_persistItem;
public UInt32[] m_getItemBit;
public Byte[] m_sellItemCount;
public LEARN_SKILL_INFO[] _GetCharaParamSkill_sts_skill;
public LEARN_SKILL_INFO[] _GetCharaParamSkill_skl;
}
[Serializable]
public class LEARN_SKILL_INFO{
}
[Serializable]
public class CSaveBattleData{
public Int32 m_battle_encount_num;
public Int32 m_battle_kill_total_num;
public Int32 m_battle_oiharatta_num;
public Int32 m_battle_victory_num;
public Int32 m_battle_husensyou_num;
public Int32 m_battle_escape_num;
public Int32 m_battle_all_dead_num;
public Int32 m_battle_kaishin_num;
public Int32 m_battle_first_attack_num;
public UInt32 m_battle_get_money;
public Single field_walk_dist;
public Single field_mithia_walk;
public Int32[] padding_i;
public DMG_PERFORMANCE dmg_performance;
public MONSTER_DATA m_monster_data;
public Int32[] dead_count;
public BTL_SCORE_TENSION btl_tension;
public Int32 m_battle_escape_none_dead_num;
public Int32 odokasu_nige_num;
public Int32 odokasu_sukumi_num;
public Int32 odokasu_btl_num;
public Int32 odokasu_noreact_num;
public Int32[] dmy_i;
public Int32[] flag_chk_val_i;
public Int16[] flag_chk_val_s;
public UInt32 metal_exp;
public Int32[] dmy;
public Int32[] touch_val_i;
}
[Serializable]
public class CMonsterTeamData{
public string[] TeamName;
public MONSTER_TEAM_MEMBER_INFO[] StockTeamMate;
public Int16[] NowTeamID;
public Int16[] padding_s;
public Int32[] padding_i;
public Int32[] team_action_used;
public Int32[] team_action_enable;
}
[Serializable]
public class CRenkinData{
public SByte[] bit_data;
public RENKIN_MAKINF maked_info;
public Single maked_dist;
public Int32[] renkin_num;
public RENKIN_MIX_ST maked_status;
public RENKIN_RECIPE_INFO[] recipe_info;
}
[Serializable]
public class RENKIN_MIX_ST{
}
[Serializable]
public class CASINO_SAVE_DATA{
public Int32 coin;
}
[Serializable]
public class CFUSION_ITEM_SELL{
public UInt16[] exchange_num;
public Byte progress;
}
[Serializable]
public class CUiOption{
public Int32 m_controlPanel;
}
[Serializable]
public class CSuspendData{
public String m_MapName;
public FIELD_AREA m_areaType;
public SnowLevel m_mapSnowLevel;
public Boolean m_killerPanther;
public Single[] m_characterData;
public Int32 m_bgmNo;
public Boolean m_bgmPlay;
public Boolean m_suspendFlag;
}
[Serializable]
public class CUsedPresentCode{
public Char[] m_code;
}
[Serializable]
public class CPartyItemData{
public USER_PERSON_ITEM_DATA[][] user_person_item_data;
public USER_ITEM_DATA[] hukuro;
public System.Int16[,] equip_list;
}
[Serializable]
public class CPersistItem{
public Int32 m_genkiEX;
public Int32 m_genkiGold;
public Int32 m_toherosu;
public Int32 m_shinobi;
public Int32 m_seisui;
public Int32 m_superSeisui;
}
[Serializable]
public class DMG_PERFORMANCE{
public Int32 id;
public Int32 max;
public Int32[] padding;
}
[Serializable]
public class MONSTER_DATA{
public SAVE_MONSTER_DATA[] mArray;
public SAVE_MONSTER_DATA[] _items;
public Int32 _size;
public Int32 _version;
}
[Serializable]
public class BTL_SCORE_TENSION{
public Single child;
public Int32 mother;
public Int32 btlend_tame;
public Int32[] dmy;
}
[Serializable]
public class RENKIN_MAKINF{
public Int32 item_no;
public Int32[] recipe_item_no;
public Int32[] recipe_item_who;
}
[Serializable]
public class DQ_PERSON_STATUS_PARAM{
public COMMON_GAGE_INT HP;
public COMMON_GAGE_INT MP;
public Int16 Level;
public UInt32 exp;
public UInt32 status_info;
public UInt64 magic_bit;
public Int16[] st_param;
public Int16[] skill_param;
public Int16 bonus_sp_count;
public Int16 last_get_sp;
}
[Serializable]
public class MONSTER_TEAM_MEMBER_INFO{
public Int32 id;
public Int32 mp;
public MOSMEMBER_STATUS status;
}
[Serializable]
public class RENKIN_RECIPE_INFO{
public Byte data;
}
[Serializable]
public class USER_ITEM_DATA{
public Int16 item_no;
public Int16 num;
}
[Serializable]
public class SAVE_MONSTER_DATA{
public Int32 kill_num;
public Int32 oiharatta_num;
public Int16[] get_item_num;
public SByte kill_lv;
public SByte[] padding_c;
public SByte[] padding_c2;
}
[Serializable]
public class USER_PERSON_ITEM_DATA{
public Int16 item_no;
}
配列になっているプリミティブ型変数は、もしかするとポインタなのかもしれません。
変数名を見ると、なんとなーく、どんな意味のデータなのかわかりそうな感じですよね。
本当は、BinaryFomatterで開いて、データを変更し、再度書き出しができればよかったのですが、いくつか定義のされてないクラス(or構造体)の情報が取り出せなかったため、それは失敗しました。
ですが、データの型により格納サイズがわかったので、それを利用して、バイナリデータを直接いじることは成功しました。
例えばCUserDataクラスの中に
UInt16 m_partyBit;
というプロパティがありますが、ゲーム開始時点では3の数字が設定されていました。
もうわかりますよね。おそらく右から4つのビットの有無でパーティーメンバーがいるかどうかを判断しているんでしょうね。
というわけで、ヤンガスをいなくして、ゼシカをパーティーに加える意味で5(0101)にして保存し、再度iPhone内へ上書き保存。
チェックサムとかあると起動しないだろうなと思っていたのですが、あっさり起動しましたw
各キャラクタのパラメーターはDQ_PERSON_STATUS_PARAMクラスに格納されている模様で、そこの数値を書き換えることで、パラメーターが変わります。
CPartyItemDataの中にhukuroという変数があり、中はitemo_noとnumという構造体クラスになっています。
これに対応して、データを書き換えてみると、
たくさんアイテムがてにはいりました。
どのコードが何に対応するかまでは、調べてないですが、しょっぱなからラーミアで飛んでいけるようになりました。
最後に
あくまで興味本位にやっただけで、このデータではまともにプレイする気はありません。
ゲームバランスも崩れますし、皆さんも絶対に真似はしないでください。
そのうち、セーブデータは暗号化されることでしょう。