kotonebot 0.5.0__py3-none-any.whl → 0.7.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.
- kotonebot/__init__.py +39 -39
- kotonebot/backend/bot.py +312 -312
- kotonebot/backend/color.py +525 -525
- kotonebot/backend/context/__init__.py +3 -3
- kotonebot/backend/context/context.py +1002 -1002
- kotonebot/backend/context/task_action.py +183 -183
- kotonebot/backend/core.py +86 -129
- kotonebot/backend/debug/entry.py +89 -89
- kotonebot/backend/debug/mock.py +78 -78
- kotonebot/backend/debug/server.py +222 -222
- kotonebot/backend/debug/vars.py +351 -351
- kotonebot/backend/dispatch.py +227 -227
- kotonebot/backend/flow_controller.py +196 -196
- kotonebot/backend/image.py +36 -5
- kotonebot/backend/loop.py +222 -208
- kotonebot/backend/ocr.py +535 -535
- kotonebot/backend/preprocessor.py +103 -103
- kotonebot/client/__init__.py +9 -9
- kotonebot/client/device.py +369 -529
- kotonebot/client/fast_screenshot.py +377 -377
- kotonebot/client/host/__init__.py +43 -43
- kotonebot/client/host/adb_common.py +101 -107
- kotonebot/client/host/custom.py +118 -118
- kotonebot/client/host/leidian_host.py +196 -196
- kotonebot/client/host/mumu12_host.py +353 -353
- kotonebot/client/host/protocol.py +214 -214
- kotonebot/client/host/windows_common.py +73 -58
- kotonebot/client/implements/__init__.py +65 -70
- kotonebot/client/implements/adb.py +89 -89
- kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
- kotonebot/client/implements/remote_windows.py +188 -188
- kotonebot/client/implements/uiautomator2.py +85 -85
- kotonebot/client/implements/windows/__init__.py +1 -0
- kotonebot/client/implements/windows/print_window.py +133 -0
- kotonebot/client/implements/windows/send_message.py +324 -0
- kotonebot/client/implements/{windows.py → windows/windows.py} +175 -176
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/client/scaler.py +467 -0
- kotonebot/config/base_config.py +103 -96
- kotonebot/config/config.py +61 -0
- kotonebot/config/manager.py +36 -36
- kotonebot/core/__init__.py +13 -0
- kotonebot/core/entities/base.py +182 -0
- kotonebot/core/entities/compound.py +75 -0
- kotonebot/core/entities/ocr.py +117 -0
- kotonebot/core/entities/template_match.py +198 -0
- kotonebot/devtools/__init__.py +42 -0
- kotonebot/devtools/cli/__init__.py +6 -0
- kotonebot/devtools/cli/main.py +53 -0
- kotonebot/{tools → devtools}/mirror.py +354 -354
- kotonebot/devtools/project/project.py +41 -0
- kotonebot/devtools/project/scanner.py +202 -0
- kotonebot/devtools/project/schema.py +99 -0
- kotonebot/devtools/resgen/__init__.py +42 -0
- kotonebot/devtools/resgen/codegen.py +331 -0
- kotonebot/devtools/resgen/core.py +94 -0
- kotonebot/devtools/resgen/parsers.py +360 -0
- kotonebot/devtools/resgen/utils.py +158 -0
- kotonebot/devtools/resgen/validation.py +115 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
- kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
- kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
- kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
- kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
- kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
- kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
- kotonebot/devtools/web/dist/index.html +25 -0
- kotonebot/devtools/web/server/__init__.py +0 -0
- kotonebot/devtools/web/server/rest_api.py +217 -0
- kotonebot/devtools/web/server/server.py +85 -0
- kotonebot/errors.py +76 -76
- kotonebot/interop/win/__init__.py +13 -9
- kotonebot/interop/win/_mouse.py +310 -310
- kotonebot/interop/win/message_box.py +313 -313
- kotonebot/interop/win/reg.py +37 -37
- kotonebot/interop/win/shake_mouse.py +224 -0
- kotonebot/interop/win/shortcut.py +43 -43
- kotonebot/interop/win/task_dialog.py +513 -513
- kotonebot/interop/win/window.py +89 -0
- kotonebot/logging/__init__.py +2 -2
- kotonebot/logging/log.py +17 -17
- kotonebot/primitives/__init__.py +19 -17
- kotonebot/primitives/geometry.py +1067 -862
- kotonebot/primitives/visual.py +143 -63
- kotonebot/ui/file_host/sensio.py +36 -36
- kotonebot/ui/file_host/tmp_send.py +54 -54
- kotonebot/ui/pushkit/__init__.py +3 -3
- kotonebot/ui/pushkit/image_host.py +88 -88
- kotonebot/ui/pushkit/protocol.py +13 -13
- kotonebot/ui/pushkit/wxpusher.py +54 -54
- kotonebot/ui/user.py +148 -148
- kotonebot/util.py +436 -436
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/METADATA +84 -82
- kotonebot-0.7.0.dist-info/RECORD +109 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/WHEEL +1 -1
- kotonebot-0.7.0.dist-info/entry_points.txt +2 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/licenses/LICENSE +673 -673
- kotonebot/client/implements/adb_raw.py +0 -163
- kotonebot-0.5.0.dist-info/RECORD +0 -71
- /kotonebot/{tools → devtools/project}/__init__.py +0 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, List, Callable
|
|
3
|
+
|
|
4
|
+
from .core import CodeWriter, ClassNode, ResourceNode, ImageAsset, BoxData, PointData, PrefabData
|
|
5
|
+
from .utils import unify_path
|
|
6
|
+
|
|
7
|
+
class StandardGenerator:
|
|
8
|
+
"""标准 Python 生成器基类"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, production: bool = False, ide_type: str | None = None,
|
|
11
|
+
path_transformer: Callable[[str], str] | None = None):
|
|
12
|
+
self.writer = CodeWriter()
|
|
13
|
+
self.production = production
|
|
14
|
+
self.ide_type = ide_type
|
|
15
|
+
self.path_transformer = path_transformer
|
|
16
|
+
|
|
17
|
+
def _transform_path(self, original_path: str, default_expr: str) -> str:
|
|
18
|
+
"""Return the code expression for the image path.
|
|
19
|
+
|
|
20
|
+
If `path_transformer` is provided, call it with the original path and
|
|
21
|
+
use its return value verbatim as the expression to emit. Otherwise
|
|
22
|
+
fall back to `default_expr`.
|
|
23
|
+
"""
|
|
24
|
+
if self.path_transformer:
|
|
25
|
+
return self.path_transformer(original_path)
|
|
26
|
+
return default_expr
|
|
27
|
+
|
|
28
|
+
def generate(self, root_nodes: List[ClassNode]) -> str:
|
|
29
|
+
self.render_header()
|
|
30
|
+
self.writer.write_empty_line()
|
|
31
|
+
for node in root_nodes:
|
|
32
|
+
self.render_class(node)
|
|
33
|
+
return self.writer.get_content()
|
|
34
|
+
|
|
35
|
+
def render_header(self):
|
|
36
|
+
"""可被重写:文件头"""
|
|
37
|
+
w = self.writer
|
|
38
|
+
if not self.production:
|
|
39
|
+
w.write("####### 图片资源文件 #######")
|
|
40
|
+
w.write("####### 此文件为自动生成,请勿编辑 #######")
|
|
41
|
+
w.write("####### AUTO GENERATED. DO NOT EDIT. #######")
|
|
42
|
+
w.write("from kotonebot.backend.core import Image, HintBox, HintPoint")
|
|
43
|
+
w.write("from kotonebot.primitives import ImageSlice, Rect")
|
|
44
|
+
|
|
45
|
+
def render_class(self, node: ClassNode):
|
|
46
|
+
"""递归渲染类"""
|
|
47
|
+
w = self.writer
|
|
48
|
+
w.write(f"class {node.name}:")
|
|
49
|
+
with w.indent():
|
|
50
|
+
if node.is_empty():
|
|
51
|
+
w.write("pass")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# 1. 渲染属性
|
|
55
|
+
for attr in node.attributes:
|
|
56
|
+
self.render_attribute(attr)
|
|
57
|
+
w.write_empty_line()
|
|
58
|
+
|
|
59
|
+
# 2. 渲染子类
|
|
60
|
+
for child in node.children:
|
|
61
|
+
self.render_class(child)
|
|
62
|
+
w.write_empty_line()
|
|
63
|
+
|
|
64
|
+
def render_attribute(self, attr: ResourceNode):
|
|
65
|
+
"""渲染单个属性。根据 attr.value 的 IR 类型生成对应的代码字符串。"""
|
|
66
|
+
val = attr.value
|
|
67
|
+
code_str = ""
|
|
68
|
+
|
|
69
|
+
if isinstance(val, ImageAsset):
|
|
70
|
+
rect_expr: str
|
|
71
|
+
if val.rect is not None:
|
|
72
|
+
x1, y1, x2, y2 = val.rect
|
|
73
|
+
width = x2 - x1
|
|
74
|
+
height = y2 - y1
|
|
75
|
+
rect_expr = f"Rect(x={x1}, y={y1}, w={width}, h={height})"
|
|
76
|
+
else:
|
|
77
|
+
rect_expr = "None"
|
|
78
|
+
# 使用相对名作为资源引用(保留原来的 sprite_path 风格)
|
|
79
|
+
rel = os.path.basename(val.path)
|
|
80
|
+
display_name = attr.metadata.get('display_name', attr.name)
|
|
81
|
+
default = f'sprite_path("{rel}")'
|
|
82
|
+
path_expr = self._transform_path(val.path, default)
|
|
83
|
+
code_str = f'ImageSlice(file_path={path_expr}, name="{display_name}", slice_rect={rect_expr})'
|
|
84
|
+
elif isinstance(val, BoxData):
|
|
85
|
+
code_str = (f'HintBox(x1={val.x1}, y1={val.y1}, x2={val.x2}, y2={val.y2}, '
|
|
86
|
+
f'source_resolution=({val.resolution[0]}, {val.resolution[1]}))')
|
|
87
|
+
elif isinstance(val, PointData):
|
|
88
|
+
code_str = f'HintPoint(x={val.x}, y={val.y})'
|
|
89
|
+
else:
|
|
90
|
+
# fallback: str 转换
|
|
91
|
+
code_str = str(val)
|
|
92
|
+
|
|
93
|
+
self.writer.write(f"{attr.name} = {code_str}")
|
|
94
|
+
if not self.production:
|
|
95
|
+
self.render_docstring(attr)
|
|
96
|
+
|
|
97
|
+
def render_docstring(self, attr: ResourceNode):
|
|
98
|
+
"""渲染 Docstring,包含图片标签生成逻辑"""
|
|
99
|
+
w = self.writer
|
|
100
|
+
base_doc = attr.docstring
|
|
101
|
+
|
|
102
|
+
# 构造 HTML 图片标签
|
|
103
|
+
img_tags = ""
|
|
104
|
+
# 1. 当前资源图片
|
|
105
|
+
if 'abs_path' in attr.metadata:
|
|
106
|
+
img_tags += self._make_img_tag(attr.metadata['abs_path'], attr.metadata.get('display_name', 'Img')) + '\\n'
|
|
107
|
+
elif 'preview_path' in attr.metadata:
|
|
108
|
+
img_tags += self._make_img_tag(attr.metadata['preview_path'], "Preview") + '\\n'
|
|
109
|
+
|
|
110
|
+
# 2. 原始大图 (可选)
|
|
111
|
+
if 'origin_file' in attr.metadata:
|
|
112
|
+
img_tags += "\nOriginal:\n" + self._make_img_tag(attr.metadata['origin_file'], "Original", height="200")
|
|
113
|
+
|
|
114
|
+
full_doc = f"{base_doc}\n\n{img_tags}"
|
|
115
|
+
|
|
116
|
+
# 写入
|
|
117
|
+
w.write('"""')
|
|
118
|
+
for line in full_doc.split('\n'):
|
|
119
|
+
w.write(line)
|
|
120
|
+
w.write('"""')
|
|
121
|
+
|
|
122
|
+
def _make_img_tag(self, path: str, title: str, height: str = "") -> str:
|
|
123
|
+
path = unify_path(path)
|
|
124
|
+
# 简单的 IDE 适配逻辑
|
|
125
|
+
if self.ide_type == 'vscode':
|
|
126
|
+
# VSCode 需要转义
|
|
127
|
+
path = path.replace('\\', '\\\\')
|
|
128
|
+
return f'<img src="vscode-file://vscode-app/{path}" title="{title}" height="{height}" />'
|
|
129
|
+
elif self.ide_type == 'pycharm':
|
|
130
|
+
return f'.. image:: http://localhost:6532/image?path={path}'
|
|
131
|
+
else:
|
|
132
|
+
return f'<img src="file:///{path}" title="{title}" height="{height}" />'
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class EntityGenerator(StandardGenerator):
|
|
136
|
+
"""
|
|
137
|
+
KotoneBot 实体代码生成器。
|
|
138
|
+
|
|
139
|
+
输出规范:
|
|
140
|
+
1. Template (图片) -> 生成继承自 TemplateMatchPrefab 的嵌套类。
|
|
141
|
+
2. HintBox/Point -> 生成类的静态属性实例。
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def render_header(self):
|
|
145
|
+
w = self.writer
|
|
146
|
+
w.write("####### 实体资源文件 #######")
|
|
147
|
+
w.write("####### 此文件为自动生成,请勿编辑 #######")
|
|
148
|
+
w.write("####### AUTO GENERATED. DO NOT EDIT. #######")
|
|
149
|
+
w.write_empty_line()
|
|
150
|
+
w.write("from kotonebot.core import TemplateMatchPrefab")
|
|
151
|
+
w.write("from kotonebot.primitives import Image, ImageSlice, Rect")
|
|
152
|
+
w.write("from kotonebot.backend.core import HintBox, HintPoint")
|
|
153
|
+
w.write_empty_line()
|
|
154
|
+
|
|
155
|
+
def render_attribute(self, attr: ResourceNode):
|
|
156
|
+
"""
|
|
157
|
+
核心分发逻辑:
|
|
158
|
+
根据 ResourceNode 携带的 value 类型,决定生成策略。
|
|
159
|
+
"""
|
|
160
|
+
data = attr.value
|
|
161
|
+
|
|
162
|
+
print(f'Writing: {attr.name} of type {type(data)}')
|
|
163
|
+
if isinstance(data, ImageAsset):
|
|
164
|
+
self._render_prefab_class(attr, data)
|
|
165
|
+
elif isinstance(data, PrefabData):
|
|
166
|
+
self._render_custom_prefab_class(attr, data)
|
|
167
|
+
elif isinstance(data, (BoxData, PointData)):
|
|
168
|
+
self._render_primitive_assignment(attr, data)
|
|
169
|
+
else:
|
|
170
|
+
# 兜底:如果 value 是未知类型或纯字符串,回退到默认赋值
|
|
171
|
+
super().render_attribute(attr)
|
|
172
|
+
|
|
173
|
+
def _render_custom_prefab_class(self, node: ResourceNode, data: PrefabData):
|
|
174
|
+
"""
|
|
175
|
+
渲染自定义基类的 Prefab 嵌套类
|
|
176
|
+
"""
|
|
177
|
+
w = self.writer
|
|
178
|
+
class_name = node.name
|
|
179
|
+
if not getattr(data, 'prefab_id', None):
|
|
180
|
+
raise ValueError(f"PrefabData missing prefab_id for node {node.name}")
|
|
181
|
+
base_class = data.prefab_id
|
|
182
|
+
|
|
183
|
+
# 1. 类定义
|
|
184
|
+
w.write(f"class {class_name}({base_class}):")
|
|
185
|
+
|
|
186
|
+
with w.indent():
|
|
187
|
+
# 2. Docstring
|
|
188
|
+
if not self.production:
|
|
189
|
+
self.render_docstring(node)
|
|
190
|
+
|
|
191
|
+
# display_name 属性(用于 Image.name 参数)
|
|
192
|
+
display_name = node.metadata.get('display_name', node.name)
|
|
193
|
+
|
|
194
|
+
# 3. If PrefabData has an image, expose it as `template` for convenience
|
|
195
|
+
# so simple prefab definitions that only provide an image still
|
|
196
|
+
# produce a usable `template` attribute on the generated class.
|
|
197
|
+
# Only expose `template` automatically for prefabs that originated
|
|
198
|
+
# from a simple meta file (isSimple == True). Complex/v2 prefabs may
|
|
199
|
+
# define images via props and should not implicitly expose `template`.
|
|
200
|
+
if data.image is not None and node.metadata.get('isSimple'):
|
|
201
|
+
rect_expr: str
|
|
202
|
+
if data.image.rect is not None:
|
|
203
|
+
x1, y1, x2, y2 = data.image.rect
|
|
204
|
+
ix1, iy1, ix2, iy2 = map(int, (x1, y1, x2, y2))
|
|
205
|
+
rect_width = ix2 - ix1
|
|
206
|
+
rect_height = iy2 - iy1
|
|
207
|
+
rect_expr = f"Rect(x={ix1}, y={iy1}, w={rect_width}, h={rect_height})"
|
|
208
|
+
else:
|
|
209
|
+
rect_expr = "None"
|
|
210
|
+
|
|
211
|
+
clean_path = unify_path(data.image.path)
|
|
212
|
+
default = f'"{clean_path}"'
|
|
213
|
+
path_expr = self._transform_path(clean_path, default)
|
|
214
|
+
w.write(f'template = ImageSlice(file_path={path_expr}, name="{display_name}", slice_rect={rect_expr})')
|
|
215
|
+
w.write_empty_line()
|
|
216
|
+
|
|
217
|
+
# 4. V2 Props
|
|
218
|
+
for key, value in data.props.items():
|
|
219
|
+
if isinstance(value, ImageAsset):
|
|
220
|
+
rect_expr: str
|
|
221
|
+
if value.rect is not None:
|
|
222
|
+
x1, y1, x2, y2 = value.rect
|
|
223
|
+
ix1, iy1, ix2, iy2 = map(int, (x1, y1, x2, y2))
|
|
224
|
+
rect_width = ix2 - ix1
|
|
225
|
+
rect_height = iy2 - iy1
|
|
226
|
+
rect_expr = f"Rect(x={ix1}, y={iy1}, w={rect_width}, h={rect_height})"
|
|
227
|
+
else:
|
|
228
|
+
rect_expr = "None"
|
|
229
|
+
clean_path = unify_path(value.path)
|
|
230
|
+
default = f'"{clean_path}"'
|
|
231
|
+
path_expr = self._transform_path(clean_path, default)
|
|
232
|
+
w.write(f'{key} = ImageSlice(file_path={path_expr}, name="{display_name}", slice_rect={rect_expr})')
|
|
233
|
+
elif isinstance(value, (int, float, str, bool)):
|
|
234
|
+
w.write(f'{key} = {repr(value)}')
|
|
235
|
+
|
|
236
|
+
# 5. display_name 属性
|
|
237
|
+
display_name = node.metadata.get('display_name', node.name)
|
|
238
|
+
w.write(f'display_name = "{display_name}"')
|
|
239
|
+
|
|
240
|
+
def _render_prefab_class(self, node: ResourceNode, data: ImageAsset):
|
|
241
|
+
"""
|
|
242
|
+
渲染 TemplateMatchPrefab 嵌套类
|
|
243
|
+
"""
|
|
244
|
+
w = self.writer
|
|
245
|
+
class_name = node.name
|
|
246
|
+
|
|
247
|
+
# 1. 类定义
|
|
248
|
+
w.write(f"class {class_name}(TemplateMatchPrefab):")
|
|
249
|
+
|
|
250
|
+
with w.indent():
|
|
251
|
+
# 2. Docstring
|
|
252
|
+
if not self.production:
|
|
253
|
+
self.render_docstring(node)
|
|
254
|
+
|
|
255
|
+
# 3. template 属性 (Image)
|
|
256
|
+
# 确保路径分隔符统一,避免 Windows 反斜杠问题
|
|
257
|
+
clean_path = unify_path(data.path)
|
|
258
|
+
rect_expr: str
|
|
259
|
+
if data.rect is not None:
|
|
260
|
+
x1, y1, x2, y2 = data.rect
|
|
261
|
+
ix1, iy1, ix2, iy2 = map(int, (x1, y1, x2, y2))
|
|
262
|
+
rect_width = ix2 - ix1
|
|
263
|
+
rect_height = iy2 - iy1
|
|
264
|
+
rect_expr = f"Rect(x={ix1}, y={iy1}, w={rect_width}, h={rect_height})"
|
|
265
|
+
else:
|
|
266
|
+
rect_expr = "None"
|
|
267
|
+
display_name = node.metadata.get('display_name', node.name)
|
|
268
|
+
default = f'"{clean_path}"'
|
|
269
|
+
path_expr = self._transform_path(clean_path, default)
|
|
270
|
+
w.write(f'template = ImageSlice(file_path={path_expr}, name="{display_name}", slice_rect={rect_expr})')
|
|
271
|
+
|
|
272
|
+
# 4. display_name 属性
|
|
273
|
+
# 优先从 metadata 取,如果没有则用变量名
|
|
274
|
+
w.write(f'display_name = "{display_name}"')
|
|
275
|
+
|
|
276
|
+
def _render_primitive_assignment(self, node: ResourceNode, data: Any):
|
|
277
|
+
"""
|
|
278
|
+
渲染 HintBox 或 HintPoint 的赋值语句
|
|
279
|
+
Example: MyBox = HintBox(x1=1, y1=2...)
|
|
280
|
+
"""
|
|
281
|
+
# 1. 生成 Docstring (如果是非生产模式)
|
|
282
|
+
if not self.production:
|
|
283
|
+
# 对于属性赋值,docstring 通常写在上方,或者不写
|
|
284
|
+
# Python 标准是将 docstring 写在赋值语句下方,但这在类属性中不太常见
|
|
285
|
+
# 这里我们选择不为 HintBox 生成复杂的 docstring,或者作为注释生成
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
# 2. 构造构造函数字符串
|
|
289
|
+
constructor_str = ""
|
|
290
|
+
|
|
291
|
+
if isinstance(data, BoxData):
|
|
292
|
+
constructor_str = (
|
|
293
|
+
f"HintBox("
|
|
294
|
+
f"x1={data.x1}, y1={data.y1}, "
|
|
295
|
+
f"x2={data.x2}, y2={data.y2}, "
|
|
296
|
+
f"source_resolution={data.resolution})"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
elif isinstance(data, PointData):
|
|
300
|
+
constructor_str = f"HintPoint(x={data.x}, y={data.y})"
|
|
301
|
+
|
|
302
|
+
# 3. 写入代码
|
|
303
|
+
self.writer.write(f"{node.name} = {constructor_str}")
|
|
304
|
+
|
|
305
|
+
def render_docstring(self, attr: ResourceNode):
|
|
306
|
+
"""
|
|
307
|
+
重写文档渲染逻辑,支持 markdown 图片预览
|
|
308
|
+
"""
|
|
309
|
+
w = self.writer
|
|
310
|
+
lines = []
|
|
311
|
+
|
|
312
|
+
# 基础描述
|
|
313
|
+
if attr.docstring:
|
|
314
|
+
lines.extend(attr.docstring.split('\n'))
|
|
315
|
+
|
|
316
|
+
# 图片预览 (仅当它是 ImageAsset 且有绝对路径用于 IDE 预览时)
|
|
317
|
+
# 注意:这里的 abs_path 需要 Parser 在 metadata 里额外塞进去,
|
|
318
|
+
# 因为 ImageAsset.path 可能已经是相对路径了。
|
|
319
|
+
if self.ide_type and isinstance(attr.value, ImageAsset):
|
|
320
|
+
preview_path = attr.metadata.get('origin_file') or attr.metadata.get('abs_path')
|
|
321
|
+
if preview_path:
|
|
322
|
+
lines.append("")
|
|
323
|
+
lines.append(self._make_img_tag(preview_path, "Preview"))
|
|
324
|
+
|
|
325
|
+
if not lines:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
w.write('"""')
|
|
329
|
+
for line in lines:
|
|
330
|
+
w.write(line)
|
|
331
|
+
w.write('"""')
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import List, Any, Protocol, Dict, Optional, Union, Tuple
|
|
4
|
+
|
|
5
|
+
# --- 工具类: CodeWriter ---
|
|
6
|
+
class CodeWriter:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self._lines = []
|
|
9
|
+
self._indent_level = 0
|
|
10
|
+
self._indent_str = " "
|
|
11
|
+
|
|
12
|
+
def write(self, text: str):
|
|
13
|
+
"""写入一行代码,自动处理缩进"""
|
|
14
|
+
self._lines.append((self._indent_str * self._indent_level) + text)
|
|
15
|
+
|
|
16
|
+
def write_empty_line(self):
|
|
17
|
+
self._lines.append("")
|
|
18
|
+
|
|
19
|
+
@contextlib.contextmanager
|
|
20
|
+
def indent(self):
|
|
21
|
+
"""缩进上下文管理器"""
|
|
22
|
+
self._indent_level += 1
|
|
23
|
+
yield
|
|
24
|
+
self._indent_level -= 1
|
|
25
|
+
|
|
26
|
+
def get_content(self) -> str:
|
|
27
|
+
return "\n".join(self._lines)
|
|
28
|
+
|
|
29
|
+
# --- 中间表示 (IR) 数据结构 ---
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ImageAsset:
|
|
34
|
+
"""代表图片资源的结构化数据"""
|
|
35
|
+
path: str
|
|
36
|
+
rect: Tuple[int, int, int, int] | None # (x1, y1, x2, y2)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class PrefabData:
|
|
41
|
+
"""代表自定义 Prefab 资源的结构化数据
|
|
42
|
+
"""
|
|
43
|
+
image: Optional[ImageAsset]
|
|
44
|
+
prefab_id: str
|
|
45
|
+
props: Dict[str, Any] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class BoxData:
|
|
50
|
+
"""代表矩形区域的结构化数据"""
|
|
51
|
+
x1: int
|
|
52
|
+
y1: int
|
|
53
|
+
x2: int
|
|
54
|
+
y2: int
|
|
55
|
+
resolution: Tuple[int, int] = (720, 1280)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class PointData:
|
|
60
|
+
"""代表点的结构化数据"""
|
|
61
|
+
x: int
|
|
62
|
+
y: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ResourceNode:
|
|
67
|
+
"""资源的最小单元 (Sprite, HintBox 等)。value 存放 IR 对象,而不是代码字符串。"""
|
|
68
|
+
name: str
|
|
69
|
+
type: str # 'template', 'hint-box', 'hint-point', 'prefab'
|
|
70
|
+
value: Union[ImageAsset, BoxData, PointData, PrefabData, Any]
|
|
71
|
+
docstring: str = ""
|
|
72
|
+
metadata: Dict[str, Any] = field(default_factory=dict) # 原始数据备份,用于扩展
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ClassNode:
|
|
76
|
+
"""表示一个生成的类节点"""
|
|
77
|
+
name: str
|
|
78
|
+
children: List['ClassNode'] = field(default_factory=list)
|
|
79
|
+
attributes: List[ResourceNode] = field(default_factory=list)
|
|
80
|
+
|
|
81
|
+
def is_empty(self) -> bool:
|
|
82
|
+
return not self.children and not self.attributes
|
|
83
|
+
|
|
84
|
+
# --- 接口定义 ---
|
|
85
|
+
|
|
86
|
+
class SchemaParser(Protocol):
|
|
87
|
+
"""解析器协议"""
|
|
88
|
+
def can_parse(self, file_path: str) -> bool:
|
|
89
|
+
"""判断该解析器是否能处理此文件"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
def parse(self, file_path: str, context: Dict[str, Any]) -> List[ResourceNode]:
|
|
93
|
+
"""解析文件并返回资源列表。Context 可包含输出目录等配置"""
|
|
94
|
+
...
|