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.
- ScratchAnalyzer/Cast.py +12 -0
- ScratchAnalyzer/Project.py +277 -0
- ScratchAnalyzer/Project.pyi +235 -0
- ScratchAnalyzer/Scratch.py +416 -0
- ScratchAnalyzer/Scratch.pyi +95 -0
- ScratchAnalyzer/__init__.py +6 -0
- ScratchAnalyzer/__main__.py +5 -0
- ScratchAnalyzer/assets/codeMain.python.tpl +8 -0
- ScratchAnalyzer/assets/progMain.python.tpl +29 -0
- ScratchAnalyzer/assets/translator.python.json +126 -0
- ScratchAnalyzer/errors.py +2 -0
- ScratchAnalyzer/iostream.py +103 -0
- ScratchAnalyzer/iostream.pyi +116 -0
- ScratchAnalyzer/main.py +78 -0
- ScratchAnalyzer/public.py +41 -0
- ScratchAnalyzer/translator.py +20 -0
- scratchanalyzer-0.1.0.dist-info/METADATA +90 -0
- scratchanalyzer-0.1.0.dist-info/RECORD +22 -0
- scratchanalyzer-0.1.0.dist-info/WHEEL +5 -0
- scratchanalyzer-0.1.0.dist-info/entry_points.txt +2 -0
- scratchanalyzer-0.1.0.dist-info/licenses/LICENSE +21 -0
- scratchanalyzer-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,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()
|