Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: XcantloadX/PWAAT-Save-Editor
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.5.1
Choose a base ref
...
head repository: XcantloadX/PWAAT-Save-Editor
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 18 commits
  • 22 files changed
  • 2 contributors

Commits on Sep 11, 2024

  1. Copy the full SHA
    c951332 View commit details

Commits on Oct 5, 2024

  1. Copy the full SHA
    5375bbf View commit details
  2. Copy the full SHA
    17adba7 View commit details
  3. 支持高 DPI

    XcantloadX committed Oct 5, 2024
    Copy the full SHA
    55e4b43 View commit details

Commits on Oct 10, 2024

  1. Copy the full SHA
    1bf6b1c View commit details
  2. Copy the full SHA
    4ec5189 View commit details
  3. Copy the full SHA
    28ed26c View commit details
  4. Copy the full SHA
    f9a5169 View commit details
  5. Copy the full SHA
    9ac74e1 View commit details

Commits on Oct 30, 2024

  1. Copy the full SHA
    0e55cf4 View commit details

Commits on Nov 10, 2024

  1. Copy the full SHA
    58f5f68 View commit details
  2. Merge pull request #5 from userwljs/fix-invalid-save-crashes

    修复了因处于 Steam 存档位置的无效存档崩溃的 bug。
    XcantloadX authored Nov 10, 2024
    Copy the full SHA
    15f104e View commit details
  3. Copy the full SHA
    e10621d View commit details

Commits on Dec 29, 2024

  1. Copy the full SHA
    cb71c82 View commit details

Commits on Feb 8, 2025

  1. Copy the full SHA
    9b1f2d0 View commit details
  2. Copy the full SHA
    c552e09 View commit details
  3. Copy the full SHA
    5f45d37 View commit details

Commits on Feb 12, 2025

  1. Copy the full SHA
    e3265ec View commit details
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
##################
.no_backup
##################
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
7 changes: 3 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -43,14 +43,13 @@
"module": "app.editor.save_editor"
},
{
"name": "Entry (Debug Mode)",
"name": "Run: native_ui.fancy.wx_save_slot",
"type": "debugpy",
"request": "launch",
"module": "app.entry",
"args": [ "--debug" ]
"module": "app.native_ui.fancy.wx_save_slot"
},
{
"name": "Entry Native (Debug Mode)",
"name": "Entry (Debug Mode)",
"type": "debugpy",
"request": "launch",
"module": "app.entry_native",
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# PWAAT-Save-Editor 逆转裁判 123 存档修改器
[For English users](./README.en.md)

## 功能
* 导入、导出存档
44 changes: 40 additions & 4 deletions app/editor/locator.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
from typing import Any

from .installed_apps import find_desktop_app, find_universal_app, App
from app.exceptions import InvalidSaveLengthError

logger = getLogger(__name__)

@@ -22,10 +23,11 @@ def _read_reg(ep, p = r"", k = ''):
XBOX_SAVE_LENGTH = 1492008
XBOX_APP_NAME = 'F024294D.PhoenixWrightAceAttorneyTrilogy_8fty0by30jkny'
STEAM_APP_NAME = 'Steam App 787480'

class _Locator:

def __init__(self) -> None:
self.__xbox_app_cache: App | None = None
self._custom_game_path: str|None = None

def __xbox_app(self) -> App | None:
return find_universal_app(XBOX_APP_NAME)
@@ -40,13 +42,20 @@ def system_steam_save_path(self) -> list[tuple[str, str]]:
logger.warning('Steam not found')
return []
saves_path = os.path.join(steam_path, 'userdata')
logger.debug(f'saves_path: {saves_path}')
if not os.path.exists(saves_path):
logger.warning('Steam saves does not exist')
return []
# 列出子文件夹(多账号)
accounts = os.listdir(saves_path)
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
if os.path.getsize(save_file) != STEAM_SAVE_LENGTH:
# raise InvaildSaveLengthError(save_file, STEAM_SAVE_LENGTH, os.path.getsize(save_file))
logger.warning(f'Save file "{save_file}" length is not {STEAM_SAVE_LENGTH}, skipped.')
continue
save_files.append((account, save_file))
return save_files

@@ -98,7 +107,12 @@ def steam_game_path(self) -> str | None:
Steam 游戏安装路径。
"""
app = find_desktop_app(STEAM_APP_NAME)
return app.installed_path if app else None
if not app:
return None
path = app.installed_path or ''
if not os.path.exists(path):
return None
return path

@property
def xbox_game_path(self) -> str | None:
@@ -111,6 +125,28 @@ def xbox_game_path(self) -> str | None:
except Exception as e:
logger.warning(e)
return app.installed_path if app else None

@property
def game_path(self) -> str|None:
"""
游戏安装路径。
优先级:自定义路径 > Steam > Xbox
"""
if self._custom_game_path:
return self._custom_game_path
elif self.steam_game_path:
return self.steam_game_path
elif self.xbox_game_path:
return self.xbox_game_path
else:
return None

@game_path.setter
def game_path(self, value: str|None):
"""
设置自定义游戏路径。
"""
self._custom_game_path = value

_ins = _Locator()

@@ -120,7 +156,7 @@ def xbox_game_path(self) -> str | None:
steam_path = _ins.steam_path
steam_game_path = _ins.steam_game_path
xbox_game_path = _ins.xbox_game_path

game_path = _ins.game_path

if __name__ == '__main__':
print(_ins.steam_path)
29 changes: 19 additions & 10 deletions app/editor/save_editor.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import os
from dataclasses import dataclass
from enum import IntEnum
from typing import TypeGuard, Literal, TypeVar, Generic, cast, overload
from typing import TypeGuard, Literal, TypeVar, Generic, cast, overload, Callable
from gettext import gettext as _

from app.exceptions import NoGameFoundError, NoOpenSaveFileError
from app.structs.steam import PresideData, GameData
from app.structs.xbox import PresideDataXbox
from app.deserializer.types import Int32, Int16, UInt16, UInt8, Int8, is_struct, FixedString
@@ -25,11 +26,6 @@ class SaveType(IntEnum):
STEAM = 0
XBOX = 1

class NoOpenSaveFileError(Exception):
pass

class NoGameFoundError(Exception):
pass

def lang2lang_id(language: Language) -> int:
match language:
@@ -73,6 +69,8 @@ def lang_id2lang(language_id: int) -> Language:

@dataclass
class SaveSlot:
in_game_slot_number: int
"""游戏内存档槽位号。从 0 开始。"""
time: str
progress: str
"""天数/回数(e.g. 第一回 法庭前篇)"""
@@ -91,14 +89,14 @@ def short_str(self) -> str:
return _(u'空')
else:
time = self.time.replace('\n', ' ')
return f'{self.title_number}-{self.scenario_number} {self.progress} {time}'
return f'{self.in_game_slot_number+1:02d}: {self.title_number}-{self.scenario_number} {self.progress} {time}'

@property
def long_str(self) -> str:
if self.time == '':
return _(u'空')
else:
return f'{self.title} {self.scenario} {self.progress} {self.time}'
return f'{self.in_game_slot_number+1:02d}: {self.title} {self.scenario} {self.progress} {self.time}'

class SaveEditorDialog:
def __init__(self, editor: 'SaveEditor') -> None:
@@ -168,14 +166,16 @@ def text_line3(self, value: str):
self.editor.preside_data.slot_list_[self.editor.selected_slot].msg_data_.msg_line03 = FixedString[Literal[512]](buff)

T = TypeVar('T', PresideData, PresideDataXbox)
PreSaveCallback = Callable[['SaveEditor', T], bool]
class SaveEditor(Generic[T]):
def __init__(
self,
game_path: str|None = None,
default_save_path: str|None = None,
language: Language = 'en'
language: Language = 'en',
presave_event: PreSaveCallback = lambda _, __: True
) -> None:
game_path = game_path or locator.steam_game_path or locator.xbox_game_path
game_path = game_path or locator.game_path
logger.debug(f'game_path: {game_path}')
if not game_path:
raise NoGameFoundError('Could not find game path')
@@ -189,6 +189,12 @@ def __init__(

self.selected_slot: int = 0
"""当前选择的实际存档槽位号。使用 `select_slot()` 方法来选择存档槽位。"""
self.presave_event: PreSaveCallback = presave_event
"""
在存档数据即将保存时触发的回调函数。
:return: 是否继续保存
"""

def __check_save_loaded(self, _ = None) -> TypeGuard[T]:
if self.__preside_data is None:
@@ -337,6 +343,8 @@ def reload(self):

def save(self, save_file_path: str|None = None):
assert self.__check_save_loaded(self.__preside_data)
if not self.presave_event(self, self.__preside_data):
return
if not save_file_path:
save_file_path = self.__save_path
if save_file_path is None:
@@ -500,6 +508,7 @@ def get_progress_text(in_title: int, in_progress: int):
scenario = ''

slots.append(SaveSlot(
in_game_slot_number=(i - start),
time=time,
progress=progress,
title=title,
10 changes: 2 additions & 8 deletions app/editor/slot_editor.py
Original file line number Diff line number Diff line change
@@ -4,9 +4,7 @@
from .save_editor import SaveEditor, SaveType
from app.structs.steam import PresideData, GameData, SaveData
from app.structs.xbox import PresideDataXbox, GameDataXbox

class IncompatibleSlotError(Exception):
pass
from app.exceptions import IncompatibleSlotError

def is_steam_editor(editor: SaveEditor) -> TypeGuard[SaveEditor[PresideData]]:
if not isinstance(editor, SaveEditor):
@@ -37,11 +35,7 @@ def is_same_slot_type(editor1: SaveEditor, editor2: SaveEditor) -> bool:
return True
return False

class IncompatibleSlotError(Exception):
def __init__(self, left_type: SaveType, right_type: SaveType) -> None:
super().__init__(f'Incompatible slot types: {left_type} and {right_type}')
self.left_type = left_type
self.right_type = right_type


T = TypeVar('T', PresideData, PresideDataXbox)
class SlotEditor(Generic[T]):
19 changes: 19 additions & 0 deletions app/entry_native.py
Original file line number Diff line number Diff line change
@@ -3,6 +3,25 @@
gettext.bindtextdomain('base', './locales')
gettext.textdomain('base')

# 错误处理
# GUI 初始化后会被消息弹窗版本的错误处理覆盖
import sys
import traceback

def excepthook(exc_type, exc_value, exc_traceback):
print('=' * 30)
print('程序崩溃了')
print('截图下面的画面并发送给开发者')
print('APP CRASHED')
print('Please screenshot the error message and report it to the developer')
print('=' * 30)
traceback.print_exception(exc_type, exc_value, exc_traceback)
print('=' * 30)
input('按回车键退出...')
input('Press Enter to exit...')

sys.excepthook = excepthook

# 程序入口
import app.native_ui.implement

52 changes: 52 additions & 0 deletions app/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import TYPE_CHECKING

from gettext import gettext as _

if TYPE_CHECKING:
from app.editor.save_editor import SaveType

class PWAATEditorError(Exception):
pass

class GameNotFoundError(PWAATEditorError):
def __init__(self) -> None:
super().__init__(_("Game not found. Have you installed the game?"))


class GameFileMissingError(PWAATEditorError):
def __init__(self, file: str) -> None:
self.file = file
super().__init__(_(
"Game file missing: {}. "
"Make sure you've installed Steam/Xbox version of game. "
"Try to check game integrity."
).format(file))

class InvalidSaveLengthError(PWAATEditorError):
"""存档实际长度与预期长度不正确"""
def __init__(self, path: str, expected: int, actual: int):
self.path = path
self.expected = expected
self.actual = actual
super().__init__(_(
'Invalid save file length: {path}, expected {expected}, got {actual}. '
'This might be caused by corrupted save file or save type mismatch.'
).format(path=path, expected=expected, actual=actual))

class NoOpenSaveFileError(PWAATEditorError):
pass

class NoGameFoundError(PWAATEditorError):
pass

class IncompatibleSlotError(PWAATEditorError):
def __init__(self, left_type: 'SaveType', right_type: 'SaveType') -> None:
self.left_type = left_type
self.right_type = right_type
super().__init__(_(
'Incompatible slot types: '
'left is {left_type} and right is {right_type}'
).format(left_type=left_type, right_type=right_type))

if __name__ == "__main__":
raise GameNotFoundError()
Loading