PyREUser3 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyreuser3/__init__.py +65 -0
- pyreuser3/__main__.py +7 -0
- pyreuser3/api.py +410 -0
- pyreuser3/cli.py +207 -0
- pyreuser3/core.py +358 -0
- pyreuser3/export/__init__.py +11 -0
- pyreuser3/export/base.py +245 -0
- pyreuser3/export/enums.py +171 -0
- pyreuser3/export/fields.py +231 -0
- pyreuser3/export/metadata.py +304 -0
- pyreuser3/export/postprocess.py +346 -0
- pyreuser3/export/tree.py +289 -0
- pyreuser3/export/user3.py +415 -0
- pyreuser3/pack/__init__.py +10 -0
- pyreuser3/pack/base.py +281 -0
- pyreuser3/pack/models.py +140 -0
- pyreuser3/pack/plan.py +566 -0
- pyreuser3/pack/writer.py +313 -0
- pyreuser3/rich_ui.py +126 -0
- pyreuser3/schema.py +193 -0
- pyreuser3/web/__init__.py +6 -0
- pyreuser3/web/__main__.py +6 -0
- pyreuser3/web/handler.py +178 -0
- pyreuser3/web/jobs.py +243 -0
- pyreuser3/web/page.py +221 -0
- pyreuser3/web/picker.py +92 -0
- pyreuser3/web/runners.py +238 -0
- pyreuser3/web/server.py +104 -0
- pyreuser3/web/settings.py +42 -0
- pyreuser3-0.1.0.dist-info/METADATA +165 -0
- pyreuser3-0.1.0.dist-info/RECORD +35 -0
- pyreuser3-0.1.0.dist-info/WHEEL +5 -0
- pyreuser3-0.1.0.dist-info/entry_points.txt +3 -0
- pyreuser3-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyreuser3-0.1.0.dist-info/top_level.txt +1 -0
pyreuser3/web/picker.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""本地路径选择对话框。
|
|
2
|
+
|
|
3
|
+
借助标准库 tkinter,在本机弹出原生的文件/目录选择框,把用户选中的绝对路径
|
|
4
|
+
回传给前端表单。GUI 相关导入均延迟到调用时进行,避免无界面环境仅启动服务时
|
|
5
|
+
就尝试初始化 tkinter。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# 串行化选择框,避免多个浏览器请求同时弹出对话框造成混乱。
|
|
15
|
+
_PICKER_LOCK = threading.Lock()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def pick_path(payload: dict[str, Any]) -> dict[str, str]:
|
|
19
|
+
"""根据前端请求打开文件或目录选择对话框。
|
|
20
|
+
|
|
21
|
+
参数:
|
|
22
|
+
payload (dict[str, Any]): 前端参数,含 ``kind``(``file`` 或 ``directory``)、
|
|
23
|
+
``title``(对话框标题)和可选的 ``filetypes``(文件过滤器)。
|
|
24
|
+
|
|
25
|
+
返回:
|
|
26
|
+
dict[str, str]: 形如 ``{"path": "..."}``;用户取消选择时 ``path`` 为空字符串。
|
|
27
|
+
|
|
28
|
+
异常:
|
|
29
|
+
ValueError: 当 ``kind`` 既不是 ``file`` 也不是 ``directory`` 时抛出。
|
|
30
|
+
"""
|
|
31
|
+
# tkinter 是 Python 标准库,适合在本地工具里弹出原生选择框。
|
|
32
|
+
# 这里延迟导入,避免无界面环境仅启动服务时就尝试初始化 GUI。
|
|
33
|
+
import tkinter as tk
|
|
34
|
+
from tkinter import filedialog
|
|
35
|
+
|
|
36
|
+
kind = str(payload.get("kind", "file")).strip().lower()
|
|
37
|
+
title = str(payload.get("title", "请选择路径")).strip() or "请选择路径"
|
|
38
|
+
filetypes = _normalize_filetypes(payload.get("filetypes"))
|
|
39
|
+
|
|
40
|
+
with _PICKER_LOCK:
|
|
41
|
+
# 多个浏览器请求同时打开文件框会非常混乱,因此用锁串行化。
|
|
42
|
+
root = tk.Tk()
|
|
43
|
+
root.withdraw()
|
|
44
|
+
try:
|
|
45
|
+
# 尽量把对话框放到最前面,避免用户以为网页没有响应。
|
|
46
|
+
root.attributes("-topmost", True)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
try:
|
|
50
|
+
if kind == "directory":
|
|
51
|
+
selected = filedialog.askdirectory(
|
|
52
|
+
parent=root,
|
|
53
|
+
title=title,
|
|
54
|
+
mustexist=False,
|
|
55
|
+
)
|
|
56
|
+
elif kind == "file":
|
|
57
|
+
selected = filedialog.askopenfilename(
|
|
58
|
+
parent=root,
|
|
59
|
+
title=title,
|
|
60
|
+
filetypes=filetypes,
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError("路径选择类型必须是 file 或 directory")
|
|
64
|
+
finally:
|
|
65
|
+
# 无论是否选择成功,都销毁临时根窗口,释放 GUI 资源。
|
|
66
|
+
root.destroy()
|
|
67
|
+
|
|
68
|
+
# 用户取消选择时返回空字符串,前端保持原字段不变。
|
|
69
|
+
return {"path": str(Path(selected)) if selected else ""}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _normalize_filetypes(raw: Any) -> list[tuple[str, str]]:
|
|
73
|
+
"""把前端传来的文件过滤器整理成 tkinter 接受的格式。
|
|
74
|
+
|
|
75
|
+
参数:
|
|
76
|
+
raw (Any): 前端传入的过滤器,期望是 ``[[标签, 通配符], ...]`` 形状的列表。
|
|
77
|
+
|
|
78
|
+
返回:
|
|
79
|
+
list[tuple[str, str]]: ``(标签, 通配符)`` 元组列表;输入非法时退回 ``[("所有文件", "*.*")]``。
|
|
80
|
+
"""
|
|
81
|
+
if not isinstance(raw, list):
|
|
82
|
+
return [("所有文件", "*.*")]
|
|
83
|
+
|
|
84
|
+
out: list[tuple[str, str]] = []
|
|
85
|
+
for item in raw:
|
|
86
|
+
if not isinstance(item, (list, tuple)) or len(item) != 2:
|
|
87
|
+
continue
|
|
88
|
+
label = str(item[0]).strip()
|
|
89
|
+
pattern = str(item[1]).strip()
|
|
90
|
+
if label and pattern:
|
|
91
|
+
out.append((label, pattern))
|
|
92
|
+
return out or [("所有文件", "*.*")]
|
pyreuser3/web/runners.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Web 表单任务到核心转换器的桥接逻辑。
|
|
2
|
+
|
|
3
|
+
负责把浏览器提交的字符串参数解析、校验并转换成 :class:`User3Exporter` 的构造
|
|
4
|
+
参数,再调用其批量导出。所有路径都要求用户通过选择按钮提供绝对路径,避免在
|
|
5
|
+
不同工作目录下产生歧义。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
from ..core import RSZ_MAGIC, USR_MAGIC
|
|
15
|
+
|
|
16
|
+
# 日志回调函数签名:接收一行文本并写回任务日志。
|
|
17
|
+
LogFn = Callable[[str], None]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConversionRunners:
|
|
21
|
+
"""把浏览器提交的参数转换为 `User3Exporter` 调用。"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, root_dir: str | Path) -> None:
|
|
24
|
+
"""保存 Web 服务配置中的根目录。
|
|
25
|
+
|
|
26
|
+
参数:
|
|
27
|
+
root_dir (str | Path): 服务根目录(兼容用途),会被展开并解析为绝对路径。
|
|
28
|
+
|
|
29
|
+
返回:
|
|
30
|
+
None: 构造函数,仅初始化实例属性。
|
|
31
|
+
"""
|
|
32
|
+
# 这里保留 root_dir 仅用于服务配置兼容。实际 Web 表单路径要求
|
|
33
|
+
# 用户通过选择按钮提供绝对路径,不再自动拼接项目根目录。
|
|
34
|
+
self.root_dir = Path(root_dir).expanduser().resolve()
|
|
35
|
+
|
|
36
|
+
def run_export(self, payload: dict[str, Any], log: LogFn) -> dict[str, Any]:
|
|
37
|
+
"""执行 Web 导出任务,只处理 `.user.3` 文件。
|
|
38
|
+
|
|
39
|
+
参数:
|
|
40
|
+
payload (dict[str, Any]): 浏览器提交的导出表单参数。
|
|
41
|
+
log (LogFn): 日志回调,用于把阶段信息写回任务。
|
|
42
|
+
|
|
43
|
+
返回:
|
|
44
|
+
dict[str, Any]: 含 ``user3``(导出统计)和 ``outputDir``(输出目录)的结果字典。
|
|
45
|
+
|
|
46
|
+
异常:
|
|
47
|
+
ValueError: 当必填参数缺失或路径不是绝对路径时抛出。
|
|
48
|
+
FileNotFoundError: 当输入路径、模板或 dump 文件不存在时抛出。
|
|
49
|
+
"""
|
|
50
|
+
# 先做参数解析和路径校验,保证常见输入错误能尽早以清晰文本返回。
|
|
51
|
+
input_dir = self._path_value(payload, "inputDir", "输入目录")
|
|
52
|
+
schema_path = self._path_value(payload, "schemaPath", "RE_RSZ 模板")
|
|
53
|
+
output_dir = self._path_value(payload, "outputDir", "JSON 输出目录")
|
|
54
|
+
il2cpp_dump_path = self._path_value(
|
|
55
|
+
payload,
|
|
56
|
+
"il2cppDumpPath",
|
|
57
|
+
"il2cpp_dump.json",
|
|
58
|
+
)
|
|
59
|
+
exclude_regexes = self._exclude_regexes(payload)
|
|
60
|
+
tree_depth = self._tree_depth(payload)
|
|
61
|
+
user_magic = self._magic(payload, "userMagic", USR_MAGIC)
|
|
62
|
+
rsz_magic = self._magic(payload, "rszMagic", RSZ_MAGIC)
|
|
63
|
+
|
|
64
|
+
# 输入文件或目录必须已存在;输出目录由底层导出器按需创建。
|
|
65
|
+
self._ensure_existing_path(input_dir, "输入目录或文件")
|
|
66
|
+
self._ensure_existing_file(schema_path, "RE_RSZ 模板")
|
|
67
|
+
self._ensure_existing_file(il2cpp_dump_path, "il2cpp_dump.json")
|
|
68
|
+
|
|
69
|
+
log(f"输入:{input_dir}")
|
|
70
|
+
log(f"模板:{schema_path}")
|
|
71
|
+
log(f"输出:{output_dir}")
|
|
72
|
+
if exclude_regexes:
|
|
73
|
+
log(f"排除规则:{len(exclude_regexes)} 条")
|
|
74
|
+
|
|
75
|
+
# 延迟导入核心导出器,避免仅启动 Web 服务或查看 --help 时加载 Rich。
|
|
76
|
+
from ..export import User3Exporter
|
|
77
|
+
|
|
78
|
+
log("开始导出 .user.3 文件。")
|
|
79
|
+
exporter = User3Exporter(
|
|
80
|
+
user3_root=input_dir,
|
|
81
|
+
schema_dir=schema_path,
|
|
82
|
+
output_root=output_dir,
|
|
83
|
+
tree_depth=tree_depth,
|
|
84
|
+
exclude_regexes=exclude_regexes,
|
|
85
|
+
il2cpp_dump_path=il2cpp_dump_path,
|
|
86
|
+
user_magic=user_magic,
|
|
87
|
+
rsz_magic=rsz_magic,
|
|
88
|
+
)
|
|
89
|
+
user3_result = exporter.run()
|
|
90
|
+
log(f".user.3 导出完成:{json.dumps(user3_result, ensure_ascii=False)}")
|
|
91
|
+
|
|
92
|
+
# 结果结构保持简单,前端会自动汇总其中的 total/success/failed。
|
|
93
|
+
return {"user3": user3_result, "outputDir": str(output_dir)}
|
|
94
|
+
|
|
95
|
+
def _path_value(self, payload: dict[str, Any], key: str, label: str) -> Path:
|
|
96
|
+
"""读取必填路径,并要求路径来自用户选择的绝对路径。
|
|
97
|
+
|
|
98
|
+
参数:
|
|
99
|
+
payload (dict[str, Any]): 表单参数。
|
|
100
|
+
key (str): 参数键名。
|
|
101
|
+
label (str): 用于错误信息的人类可读名称。
|
|
102
|
+
|
|
103
|
+
返回:
|
|
104
|
+
Path: 展开用户目录后的绝对路径。
|
|
105
|
+
|
|
106
|
+
异常:
|
|
107
|
+
ValueError: 当参数缺失,或路径不是绝对路径时抛出。
|
|
108
|
+
"""
|
|
109
|
+
path = Path(self._text_value(payload, key, label)).expanduser()
|
|
110
|
+
if not path.is_absolute():
|
|
111
|
+
raise ValueError(f"{label}必须通过选择按钮提供绝对路径")
|
|
112
|
+
return path
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _text_value(payload: dict[str, Any], key: str, label: str) -> str:
|
|
116
|
+
"""读取必填文本参数,并去掉常见的外层双引号。
|
|
117
|
+
|
|
118
|
+
参数:
|
|
119
|
+
payload (dict[str, Any]): 表单参数。
|
|
120
|
+
key (str): 参数键名。
|
|
121
|
+
label (str): 用于错误信息的人类可读名称。
|
|
122
|
+
|
|
123
|
+
返回:
|
|
124
|
+
str: 去除空白与外层引号后的非空文本。
|
|
125
|
+
|
|
126
|
+
异常:
|
|
127
|
+
ValueError: 当参数缺失或为空时抛出。
|
|
128
|
+
"""
|
|
129
|
+
value = payload.get(key)
|
|
130
|
+
if value is None:
|
|
131
|
+
raise ValueError(f"缺少参数:{label}")
|
|
132
|
+
text = str(value).strip().strip('"')
|
|
133
|
+
if not text:
|
|
134
|
+
raise ValueError(f"缺少参数:{label}")
|
|
135
|
+
return text
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _optional_text(payload: dict[str, Any], key: str) -> str:
|
|
139
|
+
"""读取可选文本参数,缺失或空值时返回空字符串。
|
|
140
|
+
|
|
141
|
+
参数:
|
|
142
|
+
payload (dict[str, Any]): 表单参数。
|
|
143
|
+
key (str): 参数键名。
|
|
144
|
+
|
|
145
|
+
返回:
|
|
146
|
+
str: 去除空白与外层引号后的文本;缺失时返回空字符串。
|
|
147
|
+
"""
|
|
148
|
+
value = payload.get(key)
|
|
149
|
+
if value is None:
|
|
150
|
+
return ""
|
|
151
|
+
return str(value).strip().strip('"')
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _ensure_existing_path(path: Path, label: str) -> None:
|
|
155
|
+
"""校验输入路径存在,可以是文件也可以是目录。
|
|
156
|
+
|
|
157
|
+
参数:
|
|
158
|
+
path (Path): 待校验的路径。
|
|
159
|
+
label (str): 用于错误信息的人类可读名称。
|
|
160
|
+
|
|
161
|
+
返回:
|
|
162
|
+
None: 仅做校验;不存在时抛出异常。
|
|
163
|
+
|
|
164
|
+
异常:
|
|
165
|
+
FileNotFoundError: 当路径不存在时抛出。
|
|
166
|
+
"""
|
|
167
|
+
if not path.exists():
|
|
168
|
+
raise FileNotFoundError(f"{label}不存在:{path}")
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _ensure_existing_file(path: Path, label: str) -> None:
|
|
172
|
+
"""校验输入路径存在且必须是文件。
|
|
173
|
+
|
|
174
|
+
参数:
|
|
175
|
+
path (Path): 待校验的路径。
|
|
176
|
+
label (str): 用于错误信息的人类可读名称。
|
|
177
|
+
|
|
178
|
+
返回:
|
|
179
|
+
None: 仅做校验;不是文件时抛出异常。
|
|
180
|
+
|
|
181
|
+
异常:
|
|
182
|
+
FileNotFoundError: 当路径不存在或不是文件时抛出。
|
|
183
|
+
"""
|
|
184
|
+
if not path.is_file():
|
|
185
|
+
raise FileNotFoundError(f"{label}不存在或不是文件:{path}")
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _exclude_regexes(payload: dict[str, Any]) -> list[str]:
|
|
189
|
+
"""解析排除正则,支持文本域逐行填写或 JSON 数组。
|
|
190
|
+
|
|
191
|
+
参数:
|
|
192
|
+
payload (dict[str, Any]): 表单参数,``excludeRegexes`` 可为列表或多行文本。
|
|
193
|
+
|
|
194
|
+
返回:
|
|
195
|
+
list[str]: 去除空白、过滤空行后的正则字符串列表。
|
|
196
|
+
"""
|
|
197
|
+
raw = payload.get("excludeRegexes", "")
|
|
198
|
+
if isinstance(raw, list):
|
|
199
|
+
return [str(item).strip() for item in raw if str(item).strip()]
|
|
200
|
+
return [line.strip() for line in str(raw).splitlines() if line.strip()]
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _tree_depth(payload: dict[str, Any]) -> int | str:
|
|
204
|
+
"""解析导出树深度,支持 `auto`、十进制和 `0x` 十六进制整数。
|
|
205
|
+
|
|
206
|
+
参数:
|
|
207
|
+
payload (dict[str, Any]): 表单参数,``treeDepth`` 为文本。
|
|
208
|
+
|
|
209
|
+
返回:
|
|
210
|
+
int | str: 非负整数,或字符串 ``"auto"``。
|
|
211
|
+
|
|
212
|
+
异常:
|
|
213
|
+
ValueError: 当解析出的整数为负时抛出。
|
|
214
|
+
"""
|
|
215
|
+
raw = ConversionRunners._optional_text(payload, "treeDepth")
|
|
216
|
+
if not raw or raw.lower() == "auto":
|
|
217
|
+
return "auto"
|
|
218
|
+
value = int(raw, 0)
|
|
219
|
+
if value < 0:
|
|
220
|
+
raise ValueError("tree-depth 必须为非负整数或 auto")
|
|
221
|
+
return value
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _magic(payload: dict[str, Any], key: str, default: int) -> int:
|
|
225
|
+
"""解析 magic 参数,未填写时使用核心库默认值。
|
|
226
|
+
|
|
227
|
+
参数:
|
|
228
|
+
payload (dict[str, Any]): 表单参数。
|
|
229
|
+
key (str): 参数键名(如 ``"userMagic"``)。
|
|
230
|
+
default (int): 未填写时使用的默认 magic。
|
|
231
|
+
|
|
232
|
+
返回:
|
|
233
|
+
int: 解析出的 magic 整数(支持 ``0x`` 前缀)。
|
|
234
|
+
"""
|
|
235
|
+
raw = ConversionRunners._optional_text(payload, key)
|
|
236
|
+
if not raw:
|
|
237
|
+
return default
|
|
238
|
+
return int(raw, 0)
|
pyreuser3/web/server.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""本地 Vue Web UI 的服务启动入口。
|
|
2
|
+
|
|
3
|
+
负责解析命令行参数、装配任务仓库与转换桥接器、构造请求处理类,并启动一个
|
|
4
|
+
多线程 HTTP 服务阻塞运行,直到用户按 Ctrl+C 中断。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
from http.server import ThreadingHTTPServer
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Sequence
|
|
13
|
+
|
|
14
|
+
from .handler import make_handler
|
|
15
|
+
from .jobs import JobStore
|
|
16
|
+
from .runners import ConversionRunners
|
|
17
|
+
from .settings import WebSettings
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReusableThreadingHTTPServer(ThreadingHTTPServer):
|
|
21
|
+
"""允许复用刚释放端口的多线程 HTTP 服务。
|
|
22
|
+
|
|
23
|
+
通过设置 ``allow_reuse_address``,避免重启服务时因端口处于 TIME_WAIT 而报
|
|
24
|
+
“地址已被占用”。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
allow_reuse_address = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_server(settings: WebSettings) -> None:
|
|
31
|
+
"""按给定配置启动本地 Web 服务,并阻塞直到用户中断。
|
|
32
|
+
|
|
33
|
+
参数:
|
|
34
|
+
settings (WebSettings): 服务运行配置(主机、端口、根目录、任务上限)。
|
|
35
|
+
|
|
36
|
+
返回:
|
|
37
|
+
None: 阻塞运行;收到 ``KeyboardInterrupt`` 后清理并返回。
|
|
38
|
+
"""
|
|
39
|
+
# 统一在服务启动前解析配置路径;网页表单仍要求用户选择绝对路径。
|
|
40
|
+
settings = settings.with_resolved_root()
|
|
41
|
+
|
|
42
|
+
# 三个对象分别负责状态、业务和路由;这样 HTTP 层不会掺入转换细节。
|
|
43
|
+
jobs = JobStore(max_jobs=settings.max_jobs)
|
|
44
|
+
runners = ConversionRunners(settings.root_dir)
|
|
45
|
+
handler = make_handler(settings, jobs, runners)
|
|
46
|
+
|
|
47
|
+
server = ReusableThreadingHTTPServer((settings.host, settings.port), handler)
|
|
48
|
+
url = f"http://{settings.host}:{settings.port}/"
|
|
49
|
+
print(f"RE User3 JSON Web 正在运行:{url}")
|
|
50
|
+
print("网页路径不会自动使用项目根目录,请在页面中手动选择。")
|
|
51
|
+
print("按 Ctrl+C 停止服务。")
|
|
52
|
+
try:
|
|
53
|
+
server.serve_forever()
|
|
54
|
+
except KeyboardInterrupt:
|
|
55
|
+
print("\n正在停止服务...")
|
|
56
|
+
finally:
|
|
57
|
+
# 无论正常退出还是异常中断,都关闭服务释放端口。
|
|
58
|
+
server.server_close()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
62
|
+
"""解析 Web 服务命令行参数。
|
|
63
|
+
|
|
64
|
+
参数:
|
|
65
|
+
argv (Sequence[str] | None): 命令行参数序列;为 ``None`` 时使用 ``sys.argv``。
|
|
66
|
+
|
|
67
|
+
返回:
|
|
68
|
+
argparse.Namespace: 含 ``host``、``port``、``root_dir``、``max_jobs`` 的解析结果。
|
|
69
|
+
"""
|
|
70
|
+
parser = argparse.ArgumentParser(description="启动本地 Vue Web UI。")
|
|
71
|
+
parser.add_argument("--host", default="127.0.0.1", help="监听主机。")
|
|
72
|
+
parser.add_argument("--port", type=int, default=8765, help="监听端口。")
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--root-dir",
|
|
75
|
+
default=str(Path.cwd()),
|
|
76
|
+
help="兼容配置;网页路径仍需通过选择按钮提供绝对路径。",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--max-jobs",
|
|
80
|
+
type=int,
|
|
81
|
+
default=50,
|
|
82
|
+
help="内存中保留的最大任务数量。",
|
|
83
|
+
)
|
|
84
|
+
return parser.parse_args(argv)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
88
|
+
"""供 `pyreuser3-web` 和 `python -m pyreuser3.web` 调用的入口。
|
|
89
|
+
|
|
90
|
+
参数:
|
|
91
|
+
argv (Sequence[str] | None): 命令行参数序列;为 ``None`` 时使用 ``sys.argv``。
|
|
92
|
+
|
|
93
|
+
返回:
|
|
94
|
+
None: 解析参数后调用 :func:`run_server` 阻塞运行。
|
|
95
|
+
"""
|
|
96
|
+
args = parse_args(argv)
|
|
97
|
+
run_server(
|
|
98
|
+
WebSettings(
|
|
99
|
+
host=args.host,
|
|
100
|
+
port=args.port,
|
|
101
|
+
root_dir=Path(args.root_dir),
|
|
102
|
+
max_jobs=args.max_jobs,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""本地 Web UI 的运行配置。
|
|
2
|
+
|
|
3
|
+
集中保存 HTTP 服务和转换任务共享的少量配置项,使用 frozen dataclass 避免运行
|
|
4
|
+
过程中被意外修改。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class WebSettings:
|
|
15
|
+
"""HTTP 服务和转换任务共享的配置。
|
|
16
|
+
|
|
17
|
+
属性:
|
|
18
|
+
host (str): 监听主机地址,默认仅本机回环 ``127.0.0.1``。
|
|
19
|
+
port (int): 监听端口,默认 ``8765``。
|
|
20
|
+
root_dir (Path): 服务根目录(兼容用途),默认当前工作目录。
|
|
21
|
+
max_jobs (int): 内存中保留的最大任务数量,默认 ``50``。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
host: str = "127.0.0.1"
|
|
25
|
+
port: int = 8765
|
|
26
|
+
root_dir: Path = Path.cwd()
|
|
27
|
+
max_jobs: int = 50
|
|
28
|
+
|
|
29
|
+
def with_resolved_root(self) -> "WebSettings":
|
|
30
|
+
"""返回根目录已解析为绝对路径的新配置对象。
|
|
31
|
+
|
|
32
|
+
返回:
|
|
33
|
+
WebSettings: 一个 ``root_dir`` 展开用户目录并解析为绝对路径的新实例,
|
|
34
|
+
其余字段保持不变。
|
|
35
|
+
"""
|
|
36
|
+
# dataclass 设置为 frozen,使用新对象可以避免运行中误改配置。
|
|
37
|
+
return WebSettings(
|
|
38
|
+
host=self.host,
|
|
39
|
+
port=self.port,
|
|
40
|
+
root_dir=self.root_dir.expanduser().resolve(),
|
|
41
|
+
max_jobs=self.max_jobs,
|
|
42
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyREUser3
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure Python tools for converting RE Engine .user.3 files to and from JSON.
|
|
5
|
+
Author: Egg Targaryen
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dzxrly/PyREUser3
|
|
8
|
+
Project-URL: Repository, https://github.com/dzxrly/PyREUser3
|
|
9
|
+
Project-URL: Issues, https://github.com/dzxrly/PyREUser3/issues
|
|
10
|
+
Keywords: RE Engine,user.3,JSON,modding,RSZ
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: File Formats
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: rich>=13.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# PyREUser3
|
|
35
|
+
|
|
36
|
+
English | [简体中文](./docs/README.zh-CN.md)
|
|
37
|
+
|
|
38
|
+
PyREUser3 is a pure Python package for converting RE Engine `.user.3` database files to JSON and packing compatible JSON back to `.user.3`.
|
|
39
|
+
|
|
40
|
+
Install it with:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install pyreuser3
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Import it with the same normalized package name:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from pyreuser3 import REUser3Converter
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What Is Included
|
|
53
|
+
|
|
54
|
+
- `.user.3 -> JSON` export.
|
|
55
|
+
- `JSON -> .user.3` packing.
|
|
56
|
+
- A reusable Python API through `REUser3Converter`.
|
|
57
|
+
- CLI commands through `pyreuser3`.
|
|
58
|
+
- A local `.user.3` export Web UI through `pyreuser3-web`.
|
|
59
|
+
|
|
60
|
+
This PyPI package intentionally does not include game resources, dumped game data, RE_RSZ templates, `il2cpp_dump.json`, `.msg.23` conversion tools, or repository-specific helper scripts.
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
- Python 3.9 or newer.
|
|
65
|
+
- A RE_RSZ schema JSON file for the target game/version.
|
|
66
|
+
- An `il2cpp_dump.json` file when exporting readable enum labels.
|
|
67
|
+
- One or more unpacked `.user.3` files.
|
|
68
|
+
|
|
69
|
+
## Command Line
|
|
70
|
+
|
|
71
|
+
Export `.user.3` files to JSON:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pyreuser3 export \
|
|
75
|
+
-i <input-user3-file-or-directory> \
|
|
76
|
+
-s <RE_RSZ-schema.json> \
|
|
77
|
+
-o <json-output-directory> \
|
|
78
|
+
-p <il2cpp_dump.json>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Pack JSON back to `.user.3`:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pyreuser3 pack \
|
|
85
|
+
-j <input-json-file-or-directory> \
|
|
86
|
+
-s <RE_RSZ-schema.json> \
|
|
87
|
+
-o <user3-output-directory> \
|
|
88
|
+
-p <il2cpp_dump.json>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The `-p/--il2cpp-dump-path` option is required for export and optional for pack. Passing it during pack is recommended when enum names need to be resolved back to numeric values.
|
|
92
|
+
|
|
93
|
+
Start the local `.user.3` export Web UI:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pyreuser3-web --port 8765
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The Web UI only handles `.user.3` export. It does not pack files and does not provide `.msg.23` conversion.
|
|
100
|
+
|
|
101
|
+
## Python API
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from pyreuser3 import REUser3Converter
|
|
105
|
+
|
|
106
|
+
converter = REUser3Converter(
|
|
107
|
+
schema_path="D:/schema/rsz_game.json",
|
|
108
|
+
il2cpp_dump_path="D:/game/il2cpp_dump.json",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
converter.export_file(
|
|
112
|
+
"input/OtomonData.user.3",
|
|
113
|
+
"json/OtomonData.user.3.json",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
converter.pack_file(
|
|
117
|
+
"json/OtomonData.user.3.json",
|
|
118
|
+
"mod/OtomonData.user.3",
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For stable patch-and-repack workflows, use `patch_file()` or `parse_pack_file()`:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from pyreuser3 import REUser3Converter
|
|
126
|
+
|
|
127
|
+
converter = REUser3Converter(
|
|
128
|
+
schema_path="D:/schema/rsz_game.json",
|
|
129
|
+
il2cpp_dump_path="D:/game/il2cpp_dump.json",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def patch(data, source_path):
|
|
133
|
+
# Modify the full instance-table JSON in place.
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
converter.patch_file(
|
|
137
|
+
"input/example.user.3",
|
|
138
|
+
"output/example.user.3",
|
|
139
|
+
patch,
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Build From Source
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
python -m pip install -U build twine
|
|
147
|
+
python -m build
|
|
148
|
+
python -m twine check dist/*
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Upload to TestPyPI first:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
python -m twine upload -r testpypi dist/*
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Then upload the same checked distribution files to PyPI:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
python -m twine upload dist/*
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT License.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
pyreuser3/__init__.py,sha256=nPCDqYLXURMuurc-eQnX6zAgjX4kTc5MsYqj948zS20,1908
|
|
2
|
+
pyreuser3/__main__.py,sha256=KLe7k-BNJD5LbC0oJYFKU1XmWI2Dn4aiSBPc_KM2V04,143
|
|
3
|
+
pyreuser3/api.py,sha256=65rAdewtWdyMapZCnXl0P8p4P_fni37BT0KH2C85DBs,16660
|
|
4
|
+
pyreuser3/cli.py,sha256=kDU-f0ytQurN3FhKKnbyQWh_CMpktgB6jB_ebQSFUDE,6078
|
|
5
|
+
pyreuser3/core.py,sha256=Q4u5K0SFzanKmIjgh_oIO1qC1_-y1rU8gbpbZer4_Jw,11506
|
|
6
|
+
pyreuser3/rich_ui.py,sha256=MZ3QaJEszwX2bG7qgr9esdnH25o_pjZC1QN4HJzqNQI,4439
|
|
7
|
+
pyreuser3/schema.py,sha256=WfNbs8iOj63Ep9nrr-g-Rq7OYx2W2V7NuhcEhM9sZb8,6824
|
|
8
|
+
pyreuser3/export/__init__.py,sha256=irRhQ3b_RLhR6YHahds6Yrcc1axbtuM-VicZ2qD0Ut0,350
|
|
9
|
+
pyreuser3/export/base.py,sha256=f-fslgQ5kTcA2ugFrfw2XraAB1p9ZLoMXvchxqAG0ao,10635
|
|
10
|
+
pyreuser3/export/enums.py,sha256=OD5_DrV2vBQ6MykxABliaspVlo_JN0cXH3hQQy-VKK4,7877
|
|
11
|
+
pyreuser3/export/fields.py,sha256=THt5id1KAPo4CcNNpfuB_JXz1P-2DlE_uSQwZbEe8MU,8994
|
|
12
|
+
pyreuser3/export/metadata.py,sha256=jNsfOQFlSbtwvX3kjRtmNg1VeLO4vrKLaU2NfASBNNE,12499
|
|
13
|
+
pyreuser3/export/postprocess.py,sha256=d1XbgIPfbaqFqQLJbHDnle7-V9g-siR2IpHjPUtGPjE,14272
|
|
14
|
+
pyreuser3/export/tree.py,sha256=SB5d1d4t7to7IE4_PZrQ9UThw8jIZx3RwpghZ5BPGp0,11433
|
|
15
|
+
pyreuser3/export/user3.py,sha256=dReHWs207Q0foZjwS5t7vtSa3GOWHohTnLDEgs0dNkA,18111
|
|
16
|
+
pyreuser3/pack/__init__.py,sha256=CvSdmaMocBwtQtgYcMK82-_bcZ81ELRf7CEv0n5hDrY,317
|
|
17
|
+
pyreuser3/pack/base.py,sha256=Twtl1GPVjJhOt86KxbxN0KPJF19yEkPNVlenLKBichw,11611
|
|
18
|
+
pyreuser3/pack/models.py,sha256=yOdbgDnyq1DwjGbJKplrbHgjm3iccPY86NHm0QvxH3s,4112
|
|
19
|
+
pyreuser3/pack/plan.py,sha256=Op-Og8ACElf1uOuHO_BWe3J3WdNOE8r4EmcCgIAWZkI,23942
|
|
20
|
+
pyreuser3/pack/writer.py,sha256=bcJl_YBWeOZRKZEFGneyHuxk2FvjFN6olZWmSNpULck,12459
|
|
21
|
+
pyreuser3/web/__init__.py,sha256=mumGq3UPLyNza4loQWsakpeTEY-sifR5Ba4toVnVcWw,174
|
|
22
|
+
pyreuser3/web/__main__.py,sha256=RDcl2BIW9P02loP6VcTzItkNJ0kOG33oXGlUSW8zBjE,116
|
|
23
|
+
pyreuser3/web/handler.py,sha256=m-57tjBqgbNi7U_Q_MqVVKtz4OkNLyOLl38o2We2B_Q,6740
|
|
24
|
+
pyreuser3/web/jobs.py,sha256=dFHE2leKz9CU3YslZ0p6sMFyCAisbSJmV6oynUFE8Gs,8740
|
|
25
|
+
pyreuser3/web/page.py,sha256=jA4jMsfXgsOWBW-4wPMUOmZ8cGNb1vig21-jfY7AtZI,14768
|
|
26
|
+
pyreuser3/web/picker.py,sha256=a1DpPYGkW-tj64gmpEjhcgL4FMmnAKK3GXddxeN4_S4,3503
|
|
27
|
+
pyreuser3/web/runners.py,sha256=iqMPIW0Vd1S5qAjhNQTGTXhsM46xG7tORsYWaiUjV3w,9101
|
|
28
|
+
pyreuser3/web/server.py,sha256=kV03lNBVaiB2Kk0HsMKnSl8a9p3_nwWrwbH-GAGaNEA,3613
|
|
29
|
+
pyreuser3/web/settings.py,sha256=MaqnpVHceRPcNScezOuS80Gj8r-sPpD9vejDlatXLQ8,1374
|
|
30
|
+
pyreuser3-0.1.0.dist-info/licenses/LICENSE,sha256=PVS778CntbrrOux88NQbIpy_Cbz_RlP60iTWr6tYTv0,1071
|
|
31
|
+
pyreuser3-0.1.0.dist-info/METADATA,sha256=4dREWAAXVh_5NGHQ17d-hHvWeF58rkWLac24FRfTCa0,4219
|
|
32
|
+
pyreuser3-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
33
|
+
pyreuser3-0.1.0.dist-info/entry_points.txt,sha256=pvkDteasOu6uISfgi5gX6lJ3_QNvdaCinq1VXcE20Fc,91
|
|
34
|
+
pyreuser3-0.1.0.dist-info/top_level.txt,sha256=_M8Iycm1EShJrz8TyY8VBDqvCDWm8hsgLc5tbyIoe7A,10
|
|
35
|
+
pyreuser3-0.1.0.dist-info/RECORD,,
|