ScratchAnalyzer 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,416 @@
1
+ from .errors import UnsupportedError
2
+ from .iostream import ColoredTqdm, ForeLightYellow
3
+ from .public import supported_languages, head_block_opcodes, entries_block_opcodes, assets_root_path
4
+ from .translator import load_translator
5
+ from .Cast import toCode
6
+ import json
7
+ import re
8
+
9
+
10
+ def replaceName(name: str):
11
+ result = ""
12
+ allowed_chars = "abcdefghijklmnopqrstuvwxyz_1234567890"
13
+ for idx, char in enumerate(name):
14
+ if idx == 0 and char in "1234567890":
15
+ result += "n"
16
+ if char.lower() not in allowed_chars:
17
+ result += f"ord_{ord(char)}_" # 不必一_开始,因为保证最后一个字符以_结束
18
+ else:
19
+ result += char
20
+ return result
21
+
22
+ class Scratch2OtherFile(object):
23
+ def __init__(self, language, name, target, isStage=False):
24
+ self.language = language
25
+ self.variables = [] # 本文件的变量
26
+ self.funcs = [] # 本文件的函数(Scratch帽子积木)
27
+ self.entries = [] # 本文件的入口函数(Scratch当绿旗被点击)
28
+ self.isStage = isStage
29
+ self.name = name
30
+ self.counts = {}
31
+ self.translator = load_translator(language)
32
+ self.target = target
33
+ self.blockToFuncName = {}
34
+ self.assets = {
35
+ "sounds": {
36
+
37
+ },
38
+ "costumes": {
39
+
40
+ }
41
+ }
42
+ self.procedures_prototypes = {}
43
+ self.getAssets()
44
+
45
+ def getAssets(self):
46
+ for costume in self.target.costumes:
47
+ self.assets["costumes"][costume["name"]] = {
48
+ "path": costume["assetId"]+"."+costume["dataFormat"],
49
+ "rotationCenterX": costume["rotationCenterX"],
50
+ "rotationCenterY": costume["rotationCenterY"]
51
+ }
52
+ for sound in self.target.sounds:
53
+ self.assets["sounds"][sound["name"]] = sound["md5ext"]
54
+
55
+ def getSpecialName(self, head):
56
+ if head.opcode == "procedures_definition":
57
+ custom_block = self.target.blocks[head.inputs["custom_block"].data[1]]
58
+ return "_"+custom_block.data["mutation"]["proccode"].replace(" ", "_").replace("%", "_"), custom_block.data["mutation"]
59
+ return "", {}
60
+
61
+ def toCodeFrom(self, head):
62
+ func_name = f"opcode_{head.opcode}_"
63
+ if func_name not in self.counts:
64
+ self.counts[func_name] = 1
65
+ else:
66
+ self.counts[func_name] += 1
67
+ func_name += str(self.counts[func_name])
68
+ special, option = self.getSpecialName(head)
69
+ func_name += special
70
+ func_name = replaceName(func_name)
71
+ self.blockToFuncName[head] = func_name
72
+ code = f"async def {func_name}(instance, task_id):\n"
73
+ # 添加记录
74
+ if head.opcode == "procedures_definition":
75
+ self.procedures_prototypes[func_name] = {
76
+ "arg_ids": json.loads(option["argumentids"]),
77
+ "arg_names": json.loads(option["argumentnames"]),
78
+ "arg_defaults": json.loads(option["argumentdefaults"]),
79
+ "warp": json.loads(f'[{option["warp"]}]')[0] if isinstance(option["warp"], str) else option["warp"],
80
+ "proccode": option["proccode"]
81
+ }
82
+ indent = 1
83
+ if not head.next:
84
+ code += " ...\n"
85
+ return code
86
+ block = head
87
+ while block.next:
88
+ block = block.next
89
+ cd, indent = block.toCode(self.translator, indent, func_name, self.procedures_prototypes)
90
+ code += (" " * indent) + cd + block.getComment(indent) + "\n"
91
+ return code
92
+
93
+ def getProceduresPrototypes(self):
94
+ code = "{"
95
+ code += ", ".join([f'{name}: {option}' for name, option in self.procedures_prototypes.items()])
96
+ code += "}"
97
+ return code
98
+
99
+ def getVariablesDict(self):
100
+ code = "{"
101
+ code += ", ".join([f'"{variable["type"]}_{variable["real_name"]}": {variable["name"]}' for variable in self.variables])
102
+ code += "}"
103
+ return code
104
+
105
+ def getFuncList(self):
106
+ code = "["
107
+ code += ", ".join([self.blockToFuncName[head] for head in self.funcs])
108
+ code += "]"
109
+ return code
110
+
111
+ def getEntryList(self):
112
+ code = "["
113
+ code += ", ".join([self.blockToFuncName[entry] for entry in self.entries])
114
+ code += "]"
115
+ return code
116
+
117
+ def getClickFuncs(self):
118
+ code = "["
119
+ click_funcs = [func for func in self.funcs if func.opcode == "event_whenthisspriteclicked"]
120
+ code += ", ".join([self.blockToFuncName[entry] for entry in click_funcs])
121
+ code += "]"
122
+ return code
123
+
124
+ def getKeyFuncMap(self):
125
+ code = "{"
126
+ keyFuncs = [item for item in self.funcs if item.opcode == "event_whenkeypressed"] # 获取所有“当按下”入口
127
+ key_to_func: dict[str, list[str]] = {}
128
+ for func in keyFuncs:
129
+ name: str = func.fields["KEY_OPTION"].data[0]
130
+ if name not in key_to_func:
131
+ key_to_func[name] = [self.blockToFuncName[func]]
132
+ else:
133
+ key_to_func[name].append(self.blockToFuncName[func])
134
+ code += ", ".join([f'"{name}": [{", ".join(bs)}]' for name, bs in key_to_func.items()])
135
+ code += "}"
136
+ return code
137
+
138
+ def getBroadcastParams(self):
139
+ code = "{"
140
+ broadcasts = [item for item in self.funcs if item.opcode == "event_whenbroadcastreceived"] # 获取所有“当接收到”入口
141
+ name_to_head: dict[str, list[str]] = {}
142
+ for broadcast in broadcasts:
143
+ name: str = broadcast.fields["BROADCAST_OPTION"].data[0]
144
+ if name not in name_to_head:
145
+ name_to_head[name] = [self.blockToFuncName[broadcast]]
146
+ else:
147
+ name_to_head[name].append(self.blockToFuncName[broadcast])
148
+ code += ", ".join([f'"{name.lower()}": [{", ".join(bs)}]' for name, bs in name_to_head.items()])
149
+ code += "}"
150
+ return code
151
+
152
+ def getBCFM(self):
153
+ code = "{"
154
+ changers = [item for item in self.funcs if item.opcode == "event_whenbackdropswitchesto"] # 获取所有“当背景换为”入口
155
+ name_to_changers: dict[str, list[str]] = {}
156
+ for changer in changers:
157
+ name: str = changer.fields["BACKDROP"].data[0]
158
+ if name not in name_to_changers:
159
+ name_to_changers[name] = [self.blockToFuncName[changer]]
160
+ else:
161
+ name_to_changers[name].append(self.blockToFuncName[changer])
162
+ code += ", ".join([f'"{name}": [{", ".join(bs)}]' for name, bs in name_to_changers.items()])
163
+ code += "}"
164
+ return code
165
+
166
+ def getCheckerMap(self):
167
+ code = "{"
168
+ checkers = [item for item in self.funcs if item.opcode == "event_whengreaterthan"] # 获取所有“当背景换为”入口
169
+ name_to_checkers: dict[str, dict[str, str]] = {}
170
+ for checker in checkers:
171
+ menu: str = checker.fields["WHENGREATERTHANMENU"].data[0]
172
+ computer: str = checker.inputs["VALUE"].toCode(self.translator, "", {})
173
+ name = self.blockToFuncName[checker]
174
+ if name not in name_to_checkers:
175
+ name_to_checkers[name] = {
176
+ "menu": menu,
177
+ "computer": computer
178
+ }
179
+ else:
180
+ raise ValueError("重复注册!")
181
+ code += ", ".join(['{name}: |-"menu": "{menu}", "computer": lambda instance: {computer}-|'.format(name=name, menu=bs["menu"],
182
+ computer=bs["computer"])
183
+ .replace("|-", "{").replace("-|", "}")
184
+ for name, bs in name_to_checkers.items()])
185
+ code += "}"
186
+ return code
187
+
188
+ def getSACFuncs(self):
189
+ code = "["
190
+ SAC_funcs = [func for func in self.funcs if func.opcode == "control_start_as_clone"]
191
+ code += ", ".join([self.blockToFuncName[entry] for entry in SAC_funcs])
192
+ code += "]"
193
+ return code
194
+
195
+ def getArgMap(self, args):
196
+ code = "{"
197
+ code += ", ".join([f'"{name}": ({code})' for name, code in args.items()])
198
+ code += "}"
199
+ return code
200
+
201
+ def generate(self, output, stage = None):
202
+ data = f'''"""
203
+ Scratch2Python库生成
204
+ 注意:由于是机器翻译代码,本文件会有多处地方出现冗余的括号、多余的代码等。请不要以此文件来学习Python。
205
+ """
206
+ import Scratch4Python as Scratch # 专用Scratch功能封装库
207
+ {"import target_Stage as Stage # 引入舞台以使用公共变量" if not self.isStage else ""}
208
+
209
+ # 变量
210
+
211
+ '''
212
+
213
+ # 生成变量
214
+ for variable in ColoredTqdm(self.variables, desc="正在生成变量代码"):
215
+ if variable["type"] == "v":
216
+ if isinstance(variable["default"], int):
217
+ data += f"{variable['name']} = {variable['default']} # Scratch变量——原名:{variable['real_name']}\n"
218
+ else:
219
+ data += f"{variable['name']} = '{variable['default']}' # Scratch变量——原名:{variable['real_name']}\n"
220
+ else:
221
+ data += f"{variable['name']} = {variable['default']} # Scratch列表——原名:{variable['real_name']}\n"
222
+
223
+ data += "\n# 函数\n\n"
224
+ # 生成函数代码
225
+ for func in ColoredTqdm(self.funcs, desc="正在生成函数"):
226
+ data += self.toCodeFrom(func) + "\n"
227
+
228
+ # 添加主程序
229
+ with open(assets_root_path / f"codeMain.{self.language}.tpl", "r", encoding="utf-8") as file:
230
+ codeMain = file.read().replace("""import Scratch4Python as Scratch
231
+ """, "")
232
+ data += codeMain.format(name=self.name, variables=self.getVariablesDict(), x=self.target.x, y=self.target.y,
233
+ direction=self.target.direction, visible=self.target.visible, size=self.target.size,
234
+ currentCostume=self.target.currentCostume, funcs=self.getFuncList(), entries=self.getEntryList(),
235
+ assets=json.dumps(self.assets, ensure_ascii=False, indent=4), broadcast_params=self.getBroadcastParams(),
236
+ layer_order=self.target.layerOrder, volume=self.target.volume, click_funcs=self.getClickFuncs(),
237
+ key_map=self.getKeyFuncMap(), backdrop_change_func_map=self.getBCFM(), checker_map=self.getCheckerMap(),
238
+ start_as_clone_funcs=self.getSACFuncs(), procedures_prototypes=self.getProceduresPrototypes())
239
+
240
+ # 代码函数引用替换
241
+ global_tasks = []
242
+ first_prcesses = []
243
+ # 使用更精确的匹配模式,确保不跨标记匹配
244
+ pattern_for_code = r'!!!\[(SPECIAL_CODE_TO_GLOBAL)\]\[(.*?)\]!!!'
245
+ pattern = r'!!!\[([^\[\]]+?)\]\[(.*?)\]!!!'
246
+
247
+ # 先处理变量请求
248
+ for match in ColoredTqdm(re.finditer(pattern_for_code, data, re.DOTALL), desc="正在查找全局替换变量请求"):
249
+ name = match.group(1).strip()
250
+ value = match.group(2)
251
+ if name == "SPECIAL_CODE_TO_GLOBAL":
252
+ first_prcesses.append((name, value))
253
+
254
+ for key, value in first_prcesses:
255
+ hasVariable = False
256
+ code = None
257
+ for vid, variable in self.target.variables.items():
258
+ if variable[0] == value:
259
+ code = f'Scratch.getVariable(instance, "{value}")'
260
+ hasVariable = True
261
+ break
262
+ if not hasVariable:
263
+ if stage:
264
+ for vid, variable in stage.variables.items():
265
+ if variable[0] == value:
266
+ code = f'Scratch.getVariable(instance, "{value}")'
267
+ hasVariable = True
268
+ break
269
+ if not hasVariable:
270
+ code = toCode(value)
271
+ if code is None:
272
+ raise ValueError(f"无法处理的特殊全局替换请求!value=`{value}`")
273
+ data = data.replace(f'!!![{key}][{value}]!!!', code) # 替换
274
+
275
+ # 其次检查其他请求
276
+ for match in ColoredTqdm(re.finditer(pattern, data, re.DOTALL), desc="正在查找全局替换普通请求"):
277
+ name = match.group(1).strip()
278
+ value = match.group(2).strip()
279
+ if name == "SPECIAL_CODE_TO_GLOBAL":
280
+ continue
281
+ global_tasks.append((name, value))
282
+
283
+ last_func_name = None
284
+ for key, value in ColoredTqdm(global_tasks, "正在进行全局替换"):
285
+ match key:
286
+ case "FUNC_NAME_TO_GLOBAL":
287
+ found = False
288
+ for name, option in self.procedures_prototypes.items():
289
+ if option["proccode"] == value:
290
+ data = data.replace(f'!!![{key}][{value}]!!!', name) # 替换为函数名
291
+ found = True
292
+ last_func_name = name
293
+ break
294
+ if not found:
295
+ raise ValueError("未找到proccode对应的自定义函数!")
296
+ case "ARGS_TO_GLOBAL":
297
+ pattern = r'Scratch\.getVariable\(instance,\s*"([^"]*)"\)'
298
+ replacement = r'Scratch.getVariable(instance, \\"\1\\")'
299
+ value2 = re.sub(pattern, replacement, value)
300
+ value2 = re.sub(r'""(.+?)""', r'"\\"\1\\""', value2)
301
+ try:
302
+ _args = json.loads(value2)
303
+ except json.JSONDecodeError:
304
+ pattern = r'\((?!\\")(")([^"\\]*(?:\\.[^"\\]*)*)(")(?<!\\)\)'
305
+ # value2 = value2.replace('("', '(\\"').replace('")', '\\")')
306
+ value2 = re.sub(pattern, r'(\\"\2\\")', value2)
307
+ value2 = value2.replace('("', '(\\"').replace('", error=False)', '\\", error=False)')
308
+ try:
309
+ _args = json.loads(value2)
310
+ except json.JSONDecodeError:
311
+ raise ValueError(f"错误的值数据:{value}\n修复后:{value2}")
312
+ if not last_func_name:
313
+ raise ValueError("意外出现的参数全局替换请求,通常是先请求替换函数名!")
314
+ option = self.procedures_prototypes[last_func_name]
315
+ args = {}
316
+ for arg_id, code in _args.items():
317
+ arg_idx = option["arg_ids"].index(arg_id)
318
+ arg_name = option["arg_names"][arg_idx]
319
+ args[arg_name] = code
320
+ data = data.replace(f'!!![{key}][{value}]!!!', self.getArgMap(args)) # 替换为参数列表
321
+ case _:
322
+ raise ValueError(f"意外的全局替换请求!key=`{key}`,value=`{value}`")
323
+
324
+ # 写入文件
325
+ path = output / f"target_{self.name}.py"
326
+ with open(path, "w", encoding="utf-8") as file:
327
+ file.write(data)
328
+
329
+ class Scratch(object):
330
+ def __init__(self, project):
331
+ self.project = project
332
+ self.public_id_to_variable_name = {}
333
+
334
+ def generate(self, output, language="python"):
335
+ language = language.lower()
336
+ if language not in supported_languages:
337
+ raise UnsupportedError(f"暂不支持转换为{language}语言")
338
+ print(ForeLightYellow("开始将Scratch项目转换为", language, "语言!"))
339
+ # 首先处理舞台
340
+ stage = self.project.targets["Stage"]
341
+ if not stage.isStage:
342
+ raise ValueError("Stage角色不是舞台!舞台角色检测失败!")
343
+ stage_file = Scratch2OtherFile(language, stage.name, stage, True)
344
+ # 收集公共变量
345
+ for vid, variable in ColoredTqdm(stage.variables.items(), desc="正在收集公共变量"):
346
+ name = replaceName(f"public_variable_{variable[0]}")
347
+ stage_file.variables.append({
348
+ "name": name,
349
+ "default": variable[1],
350
+ "real_name": variable[0],
351
+ "type": "v"
352
+ })
353
+ self.public_id_to_variable_name[vid] = name
354
+ # 收集公共列表
355
+ for vid, li in ColoredTqdm(stage.lists.items(), desc="正在收集公共列表"):
356
+ name = replaceName(f"public_list_{li[0]}")
357
+ stage_file.variables.append({
358
+ "name": name,
359
+ "default": li[1],
360
+ "real_name": li[0],
361
+ "type": "l"
362
+ })
363
+ self.public_id_to_variable_name[vid] = name
364
+ # 收集函数入口点
365
+ for bid, block in ColoredTqdm(stage.blocks.items(), desc="正在收集函数入口点"):
366
+ if block.opcode in head_block_opcodes:
367
+ stage_file.funcs.append(block)
368
+ if block.opcode in entries_block_opcodes:
369
+ stage_file.entries.append(block)
370
+ # 开始生成舞台文件
371
+ stage_file.generate(output)
372
+
373
+ # 生成剩余标准角色
374
+ for name, target in ColoredTqdm(self.project.targets.items(), desc="正在处理角色"):
375
+ if name == "Stage":
376
+ continue
377
+
378
+ target_file = Scratch2OtherFile(language, replaceName(target.name), target, False)
379
+ # 收集公共变量
380
+ for vid, variable in ColoredTqdm(target.variables.items(), desc="正在收集变量"):
381
+ name = replaceName(f"{target.name}_variable_{variable[0]}")
382
+ target_file.variables.append({
383
+ "name": name,
384
+ "default": variable[1],
385
+ "real_name": variable[0],
386
+ "type": "v"
387
+ })
388
+ # 收集公共列表
389
+ for vid, li in ColoredTqdm(target.lists.items(), desc="正在收集列表"):
390
+ name = replaceName(f"{target.name}_list_{li[0]}")
391
+ target_file.variables.append({
392
+ "name": name,
393
+ "default": li[1],
394
+ "real_name": li[0],
395
+ "type": "l"
396
+ })
397
+ # 收集函数入口点
398
+ for bid, block in ColoredTqdm(target.blocks.items(), desc="正在收集函数入口点"):
399
+ if block.opcode in head_block_opcodes:
400
+ target_file.funcs.append(block)
401
+ if block.opcode in entries_block_opcodes:
402
+ target_file.entries.append(block)
403
+ # 开始生成舞台文件
404
+ target_file.generate(output, stage=stage)
405
+ # 生成数据
406
+ imports_code = ""
407
+ inits = "["
408
+ for name, target in self.project.targets.items():
409
+ name = replaceName(name)
410
+ name = "target_{name}".format(name=name)
411
+ imports_code += "from {name} import init as {name}_init\n".format(name=name)
412
+ inits += "{name}_init, ".format(name=name)
413
+ inits += "]"
414
+ # 生成并复制主程序
415
+ with open(assets_root_path / f"progMain.{language}.tpl", "r", encoding="utf-8") as ifile, open(output / "main.py", "w", encoding="utf-8") as ofile:
416
+ ofile.write(ifile.read().format(imports=imports_code, inits=inits))
@@ -0,0 +1,95 @@
1
+ from pathlib import Path
2
+ from typing import Literal, Dict, List, TypedDict, Any, Union, Callable, Optional
3
+ from .Project import Project, Target, Block
4
+ from .translator import WrappedTranslator
5
+
6
+ Number = Union[int, float]
7
+
8
+ class VariableDict(TypedDict):
9
+ name: str
10
+ default: Any
11
+ real_name: str
12
+ type: Literal["v", "l"]
13
+
14
+ class CostumeDict(TypedDict):
15
+ path: str
16
+ rotationCenterX: Number
17
+ rotationCenterY: Number
18
+
19
+ class AssetsDict(TypedDict):
20
+ sounds: Dict[str, str] # key: name, value: basename
21
+ costumes: Dict[str, CostumeDict] # key: name, value: {path: basename, ...}
22
+
23
+ Language = Literal["python", "java"]
24
+
25
+ class Scratch2OtherFile(object):
26
+ language: Language
27
+ variables: List[VariableDict]
28
+ funcs: List[Block]
29
+ entries: List[Block]
30
+ isStage: bool
31
+ name: str
32
+ counts: Dict[str, int]
33
+ translator: WrappedTranslator
34
+ target: Target
35
+ blockToFuncName: Dict[Block, str]
36
+ procedures_prototypes: Dict[str, dict]
37
+ assets: AssetsDict
38
+
39
+ def __init__(self, language: Language, name: str, target: Target, isStage: bool=False):
40
+ ...
41
+
42
+ def getAssets(self) -> None:
43
+ ...
44
+
45
+ def getSpecialName(self, head: Block) -> tuple[str, dict]:
46
+ ...
47
+
48
+ def toCodeFrom(self, head: Block) -> str:
49
+ ...
50
+
51
+ def getProceduresPrototypes(self) -> str:
52
+ ...
53
+
54
+ def getVariablesDict(self) -> str:
55
+ ...
56
+
57
+ def getFuncList(self) -> str:
58
+ ...
59
+
60
+ def getEntryList(self) -> str:
61
+ ...
62
+
63
+ def getClickFuncs(self) -> str:
64
+ ...
65
+
66
+ def getKeyFuncMap(self) -> str:
67
+ ...
68
+
69
+ def getBroadcastParams(self) -> str:
70
+ ...
71
+
72
+ def getBCFM(self) -> str:
73
+ ...
74
+
75
+ def getCheckerMap(self) -> str:
76
+ ...
77
+
78
+ def getSACFuncs(self) -> str:
79
+ ...
80
+
81
+ def getArgMap(self, args: dict[str, str]) -> str:
82
+ ...
83
+
84
+ def generate(self, output: Path, stage: Optional[Target] = None) -> None:
85
+ ...
86
+
87
+ class Scratch(object):
88
+ project: Project
89
+ public_id_to_variable_name: Dict[str, str]
90
+
91
+ def __init__(self, project: Project):
92
+ ...
93
+
94
+ def generate(self, output: Path, language: Language="python"):
95
+ ...
@@ -0,0 +1,6 @@
1
+ from . import Project
2
+ from . import Scratch
3
+
4
+ __all__ = ["Project", "Scratch"]
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+ from .main import main
3
+
4
+ if __name__ == "__main__":
5
+ sys.exit(main())
@@ -0,0 +1,8 @@
1
+ import Scratch4Python as Scratch
2
+ # 初始化函数
3
+ def init():
4
+ default_instance = Scratch.Instance("{name}", {variables}, {x}, {y}, {direction}, {visible}, {size}, {currentCostume},
5
+ {funcs}, {entries}, {assets}, {broadcast_params}, {layer_order}, {volume},
6
+ {click_funcs}, {key_map}, {backdrop_change_func_map}, {checker_map},
7
+ {start_as_clone_funcs}, {procedures_prototypes})
8
+ Scratch.register(default_instance)
@@ -0,0 +1,29 @@
1
+ """
2
+ 本代码有Scratch2Python生成
3
+ """
4
+ # 引入各个角色
5
+ {imports}
6
+ # 引入核心库
7
+ import Scratch4Python as Scratch
8
+ # 参数解析
9
+ import argparse
10
+
11
+ # 主程序
12
+ def main():
13
+ # 初始化参数
14
+ parser = argparse.ArgumentParser()
15
+ parser.add_argument("--username", "-u", type=str, required=False, default="Scratch VM PE 用户", help="运行时使用的用户名")
16
+ parser.add_argument("--fps", "-f", type=int, required=False, default=30, help="运行帧率")
17
+ args = parser.parse_args()
18
+ # 初始化虚拟机
19
+ vm = Scratch.ScratchVM(args.username)
20
+ Scratch.setVM(vm)
21
+ # 初始化各个角色
22
+ inits = {inits}
23
+ for init in inits:
24
+ init()
25
+ # 启动!
26
+ exit(Scratch.main())
27
+
28
+ if __name__ == "__main__":
29
+ main()