From 31c97e5aac3ae2d2243b75df25b65d46efbdb9ee Mon Sep 17 00:00:00 2001 From: XcantloadX <3188996979@qq.com> Date: Mon, 29 Jul 2024 12:02:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=20wxPython=20=E7=89=88=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 7 + README.md | 35 ++ app/deserializer/types.py | 14 +- app/editor/save_editor.py | 177 ++++-- app/entry_native.py | 8 + app/native_ui/__init__.py | 0 app/native_ui/form.py | 250 ++++++++ app/native_ui/implement.py | 194 ++++++ app/structs/steam.py | 2 +- app/unpack/text_unpacker.py | 4 +- app/utils.py | 36 -- form.fbp | 1162 +++++++++++++++++++++++++++++++++++ make.ps1 | 3 +- requirements.txt | 3 +- res/icons/open.png | Bin 0 -> 360 bytes res/icons/save.png | Bin 0 -> 402 bytes 16 files changed, 1809 insertions(+), 86 deletions(-) create mode 100644 README.md create mode 100644 app/entry_native.py create mode 100644 app/native_ui/__init__.py create mode 100644 app/native_ui/form.py create mode 100644 app/native_ui/implement.py create mode 100644 form.fbp create mode 100644 res/icons/open.png create mode 100644 res/icons/save.png diff --git a/.vscode/launch.json b/.vscode/launch.json index 9d4f98a..53bda09 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -49,5 +49,12 @@ "module": "app.entry", "args": [ "--debug" ] }, + { + "name": "Entry Native (Debug Mode)", + "type": "debugpy", + "request": "launch", + "module": "app.native_ui.entry", + "args": [ "--debug" ] + }, ] } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6948d79 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# PWAAT-Save-Editor 逆转裁判 123 存档修改器 + +## 功能 +* 导入、导出存档 +* Steam 与 Xbox 存档互相转换 +* 解锁章节 +* 修改法庭内血量 + +## 使用教程 +### 下载 +1. 在 [Release 页](https://github.com/XcantloadX/PWAAT-Save-Editor/releases)下载最新版本的编辑器 +2. 解压后运行 `PWAAT Save Editor.exe` + +### 导入/导出存档 +Steam 转 Xbox:菜单栏 → 转换 → “Xbox ← Steam” + +Xbox 转 Steam:菜单栏 → 转换 → “Xbox → Steam” + +导入 Steam 存档:菜单栏 → 转换 → “Steam ← 文件...” + +导出 Steam 存档:菜单栏 → 转换 → “Steam → 文件...” + +### 解锁章节 +1. 关闭游戏 +2. 在“文件”菜单内打开任意存档 +3. 按需求调整“解锁章节”框内的设置 +4. “文件” → “保存” +5. 启动游戏 + +### 修改血量 +1. 在“文件”菜单内打开任意存档 +2. 检查“选择存档”处自动选中的存档是否为想要修改的存档,若不是,改为想要修改的存档 +3. 调整血量 +4. “文件” → “保存” +5. 在游戏内重新读取该存档 \ No newline at end of file diff --git a/app/deserializer/types.py b/app/deserializer/types.py index d7a328f..9da105c 100644 --- a/app/deserializer/types.py +++ b/app/deserializer/types.py @@ -1,6 +1,6 @@ import ctypes from dataclasses import is_dataclass, fields, dataclass -from typing import TypeVar, TypeAlias, NewType, Annotated, Generic, List, Text, Literal, Type, Any, Iterator +from typing import TypeVar, TypeAlias, NewType, Annotated, Generic, List, Text, Literal, Type, Any, Iterator, TypeGuard from typing import get_type_hints, get_args, get_origin, cast from typing_extensions import Self from abc import ABC @@ -41,9 +41,11 @@ def __iter__(self) -> Iterator[T]: ... def __next__(self) -> T: ... def __contains__(self, item: T) -> bool: ... -class FixedString(Generic[Len]): pass +class FixedString(Generic[Len]): + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: ... class Bytes(Generic[Len]): pass + # 类型判断 def is_primitive(t: type) -> bool: return any(t == pt for pt in ( @@ -85,6 +87,8 @@ def from_bytes(cls: type[Self], data: bytes) -> Self: """ # assert is_dataclass(dataclass) # type: ignore Struct_ = to_ctypes(cls) + if ctypes.sizeof(Struct_) != len(data): + raise ValueError(f"Data size mismatch: {ctypes.sizeof(Struct_)} != {len(data)}") ctype_ins = Struct_.from_buffer_copy(data) return cast(Self, ctype_ins) @@ -213,4 +217,8 @@ def _convert_type(py_type: Any, dependencies: dict[Type, Type] = {}) -> Any: elif py_type == int: raise TypeError(f"Unsupported built-in type: {py_type}, please use Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 instead.") else: - raise TypeError(f"Unsupported type: {py_type}") \ No newline at end of file + raise TypeError(f"Unsupported type: {py_type}") + +StructType = TypeVar('StructType', bound=Struct) +def is_struct(ins: Any, t: type[StructType]) -> TypeGuard[StructType]: + return isinstance(ins, to_ctypes(t)) \ No newline at end of file diff --git a/app/editor/save_editor.py b/app/editor/save_editor.py index 246af8b..6e2d790 100644 --- a/app/editor/save_editor.py +++ b/app/editor/save_editor.py @@ -1,12 +1,16 @@ import os from dataclasses import dataclass from enum import IntEnum +from typing import TypeGuard -from app.utils import find_game_path, find_save_path from app.structs.steam import PresideData -from app.deserializer.types import Int32, Int16 +from app.structs.xbox import PresideDataXbox +from app.deserializer.types import Int32, Int16, is_struct from app.unpack import TextUnpacker, TitleTextID, SaveTextID, Language from app.deserializer.types import UInt8 +from app.editor.locator import STEAM_SAVE_LENGTH, XBOX_SAVE_LENGTH +import app.editor.locator as locator +from app.structs.conventor import xbox2steam, steam2xbox from logging import getLogger logger = getLogger(__name__) @@ -16,12 +20,42 @@ class TitleId(IntEnum): GS2 = 1 GS3 = 2 +class SaveType(IntEnum): + UNKNOWN = -1 + STEAM = 0 + XBOX = 1 + +class NoOpenSaveFileError(Exception): + pass + @dataclass class SaveSlot: time: str progress: str + """天数/回数(e.g. 第一回 法庭前篇)""" title: str + """游戏名称(e.g. 逆转裁判 1)""" + title_number: int + """游戏编号(e.g. 1)""" scenario: str + """章节名称(e.g. 第四章 再会了,逆转)""" + scenario_number: int + """章节编号(e.g. 4)""" + + @property + def short_str(self) -> str: + if self.time == '': + return '空' + else: + time = self.time.replace('\n', ' ') + return f'{self.title_number}-{self.scenario_number} {self.progress} {time}' + + @property + def long_str(self) -> str: + if self.time == '': + return '空' + else: + return f'{self.title} {self.scenario} {self.progress} {self.time}' class SaveEditor: def __init__( @@ -30,85 +64,125 @@ def __init__( default_save_path: str|None = None, language: Language = 'en' ) -> None: - game_path = game_path or find_game_path() + game_path = game_path or locator.steam_game_path or locator.xbox_game_path + logger.debug(f'game_path: {game_path}') if not game_path: raise FileNotFoundError('Could not find game path') self.game_path = game_path - default_save_path = default_save_path or find_save_path() - if not default_save_path: - raise FileNotFoundError('Could not find default save path') - self.default_save_path = default_save_path self.__save_path: str|None = None - self.__preside_data: PresideData|None = None + self.__preside_data: PresideData|PresideDataXbox|None = None self.__language: Language = language - # self.__text_unpacker = None self.__text_unpacker = TextUnpacker(self.game_path, self.__language) def init(self): pass + def __check_save_loaded(self, _ = None) -> TypeGuard[PresideData|PresideDataXbox]: + if self.__preside_data is None: + raise NoOpenSaveFileError('No data loaded') + return True + + @property + def preside_data(self) -> PresideData|PresideDataXbox: + assert self.__check_save_loaded(self.__preside_data) + return self.__preside_data + + @property + def save_type(self) -> SaveType: + assert self.__check_save_loaded(self.__preside_data) + if is_struct(self.__preside_data, PresideData): + return SaveType.STEAM + elif is_struct(self.__preside_data, PresideDataXbox): + return SaveType.XBOX + else: + return SaveType.UNKNOWN + + @property + def opened(self) -> bool: + return self.__preside_data is not None + def get_save_path(self) -> str|None: return self.__save_path - def load(self, save_file_path: str|None = None): + def load(self, save_file_path: str): """ 加载存档数据 - :param save_file_path: 存档文件路径,默认为系统存档。 + :param save_file_path: 存档文件路径 """ - self.__save_path = save_file_path or self.default_save_path - with open(self.__save_path, 'rb') as f: - self.__preside_data = PresideData.from_bytes(f.read()) + self.__save_path = save_file_path + # 判断存档类型 + size = os.path.getsize(self.__save_path) + if size == STEAM_SAVE_LENGTH: + self.__preside_data = PresideData.from_file(self.__save_path) + elif size == XBOX_SAVE_LENGTH: + self.__preside_data = PresideDataXbox.from_file(self.__save_path) + else: + raise ValueError('Invalid save file') def save(self, save_file_path: str|None = None): - assert self.__preside_data is not None, 'No data loaded' + assert self.__check_save_loaded(self.__preside_data) if not save_file_path: - save_file_path = self.default_save_path - with open(save_file_path, 'wb') as f: - f.write(PresideData.to_bytes(self.__preside_data)) + save_file_path = self.__save_path + if save_file_path is None: + raise NoOpenSaveFileError('No save file path') + if is_struct(self.__preside_data, PresideData): + PresideData.to_file(self.__preside_data, save_file_path) + elif is_struct(self.__preside_data, PresideDataXbox): + PresideDataXbox.to_file(self.__preside_data, save_file_path) + else: + raise ValueError('Invalid save data') + + def convert(self, target: SaveType): + assert self.__check_save_loaded(self.__preside_data) + if target == SaveType.STEAM: + if is_struct(self.__preside_data, PresideDataXbox): + self.__preside_data = xbox2steam(self.__preside_data) + else: + raise ValueError('Expected Xbox save data, got Steam save data') + elif target == SaveType.XBOX: + if is_struct(self.__preside_data, PresideData): + self.__preside_data = steam2xbox(self.__preside_data) + else: + raise ValueError('Expected Steam save data, got Xbox save data') def set_account_id(self, account_id: int): """ 设置存档数据中保存的 Steam 账号 ID """ - assert self.__preside_data is not None, 'No data loaded' + assert self.__check_save_loaded(self.__preside_data) self.__preside_data.system_data_.reserve_work_.reserve[1] = Int32(account_id) def get_account_id(self) -> int: """ 获取存档数据中保存的 Steam 账号 ID """ - assert self.__preside_data is not None, 'No data loaded' + assert self.__check_save_loaded(self.__preside_data) return self.__preside_data.system_data_.reserve_work_.reserve[1] - def set_account_id_from_system(self): - """ - 从系统存档中获取 Steam 账号 ID 并设置到当前存档中 - """ - assert self.__preside_data is not None, 'No data loaded' - se = SaveEditor() - se.load() - self.set_account_id(se.get_account_id()) - - def get_slots(self) -> list[SaveSlot]: """ 获取所有存档槽位的信息 """ - assert self.__preside_data is not None, 'No data loaded' + assert self.__check_save_loaded(self.__preside_data) slots = [] # TODO 存档槽位的开始位置似乎与游戏语言有关 for i in range(50, 60): slot = self.__preside_data.system_data_.slot_data_.save_data_[i] - time = str(slot.time) + time = slot.time.decode() title = TitleId(slot.title) + title_number = 0 scenario = int(slot.scenario) + scenario_number = 0 if time != '': # title title = self.__text_unpacker.get_text(TitleTextID.TITLE_NAME, slot.title) + # title number + title_number = slot.title + 1 + # scenario if slot.title == TitleId.GS2 and slot.scenario >= 4: scenario = '' @@ -124,6 +198,9 @@ def get_slots(self) -> list[SaveSlot]: episode = self.__text_unpacker.get_text(TitleTextID.EPISODE_NUMBER, slot.scenario) scenario = f'{episode} {scenario}' + # scenario number + scenario_number = slot.scenario + 1 + # progress def get_progress_text(in_title: int, in_progress: int): in_text_id = 0 @@ -168,6 +245,8 @@ def get_progress_text(in_title: int, in_progress: int): progress=progress, title=title, scenario=scenario, + title_number=title_number, + scenario_number=scenario_number )) return slots @@ -177,7 +256,7 @@ def set_court_hp(self, slot_number: int, hp: int): :param slot_number: 游戏内存档槽位号,范围 [0, 9] :param hp: 血量值,范围 [0, 80] """ - assert self.__preside_data is not None, 'No data loaded' + assert self.__check_save_loaded(self.__preside_data) slot_number = self.__get_real_slot_number(slot_number) self.__preside_data.slot_list_[slot_number].global_work_.gauge_hp = Int16(hp) self.__preside_data.slot_list_[slot_number].global_work_.gauge_hp_disp = Int16(hp) @@ -187,7 +266,7 @@ def get_court_hp(self, slot_number: int) -> int: 获取法庭日血量值。 :param slot_number: 游戏内存档槽位号,范围 [0, 9] """ - assert self.__preside_data is not None, 'No data loaded' + assert self.__check_save_loaded(self.__preside_data) slot_number = self.__get_real_slot_number(slot_number) return self.__preside_data.slot_list_[slot_number].global_work_.gauge_hp @@ -197,32 +276,46 @@ def set_unlocked_chapters(self, game_number: int, chapter_count: int): :param game_number: 游戏编号。范围 [1, 3] :param chapter_counts: 解锁的章节数量。 """ - assert self.__preside_data is not None, 'No data loaded' - if chapter_count <= 0 or chapter_count >= 4: + assert self.__check_save_loaded(self.__preside_data) + if chapter_count <= 0 or chapter_count > 5: raise ValueError('Invalid chapter count.') sce_data = self.__preside_data.system_data_.sce_data_ enable_data = UInt8((16 & 0b1111) | (chapter_count << 4)) if game_number == 1: sce_data.GS1_Scenario_enable = enable_data - if chapter_count >= 5: - raise ValueError('Invalid chapter count.') elif game_number == 2: sce_data.GS2_Scenario_enable = enable_data + if chapter_count > 4: + raise ValueError('Invalid chapter count.') elif game_number == 3: sce_data.GS3_Scenario_enable = enable_data - if chapter_count >= 5: - raise ValueError('Invalid chapter count.') + + def get_unlocked_chapters(self, game_number: int) -> int: + """ + 获取解锁的章节数量。 + :param game_number: 游戏编号。范围 [1, 3] + """ + assert self.__check_save_loaded(self.__preside_data) + sce_data = self.__preside_data.system_data_.sce_data_ + if game_number == 1: + return sce_data.GS1_Scenario_enable >> 4 + elif game_number == 2: + return sce_data.GS2_Scenario_enable >> 4 + elif game_number == 3: + return sce_data.GS3_Scenario_enable >> 4 + else: + raise ValueError('Invalid game number.') def __get_real_slot_number(self, slot_number: int) -> int: """ 获取实际存档槽位号 """ - assert self.__preside_data is not None, 'No data loaded' + assert self.__check_save_loaded(self.__preside_data) return slot_number + 50 if __name__ == '__main__': from pprint import pprint se = SaveEditor(language='hans') - se.load() + # se.load() pprint(se.get_slots()) \ No newline at end of file diff --git a/app/entry_native.py b/app/entry_native.py new file mode 100644 index 0000000..23154cb --- /dev/null +++ b/app/entry_native.py @@ -0,0 +1,8 @@ +import app.native_ui.implement +# 让 PyInstaller 收集下面这些模块 +import winrt.windows.foundation +import winrt.windows.foundation.collections +import winrt.windows.management +import winrt.windows.management.deployment +import winrt.windows.storage +import winrt.windows.applicationmodel \ No newline at end of file diff --git a/app/native_ui/__init__.py b/app/native_ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/native_ui/form.py b/app/native_ui/form.py new file mode 100644 index 0000000..4b63abe --- /dev/null +++ b/app/native_ui/form.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- + +########################################################################### +## Python code generated with wxFormBuilder (version 4.0.0-0-g0efcecf) +## http://www.wxformbuilder.org/ +## +## PLEASE DO *NOT* EDIT THIS FILE! +########################################################################### + +import wx +import wx.xrc + +########################################################################### +## Class FrameMain +########################################################################### + +class FrameMain ( wx.Frame ): + + def __init__( self, parent ): + wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = u"逆转裁判 123 存档工具", pos = wx.DefaultPosition, size = wx.Size( 500,400 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL ) + + self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) + self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) ) + + bSizer1 = wx.BoxSizer( wx.VERTICAL ) + + self.m_panel2 = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer4 = wx.BoxSizer( wx.VERTICAL ) + + self.m_notebook1 = wx.Notebook( self.m_panel2, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_pnl_common = wx.Panel( self.m_notebook1, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) + bSizer2 = wx.BoxSizer( wx.VERTICAL ) + + sbSizer_chapters = wx.StaticBoxSizer( wx.StaticBox( self.m_pnl_common, wx.ID_ANY, u"解锁章节" ), wx.VERTICAL ) + + fgSizer1 = wx.FlexGridSizer( 3, 2, 0, 20 ) + fgSizer1.AddGrowableCol( 0 ) + fgSizer1.AddGrowableCol( 1 ) + fgSizer1.SetFlexibleDirection( wx.BOTH ) + fgSizer1.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED ) + + self.m_staticText1 = wx.StaticText( sbSizer_chapters.GetStaticBox(), wx.ID_ANY, u"逆转裁判 1", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText1.Wrap( -1 ) + + fgSizer1.Add( self.m_staticText1, 0, wx.ALL, 5 ) + + m_chc_gs1Choices = [ u"第一章 最初的逆转", u"第二章 逆转姐妹", u"第三章 逆转的大将军", u"第四章 逆转,然后再见", u"第五章 复苏的逆转" ] + self.m_chc_gs1 = wx.Choice( sbSizer_chapters.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_chc_gs1Choices, 0 ) + self.m_chc_gs1.SetSelection( 0 ) + fgSizer1.Add( self.m_chc_gs1, 0, wx.ALL, 5 ) + + self.m_staticText2 = wx.StaticText( sbSizer_chapters.GetStaticBox(), wx.ID_ANY, u"逆转裁判 2", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText2.Wrap( -1 ) + + fgSizer1.Add( self.m_staticText2, 0, wx.ALL, 5 ) + + m_chc_gs2Choices = [ u"第一章 失落的逆转", u"第二章 再会,然后逆转", u"第三章 逆转马戏团", u"第四章 再见,逆转" ] + self.m_chc_gs2 = wx.Choice( sbSizer_chapters.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_chc_gs2Choices, 0 ) + self.m_chc_gs2.SetSelection( 0 ) + fgSizer1.Add( self.m_chc_gs2, 0, wx.ALL, 5 ) + + self.m_staticText3 = wx.StaticText( sbSizer_chapters.GetStaticBox(), wx.ID_ANY, u"逆转裁判 3", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText3.Wrap( -1 ) + + fgSizer1.Add( self.m_staticText3, 0, wx.ALL, 5 ) + + m_chc_gs3Choices = [ u"第一章 回忆的逆转", u"第二章 遭窃的逆转", u"第三章 逆转的秘方", u"第四章 初始的逆转", u"第五章 华丽的逆转" ] + self.m_chc_gs3 = wx.Choice( sbSizer_chapters.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_chc_gs3Choices, 0 ) + self.m_chc_gs3.SetSelection( 0 ) + fgSizer1.Add( self.m_chc_gs3, 0, wx.ALL|wx.EXPAND, 5 ) + + + sbSizer_chapters.Add( fgSizer1, 1, 0, 5 ) + + + bSizer2.Add( sbSizer_chapters, 1, wx.EXPAND, 5 ) + + sbSizer_ingame = wx.StaticBoxSizer( wx.StaticBox( self.m_pnl_common, wx.ID_ANY, u"游戏内" ), wx.VERTICAL ) + + bSizer5 = wx.BoxSizer( wx.VERTICAL ) + + bSizer61 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText5 = wx.StaticText( sbSizer_ingame.GetStaticBox(), wx.ID_ANY, u"存档选择", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText5.Wrap( -1 ) + + bSizer61.Add( self.m_staticText5, 0, wx.ALL, 5 ) + + m_chc_savesChoices = [] + self.m_chc_saves = wx.Choice( sbSizer_ingame.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.Size( 350,-1 ), m_chc_savesChoices, 0 ) + self.m_chc_saves.SetSelection( 0 ) + bSizer61.Add( self.m_chc_saves, 0, wx.ALL, 5 ) + + + bSizer5.Add( bSizer61, 0, wx.EXPAND, 5 ) + + bSizer6 = wx.BoxSizer( wx.HORIZONTAL ) + + self.m_staticText4 = wx.StaticText( sbSizer_ingame.GetStaticBox(), wx.ID_ANY, u"法庭内血量", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText4.Wrap( -1 ) + + bSizer6.Add( self.m_staticText4, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) + + self.m_sld_hp = wx.Slider( sbSizer_ingame.GetStaticBox(), wx.ID_ANY, 0, 0, 80, wx.DefaultPosition, wx.Size( 150,-1 ), wx.SL_LABELS ) + bSizer6.Add( self.m_sld_hp, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) + + + bSizer5.Add( bSizer6, 0, wx.EXPAND, 5 ) + + + sbSizer_ingame.Add( bSizer5, 1, wx.EXPAND, 5 ) + + + bSizer2.Add( sbSizer_ingame, 1, wx.EXPAND, 5 ) + + + self.m_pnl_common.SetSizer( bSizer2 ) + self.m_pnl_common.Layout() + bSizer2.Fit( self.m_pnl_common ) + self.m_notebook1.AddPage( self.m_pnl_common, u"常用设置", True ) + + bSizer4.Add( self.m_notebook1, 1, wx.EXPAND, 5 ) + + + self.m_panel2.SetSizer( bSizer4 ) + self.m_panel2.Layout() + bSizer4.Fit( self.m_panel2 ) + bSizer1.Add( self.m_panel2, 1, wx.EXPAND |wx.ALL, 5 ) + + + self.SetSizer( bSizer1 ) + self.Layout() + self.m_menubar = wx.MenuBar( 0 ) + self.m_menu_file = wx.Menu() + self.m_mi_open = wx.MenuItem( self.m_menu_file, wx.ID_ANY, u"打开..."+ u"\t" + u"Ctrl+O", wx.EmptyString, wx.ITEM_NORMAL ) + self.m_mi_open.SetBitmap( wx.NullBitmap ) + self.m_menu_file.Append( self.m_mi_open ) + + self.m_mi_open_steam = wx.MenuItem( self.m_menu_file, wx.ID_ANY, u"打开 Steam 存档", wx.EmptyString, wx.ITEM_NORMAL ) + self.m_menu_file.Append( self.m_mi_open_steam ) + + self.m_mi_open_xbox = wx.MenuItem( self.m_menu_file, wx.ID_ANY, u"打开 Xbox 存档", wx.EmptyString, wx.ITEM_NORMAL ) + self.m_menu_file.Append( self.m_mi_open_xbox ) + + self.m_menu_file.AppendSeparator() + + self.m_mi_save = wx.MenuItem( self.m_menu_file, wx.ID_ANY, u"保存"+ u"\t" + u"Ctrl+S", wx.EmptyString, wx.ITEM_NORMAL ) + self.m_mi_save.SetBitmap( wx.NullBitmap ) + self.m_menu_file.Append( self.m_mi_save ) + + self.m_mi_save_as = wx.MenuItem( self.m_menu_file, wx.ID_ANY, u"另存为...", wx.EmptyString, wx.ITEM_NORMAL ) + self.m_menu_file.Append( self.m_mi_save_as ) + + self.m_menu_file.AppendSeparator() + + self.m_mi_show_debug = wx.MenuItem( self.m_menu_file, wx.ID_ANY, u"显示调试信息", wx.EmptyString, wx.ITEM_NORMAL ) + self.m_menu_file.Append( self.m_mi_show_debug ) + + self.m_menubar.Append( self.m_menu_file, u"文件" ) + + self.m_menu_convert = wx.Menu() + self.m_mi_xbox2steam = wx.MenuItem( self.m_menu_convert, wx.ID_ANY, u"Xbox → Steam", wx.EmptyString, wx.ITEM_NORMAL ) + self.m_menu_convert.Append( self.m_mi_xbox2steam ) + + self.m_mi_steam2xbox = wx.MenuItem( self.m_menu_convert, wx.ID_ANY, u"Xbox ← Steam", wx.EmptyString, wx.ITEM_NORMAL ) + self.m_menu_convert.Append( self.m_mi_steam2xbox ) + + self.m_menu_convert.AppendSeparator() + + self.m_mi_steam2file = wx.MenuItem( self.m_menu_convert, wx.ID_ANY, u"Steam → 文件...", wx.EmptyString, wx.ITEM_NORMAL ) + self.m_menu_convert.Append( self.m_mi_steam2file ) + + self.m_mi_file2steam = wx.MenuItem( self.m_menu_convert, wx.ID_ANY, u"Steam ← 文件...", wx.EmptyString, wx.ITEM_NORMAL ) + self.m_menu_convert.Append( self.m_mi_file2steam ) + + self.m_menubar.Append( self.m_menu_convert, u"转换" ) + + self.SetMenuBar( self.m_menubar ) + + + self.Centre( wx.BOTH ) + + # Connect Events + self.m_chc_gs1.Bind( wx.EVT_CHOICE, self.chc_gs1_on_choice ) + self.m_chc_gs2.Bind( wx.EVT_CHOICE, self.chc_gs2_on_choice ) + self.m_chc_gs3.Bind( wx.EVT_CHOICE, self.chc_gs3_on_choice ) + self.m_chc_saves.Bind( wx.EVT_CHOICE, self.chc_savs_on_choice ) + self.m_sld_hp.Bind( wx.EVT_SCROLL_CHANGED, self.sld_hp_on_scroll_changed ) + self.Bind( wx.EVT_MENU, self.mi_open_on_select, id = self.m_mi_open.GetId() ) + self.Bind( wx.EVT_MENU, self.mi_open_steam_on_select, id = self.m_mi_open_steam.GetId() ) + self.Bind( wx.EVT_MENU, self.mi_open_xbox_on_select, id = self.m_mi_open_xbox.GetId() ) + self.Bind( wx.EVT_MENU, self.mi_save_on_select, id = self.m_mi_save.GetId() ) + self.Bind( wx.EVT_MENU, self.mi_save_as_on_select, id = self.m_mi_save_as.GetId() ) + self.Bind( wx.EVT_MENU, self.mi_show_debug_on_select, id = self.m_mi_show_debug.GetId() ) + self.Bind( wx.EVT_MENU, self.mi_xbox2steam_on_choice, id = self.m_mi_xbox2steam.GetId() ) + self.Bind( wx.EVT_MENU, self.mi_steam2xbox_on_choice, id = self.m_mi_steam2xbox.GetId() ) + self.Bind( wx.EVT_MENU, self.mi_steam2file_on_choice, id = self.m_mi_steam2file.GetId() ) + self.Bind( wx.EVT_MENU, self.mi_file2steam_on_choice, id = self.m_mi_file2steam.GetId() ) + + def __del__( self ): + pass + + + # Virtual event handlers, override them in your derived class + def chc_gs1_on_choice( self, event ): + event.Skip() + + def chc_gs2_on_choice( self, event ): + event.Skip() + + def chc_gs3_on_choice( self, event ): + event.Skip() + + def chc_savs_on_choice( self, event ): + event.Skip() + + def sld_hp_on_scroll_changed( self, event ): + event.Skip() + + def mi_open_on_select( self, event ): + event.Skip() + + def mi_open_steam_on_select( self, event ): + event.Skip() + + def mi_open_xbox_on_select( self, event ): + event.Skip() + + def mi_save_on_select( self, event ): + event.Skip() + + def mi_save_as_on_select( self, event ): + event.Skip() + + def mi_show_debug_on_select( self, event ): + event.Skip() + + def mi_xbox2steam_on_choice( self, event ): + event.Skip() + + def mi_steam2xbox_on_choice( self, event ): + event.Skip() + + def mi_steam2file_on_choice( self, event ): + event.Skip() + + def mi_file2steam_on_choice( self, event ): + event.Skip() + + diff --git a/app/native_ui/implement.py b/app/native_ui/implement.py new file mode 100644 index 0000000..e0aae8a --- /dev/null +++ b/app/native_ui/implement.py @@ -0,0 +1,194 @@ +import sys +import traceback +import datetime + +import wx + +from .form import FrameMain +from app.editor.save_editor import SaveEditor, NoOpenSaveFileError, SaveType +import app.editor.locator as locator + +def _excepthook(type, value, tb): + if type == NoOpenSaveFileError: + # 不直接调用是因为弹出的消息框会导致原来的焦点控件多次聚焦, + # 形成多次值改变事件,进而弹出多个消息框 + wx.CallAfter(wx.MessageBox, '请先打开任意存档文件。', '错误', wx.OK | wx.ICON_ERROR) + else: + # 提示错误 + wx.MessageBox(''.join(traceback.format_exception(type, value, tb)), '错误', wx.OK | wx.ICON_ERROR) + traceback.print_exception(type, value, tb) +sys.excepthook = _excepthook + +class Dialog(wx.Dialog): + def __init__(self, parent, title, text): + super().__init__(parent, title=title, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) + + sizer = wx.BoxSizer(wx.VERTICAL) + + text_ctrl = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY) + text_ctrl.SetValue(text) + sizer.Add(text_ctrl, proportion=1, flag=wx.EXPAND | wx.ALL, border=10) + + button = wx.Button(self, label="确认") + button.Bind(wx.EVT_BUTTON, self.on_confirm) + sizer.Add(button, flag=wx.ALIGN_CENTER | wx.ALL, border=10) + + self.SetSizerAndFit(sizer) + self.SetSize(600, 400) + + def on_confirm(self, event): + self.EndModal(wx.ID_OK) + +class FrameMainImpl(FrameMain): + def __init__(self, parent): + super().__init__(parent) + self.m_sld_hp.SetLineSize(8) + + self.editor = SaveEditor(language='hans') # TODO: i18n + + + def sld_hp_on_scroll_changed(self, event): + # 强制步进 8 + value = self.m_sld_hp.Value + remainder = value % 8 + if remainder != 0: + value -= remainder + self.m_sld_hp.Value = value + + # 处理事件 + self.editor.set_court_hp(self.m_chc_saves.GetSelection(), self.m_sld_hp.Value) + + def mi_open_on_select(self, event): + with wx.FileDialog(self, "打开存档文件", wildcard="存档文件 (*.*)|*.*", style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fileDialog: + if fileDialog.ShowModal() == wx.ID_CANCEL: + return + path = fileDialog.GetPath() + self.editor.load(path) + self.load_ui() + + def mi_open_steam_on_select(self, event): + _, steam_path = locator.system_steam_save_path[0] + self.editor.load(steam_path) + self.load_ui() + + def mi_open_xbox_on_select(self, event): + xbox_path = locator.system_xbox_save_path[0] + self.editor.load(xbox_path) + self.load_ui() + + def mi_save_on_select(self, event): + self.editor.save() + wx.MessageBox('保存成功', '提示', wx.OK | wx.ICON_INFORMATION) + + def mi_xbox2steam_on_choice(self, event): + if not wx.MessageBox('此操作将会覆盖当前 Steam 存档文件,是否继续?', '警告', wx.YES_NO | wx.ICON_WARNING) == wx.YES: + return + xbox_path = locator.system_xbox_save_path[0] + steam_id, steam_path = locator.system_steam_save_path[0] + self.editor.load(xbox_path) + self.editor.set_account_id(int(steam_id)) + self.editor.convert(SaveType.STEAM) + self.editor.save(steam_path) + wx.MessageBox('转换成功', '提示', wx.OK | wx.ICON_INFORMATION) + + def mi_steam2xbox_on_choice(self, event): + if not wx.MessageBox('此操作将会覆盖当前 Xbox 存档文件,是否继续?', '警告', wx.YES_NO | wx.ICON_WARNING) == wx.YES: + return + xbox_path = locator.system_xbox_save_path[0] + _, steam_path = locator.system_steam_save_path[0] + self.editor.load(steam_path) + self.editor.convert(SaveType.XBOX) + self.editor.save(xbox_path) + wx.MessageBox('转换成功', '提示', wx.OK | wx.ICON_INFORMATION) + + def mi_save_as_on_select(self, event): + with wx.FileDialog(self, "保存存档文件", wildcard="存档文件 (*.*)|*.*", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog: + if fileDialog.ShowModal() == wx.ID_CANCEL: + return + path = fileDialog.GetPath() + self.editor.save(path) + wx.MessageBox('保存成功', '提示', wx.OK | wx.ICON_INFORMATION) + + def mi_show_debug_on_select(self, event): + msg = 'Steam 路径: ' + repr(locator.steam_path) + '\n' + msg += 'Steam 游戏路径: ' + repr(locator.steam_game_path) + '\n' + msg += 'Steam 存档路径: ' + repr(locator.system_steam_save_path) + '\n' + msg += 'Xbox 游戏路径: ' + repr(locator.xbox_game_path) + '\n' + msg += 'Xbox 存档路径: ' + repr(locator.system_xbox_save_path) + '\n' + if self.editor.opened: + msg += '存档类型: ' + repr(self.editor.save_type) + '\n' + msg += 'Steam ID: ' + repr(self.editor.get_account_id()) + '\n' + # 显示信息 + dialog = Dialog(self, '调试信息', msg) + dialog.ShowModal() + dialog.Destroy() + + def load_ui(self): + # 解锁章节 + gs1 = self.editor.get_unlocked_chapters(1) + self.m_chc_gs1.SetSelection(gs1 - 1) + gs2 = self.editor.get_unlocked_chapters(2) + self.m_chc_gs2.SetSelection(gs2 - 1) + gs3 = self.editor.get_unlocked_chapters(3) + self.m_chc_gs3.SetSelection(gs3 - 1) + + # 存档选择 + self.m_chc_saves.Clear() + slots = self.editor.get_slots() + for i, slot in enumerate(slots, 1): + self.m_chc_saves.Append(f'{i:02d}: ' + slot.short_str) + newest = max(slots, key=( + lambda slot: + datetime.datetime.strptime(slot.time, '%Y/%m/%d %H:%M:%S') if slot.time + else datetime.datetime(1, 1, 1) + )) + self.m_chc_saves.SetSelection(slots.index(newest)) + + # 触发选择存档事件 + self.chc_savs_on_choice(None) + + def chc_savs_on_choice(self, event): + slot_number = self.m_chc_saves.GetSelection() + if slot_number == wx.NOT_FOUND: + return + self.m_sld_hp.Value = self.editor.get_court_hp(slot_number) + + def mi_file2steam_on_choice(self, event): + with wx.FileDialog(self, "打开文件", wildcard="存档文件 (*.*)|*.*", style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fileDialog: + if fileDialog.ShowModal() == wx.ID_CANCEL: + return + if not wx.MessageBox('此操作将会覆盖当前 Steam 存档文件,是否继续?', '警告', wx.YES_NO | wx.ICON_WARNING) == wx.YES: + return + path = fileDialog.GetPath() + self.editor.load(path) + steam_id, steam_path = locator.system_steam_save_path[0] + self.editor.set_account_id(int(steam_id)) + self.editor.save(steam_path) + wx.MessageBox('导入成功', '提示', wx.OK | wx.ICON_INFORMATION) + + def mi_steam2file_on_choice(self, event): + # TODO 自动检测存档类型 + with wx.FileDialog(self, "保存文件", wildcard="存档文件 (*.*)|*.*", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog: + if fileDialog.ShowModal() == wx.ID_CANCEL: + return + path = fileDialog.GetPath() + _, steam_path = locator.system_steam_save_path[0] + self.editor.load(steam_path) + if wx.MessageBox('是否擦除存档中 Steam ID 信息?', '提示', wx.YES_NO | wx.ICON_QUESTION) == wx.YES: + self.editor.set_account_id(111111111) + self.editor.save(path) + wx.MessageBox('导出成功', '提示', wx.OK | wx.ICON_INFORMATION) + + def chc_gs1_on_choice(self, event): + self.editor.set_unlocked_chapters(1, self.m_chc_gs1.GetSelection() + 1) + + def chc_gs2_on_choice(self, event): + self.editor.set_unlocked_chapters(2, self.m_chc_gs2.GetSelection() + 1) + + def chc_gs3_on_choice(self, event): + self.editor.set_unlocked_chapters(3, self.m_chc_gs3.GetSelection() + 1) + +app = wx.App() +frame = FrameMainImpl(None) +frame.Show() +app.MainLoop() diff --git a/app/structs/steam.py b/app/structs/steam.py index 9e593ad..3515c14 100644 --- a/app/structs/steam.py +++ b/app/structs/steam.py @@ -161,7 +161,7 @@ class GlobalWork(Struct): gauge_rno_1: byte gauge_hp: short """ - 血量(?),满血为 80\n + 血量(?),满血为 80,总共有 10 格,一格血量为 8。\n 若修改血量,需要同时修改 `gauge_hp` 和 `gauge_hp_disp。` """ gauge_hp_disp: short diff --git a/app/unpack/text_unpacker.py b/app/unpack/text_unpacker.py index 5541095..4433bc5 100644 --- a/app/unpack/text_unpacker.py +++ b/app/unpack/text_unpacker.py @@ -3,7 +3,7 @@ from .decompiled import * from .decrypt import decrypt_bytes -from app.utils import find_game_path +import app.editor.locator as locator class TextUnpacker: @@ -16,7 +16,7 @@ def __init__( :param game_path: 游戏安装路径。默认为自动搜索 :param language: 语言。默认为英文 """ - self.game_path = game_path or find_game_path() + self.game_path = game_path or locator.steam_game_path or locator.xbox_game_path if not self.game_path: raise FileNotFoundError('Could not find game path') diff --git a/app/utils.py b/app/utils.py index b05c6dc..21b405d 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,37 +1 @@ import os - - - -def find_game_path() -> str|None: - """ - 自动搜索游戏安装路径 - """ - drives = ['C:', 'D:', 'E:', 'F:', 'G:', 'H:', 'I:', 'J:', 'K:', 'L:', 'M:', 'N:', 'O:', 'P:', 'Q:', 'R:', 'S:', 'T:', 'U:', 'V:', 'W:', 'X:', 'Y:', 'Z:'] - for drive in drives: - steam_library = os.path.join(drive, 'SteamLibrary') - game_path = os.path.join(steam_library, 'steamapps', 'common', 'Phoenix Wright Ace Attorney Trilogy') - if os.path.exists(game_path): - return game_path - return None - -def find_save_path() -> str|None: - """ - 自动搜索存档路径 - """ - # 首先搜索 Steam 安装路径 - guesses = [ - 'C:\\Program Files (x86)\\Steam', - 'D:\\Program Files (x86)\\Steam', - 'E:\\Program Files (x86)\\Steam', - 'F:\\Program Files (x86)\\Steam', - 'G:\\Program Files (x86)\\Steam', - ] - guesses = [path for path in guesses if os.path.exists(path)] - if not guesses: - return None - steam_path = guesses[0] - # C:\Program Files (x86)\Steam\userdata\1082712526\787480\remote - user_path = os.path.join(steam_path, 'userdata', '1082712526', '787480', 'remote', 'systemdata') - if os.path.exists(user_path): - return user_path - return None \ No newline at end of file diff --git a/form.fbp b/form.fbp new file mode 100644 index 0000000..d55bde1 --- /dev/null +++ b/form.fbp @@ -0,0 +1,1162 @@ + + + + + ; + Python + 1 + source_name + 0 + 0 + res + UTF-8 + connect + form + 1000 + none + + 1 + 0 + pwaat-save-editor-ui + + ./app/native_ui + + 1 + 1 + 1 + 1 + UI + 0 + 0 + 0 + + 0 + wxAUI_MGR_DEFAULT + wxSYS_COLOUR_WINDOW + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + FrameMain + + 500,400 + wxDEFAULT_FRAME_STYLE + ; ; forward_declare + 逆转裁判 123 存档工具 + + 0 + + + wxTAB_TRAVERSAL + 1 + + + bSizer1 + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panel2 + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer4 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + 1 + 1 + 1 + 1 + + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_notebook1 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + + + 常用设置 + 1 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_pnl_common + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + bSizer2 + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + wxID_ANY + 解锁章节 + + sbSizer_chapters + wxVERTICAL + 1 + none + + 5 + + 1 + + 2 + wxBOTH + 0,1 + + 20 + + fgSizer1 + wxFLEX_GROWMODE_SPECIFIED + none + 3 + 0 + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 逆转裁判 1 + 0 + + 0 + + + 0 + + 1 + m_staticText1 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + "第一章 最初的逆转" "第二章 逆转姐妹" "第三章 逆转的大将军" "第四章 逆转,然后再见" "第五章 复苏的逆转" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_chc_gs1 + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + chc_gs1_on_choice + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 逆转裁判 2 + 0 + + 0 + + + 0 + + 1 + m_staticText2 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + "第一章 失落的逆转" "第二章 再会,然后逆转" "第三章 逆转马戏团" "第四章 再见,逆转" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_chc_gs2 + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + chc_gs2_on_choice + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 逆转裁判 3 + 0 + + 0 + + + 0 + + 1 + m_staticText3 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + "第一章 回忆的逆转" "第二章 遭窃的逆转" "第三章 逆转的秘方" "第四章 初始的逆转" "第五章 华丽的逆转" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_chc_gs3 + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + chc_gs3_on_choice + + + + + + + + 5 + wxEXPAND + 1 + + wxID_ANY + 游戏内 + + sbSizer_ingame + wxVERTICAL + 1 + none + + 5 + wxEXPAND + 1 + + + bSizer5 + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + + bSizer61 + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 存档选择 + 0 + + 0 + + + 0 + + 1 + m_staticText5 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_chc_saves + 1 + + + protected + 1 + + Resizable + 0 + 1 + 350,-1 + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + chc_savs_on_choice + + + + + + 5 + wxEXPAND + 0 + + + bSizer6 + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER_VERTICAL|wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 法庭内血量 + 0 + + 0 + + + 0 + + 1 + m_staticText4 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALIGN_CENTER_VERTICAL|wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 80 + + 0 + + 0 + + 0 + + 1 + m_sld_hp + 1 + + + protected + 1 + + Resizable + 1 + 150,-1 + wxSL_LABELS + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + sld_hp_on_scroll_changed + + + + + + + + + + + + + + + + + + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + + m_menubar + protected + + + + ; ; forward_declare + + + + + + 文件 + m_menu_file + protected + + Load From File; + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + 打开... + m_mi_open + none + Ctrl+O + + mi_open_on_select + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + 打开 Steam 存档 + m_mi_open_steam + none + + + mi_open_steam_on_select + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + 打开 Xbox 存档 + m_mi_open_xbox + none + + + mi_open_xbox_on_select + + + m_separator1 + none + + + Load From File; + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + 保存 + m_mi_save + none + Ctrl+S + + mi_save_on_select + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + 另存为... + m_mi_save_as + none + + + mi_save_as_on_select + + + m_separator3 + none + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + 显示调试信息 + m_mi_show_debug + none + + + mi_show_debug_on_select + + + + 转换 + m_menu_convert + protected + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Xbox → Steam + m_mi_xbox2steam + none + + + mi_xbox2steam_on_choice + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Xbox ← Steam + m_mi_steam2xbox + none + + + mi_steam2xbox_on_choice + + + m_separator2 + none + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Steam → 文件... + m_mi_steam2file + none + + + mi_steam2file_on_choice + + + + 0 + 1 + + wxID_ANY + wxITEM_NORMAL + Steam ← 文件... + m_mi_file2steam + none + + + mi_file2steam_on_choice + + + + + + diff --git a/make.ps1 b/make.ps1 index ef78785..d75ba9a 100644 --- a/make.ps1 +++ b/make.ps1 @@ -14,7 +14,8 @@ cd .. --add-data "res;res" ` --noconfirm ` --name "PWAAT Save Editor" ` - app\entry.py + .\app\entry_native.py + # app\entry.py $bat = @" @echo off diff --git a/requirements.txt b/requirements.txt index ec713c4..1da17f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ winrt-Windows.Foundation==2.1.0 winrt-Windows.Foundation.Collections==2.1.0 winrt-Windows.Management==2.1.0 winrt-Windows.Management.Deployment==2.1.0 -winrt-Windows.Storage==2.1.0 \ No newline at end of file +winrt-Windows.Storage==2.1.0 +wxPython==4.2.1 \ No newline at end of file diff --git a/res/icons/open.png b/res/icons/open.png new file mode 100644 index 0000000000000000000000000000000000000000..8b34c6c3e5408ffd7ebb597f8e2606fadaabe19c GIT binary patch literal 360 zcmV-u0hj)XP)~;&oB55N5_Z%oPb*#LRgq)JW62r%|8jUG($S6=G`k~`w3p} z9d=ECF=iGxjssy^;G7}8ZDFIah=+k93C+zXY@J+Sxf9|`k1!2MNOr!vh9pU6C%Aqb zVt;c5AKe4!{wXf^)?NMQuIw+^ zWjIXO4wW%50fqEET^vI=t|uonFs3C77ziaKloj%pa(ad}7~SDnz$4C~EYi!!mSTKh znw0c`SVjghO-b&9@1z$1wW^l5MwFx^mZVxG7o`Fz1|tI_6I}yyT_cMS0}Cq?3oBzo zZ36=<1B0BKb8AsFgculF8JSxd8bCBmUi9D;P=f~ChLX(O j)Z&uF+yXR9OpUAz%piKMixyP@^)Pt4`njxgN@xNAb@g&* literal 0 HcmV?d00001