Skip to content

Commit

Permalink
实现 Xbox 存档转换为 Steam 存档
Browse files Browse the repository at this point in the history
  • Loading branch information
XcantloadX committed Jul 27, 2024
1 parent 64b6004 commit c77cdc5
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 13 deletions.
18 changes: 18 additions & 0 deletions app/deserializer/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class Struct(ABC):
def from_bytes(cls: type[Self], data: bytes) -> Self:
"""
从二进制数据中读取结构体实例。
:returns 返回值实际为 ctypes.Structure 实例。为了方便起见,标注返回类型为 Self。
"""
Expand All @@ -87,17 +88,34 @@ def from_bytes(cls: type[Self], data: bytes) -> Self:
ctype_ins = Struct_.from_buffer_copy(data)
return cast(Self, ctype_ins)

@classmethod
def from_file(cls: type[Self], file_path: str) -> Self:
"""
从文件中读取结构体实例。
"""
with open(file_path, 'rb') as f:
return cls.from_bytes(f.read())

@classmethod
def to_bytes(cls, ins: Self) -> bytes:
"""
将结构体实例转化为二进制数据。
:param ins: 结构体实例。实际类型为 ctypes.Structure,为了方便起见,标注类型为 Self。
"""
ctype_ins = cast(ctypes.Structure, ins)
if not isinstance(ins, ctypes.Structure):
raise TypeError(f"Unsupported type: {type(ins)}, expected {ctypes.Structure}")
return ctypes.string_at(ctypes.addressof(ctype_ins), ctypes.sizeof(ctype_ins))

@classmethod
def to_file(cls: type[Self], ins: Self, file_path: str) -> None:
"""
将结构体实例写入文件。
"""
with open(file_path, 'wb') as f:
f.write(cls.to_bytes(ins))

def size(self) -> int:
assert is_dataclass(self)
Struct_ = to_ctypes(type(self))
Expand Down
20 changes: 17 additions & 3 deletions app/editor/locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __xbox_app(self) -> App | None:
return find_universal_app(XBOX_APP_NAME)

@property
def system_steam_save_path(self) -> list[str]:
def system_steam_save_path(self) -> list[tuple[str, str]]:
"""
默认 Steam 存档路径。
"""
Expand All @@ -42,12 +42,12 @@ def system_steam_save_path(self) -> list[str]:
saves_path = os.path.join(steam_path, 'userdata')
# 列出子文件夹(多账号)
accounts = os.listdir(saves_path)
save_files = []
save_files: list[tuple[str, str]] = []
for account in accounts:
save_file = os.path.join(saves_path, account, '787480', 'remote', 'systemdata')
if os.path.exists(save_file):
assert os.path.getsize(save_file) == STEAM_SAVE_LENGTH
save_files.append(save_file)
save_files.append((account, save_file))
return save_files

@property
Expand Down Expand Up @@ -78,6 +78,20 @@ def steam_path(self) -> str | None:
path64 = _read_reg(ep = winreg.HKEY_LOCAL_MACHINE, p = r"SOFTWARE\Valve\Steam", k = 'InstallPath')
return path32 or path64

@property
def steam_accounts(self) -> list[str]:
"""
存在逆转裁判存档的 Steam 账号列表。
"""
steam_path = self.steam_path
if not steam_path:
logger.warning('Steam not found')
return []
saves_path = os.path.join(steam_path, 'userdata')
ret = os.listdir(saves_path)
ret = [account for account in ret if os.path.exists(os.path.join(saves_path, account, '787480', 'remote', 'systemdata'))]
return ret

@property
def steam_game_path(self) -> str | None:
"""
Expand Down
47 changes: 40 additions & 7 deletions app/structs/conventor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from .xbox import *

T = TypeVar('T')
V = TypeVar('V')
def _copy_attr(from_: object, to: T, ignore_incompatible_types: bool = True) -> T:
assert isinstance(to, Structure)
assert isinstance(from_, Structure)
Expand Down Expand Up @@ -36,12 +35,14 @@ def steam2xbox(data: PresideData) -> PresideDataXbox:
system_data_xbox = SystemDataXbox.new()
_copy_attr(system_data_steam, system_data_xbox)
system_data_xbox.option_work_ = option_work_xbox
xbox.system_data_ = system_data_xbox

# GameDataXbox
for i, game_data_steam in enumerate(data.slot_list_):
msg_data_steam = game_data_steam.msg_data_
msg_data_xbox = MessageDataXbox.new()
_copy_attr(msg_data_steam, msg_data_xbox)

game_data_xbox = GameDataXbox.new()
_copy_attr(game_data_steam, game_data_xbox)
game_data_xbox.msg_data_ = msg_data_xbox
Expand All @@ -55,16 +56,48 @@ def xbox2steam(data: PresideDataXbox) -> PresideData:
Xbox 存档中缺少的数据将会从默认空存档中读取。
"""
steam = PresideData.new()
# TODO 创建保存一个默认空存档
default_save = PresideData.from_file('res/steam_empty_save')
# OptionWork
option_work_xbox = data.system_data_.option_work_
option_work_steam = OptionWork.new()
option_work_default = default_save.system_data_.option_work_
_copy_attr(option_work_default, option_work_steam)
_copy_attr(option_work_xbox, option_work_steam)
# SystemData
system_data_xbox = data.system_data_
system_data_steam = SystemData.new()
_copy_attr(system_data_xbox, system_data_steam)
system_data_steam.option_work_ = option_work_steam
steam.system_data_ = system_data_steam

# GameData
for i, game_data_xbox in enumerate(data.slot_list_):
msg_data_xbox = game_data_xbox.msg_data_
msg_data_steam = MessageData.new()
msg_data_default = default_save.slot_list_[i].msg_data_
_copy_attr(msg_data_default, msg_data_steam)
_copy_attr(msg_data_xbox, msg_data_steam)

game_data_steam = GameData.new()
_copy_attr(game_data_xbox, game_data_steam)
game_data_steam.msg_data_ = msg_data_steam
steam.slot_list_[i] = game_data_steam

return deepcopy(steam)

if __name__ == '__main__':
steam_path = r"C:\Program Files (x86)\Steam\userdata\1082712526\787480\remote\systemdata"
xbox_path = r"C:\Users\ZhouXiaokang\AppData\Local\Packages\F024294D.PhoenixWrightAceAttorneyTrilogy_8fty0by30jkny\SystemAppData\wgs\000901F626FB5D52_A0500100226344F587F63D7231FE421D\F61F7E2F700E4F53BCE0FB54B25B0E7B\D99377AF672E4214842D30CEF4CDA5AD"
with open(steam_path, 'rb') as f:
steam = PresideData.from_bytes(f.read())
xbox = steam2xbox(steam)
import app.editor.locator as locator
steam_id, steam_path = locator.system_steam_save_path[0]
xbox_path = locator.system_xbox_save_path[0]
# with open(steam_path, 'rb') as f:
# steam = PresideData.from_bytes(f.read())
# xbox = steam2xbox(steam)

xbox = PresideDataXbox.from_file(xbox_path)
steam = xbox2steam(xbox)
steam.system_data_.reserve_work_.reserve[1] = Int32(int(steam_id))
PresideData.to_file(steam, steam_path)

1
# with open(xbox_path, 'wb') as f:
# f.write(PresideDataXbox.to_bytes(xbox))
18 changes: 15 additions & 3 deletions app/structs/xbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,20 @@ class MessageDataXbox(Struct):

if __name__ == '__main__':
import app.editor.locator as locator
from .conventor import steam2xbox
# print(PresideDataXbox().size())
# with open(r"C:\Program Files (x86)\Steam\userdata\1082712526\787480\remote\systemdata.3rd", 'rb') as f:
# sv_steam = PresideData.from_bytes(f.read())
# sv_steam_before = PresideData.to_bytes(sv_steam)
# sv_xbox = steam2xbox(sv_steam)
# sv_steam_after = PresideData.to_bytes((sv_xbox))
steam_id, steam_path = locator.system_steam_save_path[0]
path = locator.system_xbox_save_path[0]
print(PresideDataXbox().size())
with open(path, 'rb') as f:
sv = PresideDataXbox.from_bytes(f.read())
with open(steam_path, 'rb') as f:
sv_steam = PresideData.from_bytes(f.read())
sv_xbox = steam2xbox(sv_steam)
with open(path, 'wb') as f:
f.write(PresideDataXbox.to_bytes(sv_xbox))


1
1 change: 1 addition & 0 deletions make.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ cd ..

&pyinstaller `
--add-data "ui/dist;ui" `
--add-data "res;res" `
--noconfirm `
--name "PWAAT Save Editor" `
app\entry.py
Expand Down
Binary file added res/steam_empty_save
Binary file not shown.

0 comments on commit c77cdc5

Please sign in to comment.