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.
@@ -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 [("所有文件", "*.*")]
@@ -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)
@@ -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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pyreuser3 = pyreuser3.cli:main
3
+ pyreuser3-web = pyreuser3.web.server:main