scratchattach 3.0.0b0__py3-none-any.whl → 3.0.0b2__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.
- scratchattach/cli/__about__.py +1 -0
- scratchattach/cli/__init__.py +26 -0
- scratchattach/cli/cmd/__init__.py +4 -0
- scratchattach/cli/cmd/group.py +127 -0
- scratchattach/cli/cmd/login.py +60 -0
- scratchattach/cli/cmd/profile.py +7 -0
- scratchattach/cli/cmd/sessions.py +5 -0
- scratchattach/cli/context.py +142 -0
- scratchattach/cli/db.py +66 -0
- scratchattach/cli/namespace.py +14 -0
- scratchattach/cloud/__init__.py +2 -0
- scratchattach/cloud/_base.py +483 -0
- scratchattach/cloud/cloud.py +183 -0
- scratchattach/editor/__init__.py +22 -0
- scratchattach/editor/asset.py +265 -0
- scratchattach/editor/backpack_json.py +115 -0
- scratchattach/editor/base.py +191 -0
- scratchattach/editor/block.py +584 -0
- scratchattach/editor/blockshape.py +357 -0
- scratchattach/editor/build_defaulting.py +51 -0
- scratchattach/editor/code_translation/__init__.py +0 -0
- scratchattach/editor/code_translation/parse.py +177 -0
- scratchattach/editor/comment.py +80 -0
- scratchattach/editor/commons.py +145 -0
- scratchattach/editor/extension.py +50 -0
- scratchattach/editor/field.py +99 -0
- scratchattach/editor/inputs.py +138 -0
- scratchattach/editor/meta.py +117 -0
- scratchattach/editor/monitor.py +185 -0
- scratchattach/editor/mutation.py +381 -0
- scratchattach/editor/pallete.py +88 -0
- scratchattach/editor/prim.py +174 -0
- scratchattach/editor/project.py +381 -0
- scratchattach/editor/sprite.py +609 -0
- scratchattach/editor/twconfig.py +114 -0
- scratchattach/editor/vlb.py +134 -0
- scratchattach/eventhandlers/__init__.py +0 -0
- scratchattach/eventhandlers/_base.py +101 -0
- scratchattach/eventhandlers/cloud_events.py +130 -0
- scratchattach/eventhandlers/cloud_recorder.py +26 -0
- scratchattach/eventhandlers/cloud_requests.py +544 -0
- scratchattach/eventhandlers/cloud_server.py +249 -0
- scratchattach/eventhandlers/cloud_storage.py +135 -0
- scratchattach/eventhandlers/combine.py +30 -0
- scratchattach/eventhandlers/filterbot.py +163 -0
- scratchattach/eventhandlers/message_events.py +42 -0
- scratchattach/other/__init__.py +0 -0
- scratchattach/other/other_apis.py +598 -0
- scratchattach/other/project_json_capabilities.py +475 -0
- scratchattach/site/__init__.py +0 -0
- scratchattach/site/_base.py +93 -0
- scratchattach/site/activity.py +426 -0
- scratchattach/site/alert.py +226 -0
- scratchattach/site/backpack_asset.py +119 -0
- scratchattach/site/browser_cookie3_stub.py +17 -0
- scratchattach/site/browser_cookies.py +61 -0
- scratchattach/site/classroom.py +454 -0
- scratchattach/site/cloud_activity.py +121 -0
- scratchattach/site/comment.py +228 -0
- scratchattach/site/forum.py +436 -0
- scratchattach/site/placeholder.py +132 -0
- scratchattach/site/project.py +932 -0
- scratchattach/site/session.py +1323 -0
- scratchattach/site/studio.py +704 -0
- scratchattach/site/typed_dicts.py +151 -0
- scratchattach/site/user.py +1252 -0
- scratchattach/utils/__init__.py +0 -0
- scratchattach/utils/commons.py +263 -0
- scratchattach/utils/encoder.py +161 -0
- scratchattach/utils/enums.py +237 -0
- scratchattach/utils/exceptions.py +277 -0
- scratchattach/utils/optional_async.py +154 -0
- scratchattach/utils/requests.py +306 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/METADATA +1 -1
- scratchattach-3.0.0b2.dist-info/RECORD +81 -0
- scratchattach-3.0.0b0.dist-info/RECORD +0 -8
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/WHEEL +0 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/entry_points.txt +0 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import Optional, Iterable, Union
|
|
5
|
+
from typing_extensions import Self
|
|
6
|
+
|
|
7
|
+
from . import base, sprite, mutation, field, inputs, commons, vlb, blockshape, prim, comment, build_defaulting
|
|
8
|
+
from scratchattach.utils import exceptions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Block(base.SpriteSubComponent):
|
|
12
|
+
"""
|
|
13
|
+
Represents a block in the scratch editor, as a subcomponent of a sprite.
|
|
14
|
+
"""
|
|
15
|
+
_id: Optional[str] = None
|
|
16
|
+
def __init__(self, _opcode: str, _shadow: bool = False, _top_level: Optional[bool] = None,
|
|
17
|
+
_mutation: Optional[mutation.Mutation] = None, _fields: Optional[dict[str, field.Field]] = None,
|
|
18
|
+
_inputs: Optional[dict[str, inputs.Input]] = None, x: int = 0, y: int = 0, pos: Optional[tuple[int, int]] = None,
|
|
19
|
+
|
|
20
|
+
_next: Optional[Block] = None, _parent: Optional[Block] = None,
|
|
21
|
+
*, _next_id: Optional[str] = None, _parent_id: Optional[str] = None, _sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT):
|
|
22
|
+
# Defaulting for args
|
|
23
|
+
if _fields is None:
|
|
24
|
+
_fields = {}
|
|
25
|
+
if _inputs is None:
|
|
26
|
+
_inputs = {}
|
|
27
|
+
|
|
28
|
+
if pos is not None:
|
|
29
|
+
x, y = pos
|
|
30
|
+
|
|
31
|
+
self.opcode = _opcode
|
|
32
|
+
self.is_shadow = _shadow
|
|
33
|
+
self.is_top_level = _top_level
|
|
34
|
+
|
|
35
|
+
self.x, self.y = x, y
|
|
36
|
+
|
|
37
|
+
self.mutation = _mutation
|
|
38
|
+
self.fields = _fields
|
|
39
|
+
self.inputs = _inputs
|
|
40
|
+
|
|
41
|
+
# Temporarily stores id of next block. Will be used later during project instantiation to find the next block object
|
|
42
|
+
self._next_id = _next_id
|
|
43
|
+
|
|
44
|
+
# Temporarily stores id of parent block. Will be used later during project instantiation to find the parent block object
|
|
45
|
+
self._parent_id = _parent_id
|
|
46
|
+
|
|
47
|
+
self.next = _next
|
|
48
|
+
self.parent = _parent
|
|
49
|
+
|
|
50
|
+
self.check_toplevel()
|
|
51
|
+
|
|
52
|
+
super().__init__(_sprite)
|
|
53
|
+
self.link_subcomponents()
|
|
54
|
+
|
|
55
|
+
def __repr__(self):
|
|
56
|
+
return f"Block<{self.opcode!r}>"
|
|
57
|
+
|
|
58
|
+
def link_subcomponents(self):
|
|
59
|
+
"""
|
|
60
|
+
Iterate through subcomponents and assign the 'block' attribute
|
|
61
|
+
"""
|
|
62
|
+
if self.mutation:
|
|
63
|
+
self.mutation.block = self
|
|
64
|
+
|
|
65
|
+
for iterable in (self.fields.values(), self.inputs.values()):
|
|
66
|
+
for subcomponent in iterable:
|
|
67
|
+
subcomponent.block = self
|
|
68
|
+
|
|
69
|
+
def add_input(self, name: str, _input: inputs.Input) -> Self:
|
|
70
|
+
"""
|
|
71
|
+
Attach an input object to the block.
|
|
72
|
+
"""
|
|
73
|
+
self.inputs[name] = _input
|
|
74
|
+
for val in (_input.value, _input.obscurer):
|
|
75
|
+
if isinstance(val, Block):
|
|
76
|
+
val.parent = self
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def add_field(self, name: str, _field: field.Field) -> Self:
|
|
80
|
+
"""
|
|
81
|
+
Attach a field object to the block.
|
|
82
|
+
"""
|
|
83
|
+
self.fields[name] = _field
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def set_mutation(self, _mutation: mutation.Mutation) -> Self:
|
|
87
|
+
"""
|
|
88
|
+
Attach a mutation object and link it, also asking the mutation to link its own subcomponents.
|
|
89
|
+
"""
|
|
90
|
+
self.mutation = _mutation
|
|
91
|
+
_mutation.block = self
|
|
92
|
+
_mutation.link_arguments()
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def set_comment(self, _comment: comment.Comment) -> Self:
|
|
96
|
+
"""
|
|
97
|
+
Attach a comment and add it to the sprite.
|
|
98
|
+
"""
|
|
99
|
+
_comment.block = self
|
|
100
|
+
self.sprite.add_comment(_comment)
|
|
101
|
+
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def check_toplevel(self):
|
|
105
|
+
"""
|
|
106
|
+
Edit the toplevel, x, and y attributes based on whether the parent attribute is None
|
|
107
|
+
"""
|
|
108
|
+
self.is_top_level = self.parent is None
|
|
109
|
+
|
|
110
|
+
if not self.is_top_level:
|
|
111
|
+
self.x, self.y = None, None
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def target(self):
|
|
115
|
+
"""
|
|
116
|
+
Alias for .sprite
|
|
117
|
+
"""
|
|
118
|
+
# should this be deprecated?
|
|
119
|
+
return self.sprite
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def block_shape(self) -> blockshape.BlockShape:
|
|
123
|
+
"""
|
|
124
|
+
Search for & return the associated blockshape stored in blockshape.py
|
|
125
|
+
:return: The block's block shape (by opcode)
|
|
126
|
+
"""
|
|
127
|
+
_shape = blockshape.BlockShapes.find(self.opcode, "opcode")
|
|
128
|
+
if _shape is None:
|
|
129
|
+
warnings.warn(f"No blockshape {self.opcode!r} exists! Defaulting to {blockshape.BlockShapes.UNDEFINED}")
|
|
130
|
+
_shape = blockshape.BlockShapes.UNDEFINED
|
|
131
|
+
assert isinstance(_shape, blockshape.BlockShape)
|
|
132
|
+
return _shape
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def can_next(self):
|
|
136
|
+
"""
|
|
137
|
+
:return: Whether the block *can* have a next block (checks if it's not a cap block, but also considering the special behaviour of control_stop)
|
|
138
|
+
"""
|
|
139
|
+
_shape = self.block_shape
|
|
140
|
+
if _shape.is_cap is not blockshape.MUTATION_DEPENDENT:
|
|
141
|
+
return _shape.is_attachable
|
|
142
|
+
else:
|
|
143
|
+
if self.mutation is None:
|
|
144
|
+
# If there's no mutation, let's just assume yes
|
|
145
|
+
# add filterwarnings?
|
|
146
|
+
warnings.warn(f"{self} has no mutation! Assuming we can add blocks!")
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
return self.mutation.has_next
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def id(self) -> str:
|
|
153
|
+
"""
|
|
154
|
+
Work out the id of this block by searching through the sprite dictionary
|
|
155
|
+
If one cannot be found, generate a new one and return that.
|
|
156
|
+
"""
|
|
157
|
+
if self._id:
|
|
158
|
+
return self._id
|
|
159
|
+
# warnings.warn(f"Using block IDs can cause consistency issues and is not recommended")
|
|
160
|
+
# This property is used when converting comments to JSON (we don't want random warning when exporting a project)
|
|
161
|
+
for _block_id, _block in self.sprite.blocks.items():
|
|
162
|
+
if _block is self:
|
|
163
|
+
self._id = _block_id
|
|
164
|
+
return self._id
|
|
165
|
+
|
|
166
|
+
# Let's just automatically assign ourselves an id
|
|
167
|
+
self.sprite.add_block(self)
|
|
168
|
+
return self._id
|
|
169
|
+
|
|
170
|
+
@id.setter
|
|
171
|
+
def id(self, value: str) -> None:
|
|
172
|
+
self._id = value
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def parent_id(self):
|
|
176
|
+
"""
|
|
177
|
+
Get the id of the parent block, if applicable
|
|
178
|
+
"""
|
|
179
|
+
if self.parent is not None:
|
|
180
|
+
return self.parent.id
|
|
181
|
+
else:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def next_id(self):
|
|
186
|
+
"""
|
|
187
|
+
Get the id of the next block, if applicable
|
|
188
|
+
"""
|
|
189
|
+
if self.next is not None:
|
|
190
|
+
return self.next.id
|
|
191
|
+
else:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def relatives(self) -> list[Block]:
|
|
196
|
+
"""
|
|
197
|
+
:return: A list of blocks which are related to this block (i.e. parent & next)
|
|
198
|
+
"""
|
|
199
|
+
# TODO: consider adding input blocks?
|
|
200
|
+
_ret = []
|
|
201
|
+
|
|
202
|
+
def yield_block(_block: Block | None):
|
|
203
|
+
if isinstance(_block, Block):
|
|
204
|
+
_ret.append(_block)
|
|
205
|
+
|
|
206
|
+
yield_block(self.next)
|
|
207
|
+
yield_block(self.parent)
|
|
208
|
+
|
|
209
|
+
return _ret
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def children(self) -> list[Block | prim.Prim]:
|
|
213
|
+
"""
|
|
214
|
+
:return: A list of blocks that are inside of this block, **NOT INCLUDING THE ATTACHED BLOCK**
|
|
215
|
+
"""
|
|
216
|
+
# does this include procedure definitions' inner block?
|
|
217
|
+
_children = []
|
|
218
|
+
for _input in self.inputs.values():
|
|
219
|
+
if isinstance(_input.value, Block) or isinstance(_input.value, prim.Prim):
|
|
220
|
+
_children.append(_input.value)
|
|
221
|
+
|
|
222
|
+
if _input.obscurer is not None:
|
|
223
|
+
_children.append(_input.obscurer)
|
|
224
|
+
return _children
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def previous_chain(self):
|
|
228
|
+
"""
|
|
229
|
+
Recursive getter method to get all previous blocks in the blockchain (until hitting a top-level block)
|
|
230
|
+
"""
|
|
231
|
+
# TODO: check if this hits the recursion limit
|
|
232
|
+
if self.parent is None: # TODO: use is_top_level?
|
|
233
|
+
return [self]
|
|
234
|
+
|
|
235
|
+
return [self] + self.parent.previous_chain
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def attached_chain(self):
|
|
239
|
+
"""
|
|
240
|
+
Recursive getter method to get all next blocks in the blockchain (until hitting a bottom-levell block)
|
|
241
|
+
"""
|
|
242
|
+
# TODO: check if this hits the recursion limit
|
|
243
|
+
if self.next is None:
|
|
244
|
+
return [self]
|
|
245
|
+
|
|
246
|
+
return [self] + self.next.attached_chain
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def complete_chain(self):
|
|
250
|
+
"""
|
|
251
|
+
Attach previous and attached chains from this block
|
|
252
|
+
"""
|
|
253
|
+
return self.previous_chain[:1:-1] + self.attached_chain
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def top_level_block(self):
|
|
257
|
+
"""
|
|
258
|
+
Get the first (top level) block in the block stack that this block is part of
|
|
259
|
+
"""
|
|
260
|
+
return self.previous_chain[-1]
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def bottom_level_block(self):
|
|
264
|
+
"""
|
|
265
|
+
Get the last block in the block stack that this block is part of
|
|
266
|
+
"""
|
|
267
|
+
return self.attached_chain[-1]
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def stack_tree(self):
|
|
271
|
+
"""
|
|
272
|
+
Useful for showing a block stack in the console, using pprint
|
|
273
|
+
:return: A tree-like nested list structure representing the stack of blocks, including inputs, starting at this block
|
|
274
|
+
"""
|
|
275
|
+
_tree: list[Block | prim.Prim | list[Block]] = [self]
|
|
276
|
+
for child in self.children:
|
|
277
|
+
if isinstance(child, prim.Prim):
|
|
278
|
+
_tree.append(child)
|
|
279
|
+
elif isinstance(child, Block):
|
|
280
|
+
_tree.append(child.stack_tree)
|
|
281
|
+
|
|
282
|
+
if self.next:
|
|
283
|
+
_tree += self.next.stack_tree
|
|
284
|
+
|
|
285
|
+
return _tree
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def category(self):
|
|
289
|
+
"""
|
|
290
|
+
Works out what category of block this is as a string, using the opcode. Does not perform validation
|
|
291
|
+
"""
|
|
292
|
+
return self.opcode.split('_')[0]
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def is_input(self):
|
|
296
|
+
"""
|
|
297
|
+
:return: Whether this block is an input obscurer or value
|
|
298
|
+
"""
|
|
299
|
+
return self.parent_input is not None
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def is_next_block(self):
|
|
303
|
+
"""
|
|
304
|
+
:return: Whether this block is attached (as next block) to a previous block and not an input
|
|
305
|
+
"""
|
|
306
|
+
return self.parent and not self.is_input
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def parent_input(self):
|
|
310
|
+
"""
|
|
311
|
+
Fetch an input that this block is placed inside of (if applicable)
|
|
312
|
+
"""
|
|
313
|
+
if not self.parent:
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
for _input in self.parent.inputs.values():
|
|
317
|
+
if _input.obscurer is self or _input.value is self:
|
|
318
|
+
return _input
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def new_id(self):
|
|
323
|
+
return self.sprite.new_id
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def comment(self) -> comment.Comment | None:
|
|
327
|
+
"""
|
|
328
|
+
Fetch an associated comment (if applicable) by searching the associated sprite
|
|
329
|
+
"""
|
|
330
|
+
for _comment in self.sprite.comments:
|
|
331
|
+
if _comment.block is self:
|
|
332
|
+
return _comment
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def turbowarp_block_opcode(self):
|
|
337
|
+
"""
|
|
338
|
+
:return: The 'opcode' if this is a turbowarp block: e.g.
|
|
339
|
+
- log
|
|
340
|
+
- breakpoint
|
|
341
|
+
- error
|
|
342
|
+
- warn
|
|
343
|
+
- is compiled?
|
|
344
|
+
- is turbowarp?
|
|
345
|
+
- is forkphorus?
|
|
346
|
+
If it's not one, just returns None
|
|
347
|
+
"""
|
|
348
|
+
if self.opcode == "procedures_call":
|
|
349
|
+
if self.mutation:
|
|
350
|
+
if self.mutation.proc_code:
|
|
351
|
+
# \u200B is a zero-width space
|
|
352
|
+
if self.mutation.proc_code == "\u200B\u200Bbreakpoint\u200B\u200B":
|
|
353
|
+
return "breakpoint"
|
|
354
|
+
elif self.mutation.proc_code == "\u200B\u200Blog\u200B\u200B %s":
|
|
355
|
+
return "log"
|
|
356
|
+
elif self.mutation.proc_code == "\u200B\u200Berror\u200B\u200B %s":
|
|
357
|
+
return "error"
|
|
358
|
+
elif self.mutation.proc_code == "\u200B\u200Bwarn\u200B\u200B %s":
|
|
359
|
+
return "warn"
|
|
360
|
+
|
|
361
|
+
elif self.opcode == "argument_reporter_boolean":
|
|
362
|
+
arg = self.fields.get("VALUE")
|
|
363
|
+
|
|
364
|
+
if arg is not None:
|
|
365
|
+
arg = arg.value
|
|
366
|
+
if isinstance(arg, str):
|
|
367
|
+
arg = arg.lower()
|
|
368
|
+
|
|
369
|
+
if arg == "is turbowarp?":
|
|
370
|
+
return "is_turbowarp?"
|
|
371
|
+
|
|
372
|
+
elif arg == "is compiled?":
|
|
373
|
+
return "is_compiled?"
|
|
374
|
+
|
|
375
|
+
elif arg == "is forkphorus?":
|
|
376
|
+
return "is_forkphorus?"
|
|
377
|
+
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def is_turbowarp_block(self):
|
|
382
|
+
"""
|
|
383
|
+
Return whether this block is actually a turbowarp debugger/boolean block, based on mutation
|
|
384
|
+
"""
|
|
385
|
+
return self.turbowarp_block_opcode is not None
|
|
386
|
+
|
|
387
|
+
@staticmethod
|
|
388
|
+
def from_json(data: dict) -> Block:
|
|
389
|
+
"""
|
|
390
|
+
Load a block from the JSON dictionary.
|
|
391
|
+
:param data: a dictionary (not list)
|
|
392
|
+
:return: The new Block object
|
|
393
|
+
"""
|
|
394
|
+
_opcode = data["opcode"]
|
|
395
|
+
|
|
396
|
+
_x, _y = data.get("x"), data.get("y")
|
|
397
|
+
|
|
398
|
+
_next_id = data.get("next")
|
|
399
|
+
_parent_id = data.get("parent")
|
|
400
|
+
|
|
401
|
+
_shadow = data.get("shadow", False)
|
|
402
|
+
_top_level = data.get("topLevel", _parent_id is None)
|
|
403
|
+
|
|
404
|
+
_inputs = {}
|
|
405
|
+
for _input_code, _input_data in data.get("inputs", {}).items():
|
|
406
|
+
_inputs[_input_code] = inputs.Input.from_json(_input_data)
|
|
407
|
+
|
|
408
|
+
_fields = {}
|
|
409
|
+
for _field_code, _field_data in data.get("fields", {}).items():
|
|
410
|
+
_fields[_field_code] = field.Field.from_json(_field_data)
|
|
411
|
+
|
|
412
|
+
if "mutation" in data:
|
|
413
|
+
_mutation = mutation.Mutation.from_json(data["mutation"])
|
|
414
|
+
else:
|
|
415
|
+
_mutation = None
|
|
416
|
+
|
|
417
|
+
return Block(_opcode, _shadow, _top_level, _mutation, _fields, _inputs, _x, _y, _next_id=_next_id,
|
|
418
|
+
_parent_id=_parent_id)
|
|
419
|
+
|
|
420
|
+
def to_json(self) -> dict:
|
|
421
|
+
"""
|
|
422
|
+
Convert a block to the project.json format
|
|
423
|
+
"""
|
|
424
|
+
self.check_toplevel()
|
|
425
|
+
|
|
426
|
+
_json = {
|
|
427
|
+
"opcode": self.opcode,
|
|
428
|
+
"next": self.next_id,
|
|
429
|
+
"parent": self.parent_id,
|
|
430
|
+
"inputs": {_id: _input.to_json() for _id, _input in self.inputs.items()},
|
|
431
|
+
"fields": {_id: _field.to_json() for _id, _field in self.fields.items()},
|
|
432
|
+
"shadow": self.is_shadow,
|
|
433
|
+
"topLevel": self.is_top_level,
|
|
434
|
+
}
|
|
435
|
+
_comment = self.comment
|
|
436
|
+
if _comment:
|
|
437
|
+
commons.noneless_update(_json, {
|
|
438
|
+
"comment": _comment.id
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
if self.is_top_level:
|
|
442
|
+
commons.noneless_update(_json, {
|
|
443
|
+
"x": self.x,
|
|
444
|
+
"y": self.y,
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
if self.mutation is not None:
|
|
448
|
+
commons.noneless_update(_json, {
|
|
449
|
+
"mutation": self.mutation.to_json(),
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
return _json
|
|
453
|
+
|
|
454
|
+
def link_using_sprite(self, link_subs: bool = True):
|
|
455
|
+
"""
|
|
456
|
+
Link this block to various other blocks once the sprite has been assigned
|
|
457
|
+
"""
|
|
458
|
+
if link_subs:
|
|
459
|
+
self.link_subcomponents()
|
|
460
|
+
|
|
461
|
+
if self.mutation:
|
|
462
|
+
self.mutation.link_arguments()
|
|
463
|
+
|
|
464
|
+
if self._parent_id is not None:
|
|
465
|
+
self.parent = self.sprite.find_block(self._parent_id, "id")
|
|
466
|
+
if self.parent is not None:
|
|
467
|
+
self._parent_id = None
|
|
468
|
+
|
|
469
|
+
if self._next_id is not None:
|
|
470
|
+
self.next = self.sprite.find_block(self._next_id, "id")
|
|
471
|
+
if self.next is not None:
|
|
472
|
+
self._next_id = None
|
|
473
|
+
|
|
474
|
+
for _block in self.relatives:
|
|
475
|
+
_block.sprite = self.sprite
|
|
476
|
+
|
|
477
|
+
for _field in self.fields.values():
|
|
478
|
+
if _field.id is not None:
|
|
479
|
+
new_value = self.sprite.find_vlb(_field.id, "id")
|
|
480
|
+
if new_value is None:
|
|
481
|
+
# We probably need to add a local global variable
|
|
482
|
+
_type = _field.type
|
|
483
|
+
|
|
484
|
+
if _type == field.Types.VARIABLE:
|
|
485
|
+
# Create a new variable
|
|
486
|
+
new_value = vlb.Variable(commons.gen_id(),
|
|
487
|
+
_field.value)
|
|
488
|
+
elif _type == field.Types.LIST:
|
|
489
|
+
# Create a list
|
|
490
|
+
new_value = vlb.List(commons.gen_id(),
|
|
491
|
+
_field.value)
|
|
492
|
+
elif _type == field.Types.BROADCAST:
|
|
493
|
+
# Create a broadcast
|
|
494
|
+
new_value = vlb.Broadcast(commons.gen_id(),
|
|
495
|
+
_field.value)
|
|
496
|
+
else:
|
|
497
|
+
# Something probably went wrong
|
|
498
|
+
warnings.warn(
|
|
499
|
+
f"Could not find {_field.id!r} in {self.sprite}. Can't create a new {_type} so we gave a warning")
|
|
500
|
+
|
|
501
|
+
if new_value is not None:
|
|
502
|
+
self.sprite.add_local_global(new_value)
|
|
503
|
+
|
|
504
|
+
# Check again since there may have been a newly created VLB
|
|
505
|
+
if new_value is not None:
|
|
506
|
+
_field.value = new_value
|
|
507
|
+
_field.id = None
|
|
508
|
+
|
|
509
|
+
for _input in self.inputs.values():
|
|
510
|
+
_input.link_using_block()
|
|
511
|
+
|
|
512
|
+
# Adding/removing block
|
|
513
|
+
def attach_block(self, new: Block) -> Block:
|
|
514
|
+
"""
|
|
515
|
+
Connect another block onto the boottom of this block (not necessarily bottom of chain)
|
|
516
|
+
"""
|
|
517
|
+
if not self.can_next:
|
|
518
|
+
raise exceptions.BadBlockShape(f"{self.block_shape} cannot be stacked onto")
|
|
519
|
+
elif new.block_shape.is_hat or not new.block_shape.is_stack:
|
|
520
|
+
raise exceptions.BadBlockShape(f"{new.block_shape} is not stackable")
|
|
521
|
+
|
|
522
|
+
new.parent = self
|
|
523
|
+
new.next = self.next
|
|
524
|
+
|
|
525
|
+
self.next = new
|
|
526
|
+
|
|
527
|
+
new.check_toplevel()
|
|
528
|
+
self.sprite.add_block(new)
|
|
529
|
+
|
|
530
|
+
return new
|
|
531
|
+
|
|
532
|
+
def duplicate_single_block(self) -> Block:
|
|
533
|
+
return self.attach_block(self.dcopy())
|
|
534
|
+
|
|
535
|
+
def attach_chain(self, *chain: Block) -> Block:
|
|
536
|
+
attaching_block = self
|
|
537
|
+
for _block in chain:
|
|
538
|
+
attaching_block = attaching_block.attach_block(_block)
|
|
539
|
+
|
|
540
|
+
return attaching_block
|
|
541
|
+
|
|
542
|
+
def duplicate_chain(self) -> Block:
|
|
543
|
+
return self.bottom_level_block.attach_chain(
|
|
544
|
+
*map(Block.dcopy, self.attached_chain)
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
def slot_above(self, new: Block) -> Block:
|
|
548
|
+
"""
|
|
549
|
+
Place a single block directly above this block
|
|
550
|
+
"""
|
|
551
|
+
if not new.can_next:
|
|
552
|
+
raise exceptions.BadBlockShape(f"{new.block_shape} cannot be stacked onto")
|
|
553
|
+
|
|
554
|
+
elif self.block_shape.is_hat or not self.block_shape.is_stack:
|
|
555
|
+
raise exceptions.BadBlockShape(f"{self.block_shape} is not stackable")
|
|
556
|
+
|
|
557
|
+
new.parent, new.next = self.parent, self
|
|
558
|
+
|
|
559
|
+
self.parent = new
|
|
560
|
+
|
|
561
|
+
if new.parent:
|
|
562
|
+
new.parent.next = new
|
|
563
|
+
|
|
564
|
+
return self.sprite.add_block(new)
|
|
565
|
+
|
|
566
|
+
def delete_single_block(self):
|
|
567
|
+
if self.is_next_block:
|
|
568
|
+
self.parent.next = self.next
|
|
569
|
+
|
|
570
|
+
if self.next:
|
|
571
|
+
self.next.parent = self.parent
|
|
572
|
+
|
|
573
|
+
if self.is_top_level:
|
|
574
|
+
self.next.is_top_level = True
|
|
575
|
+
self.next.x, self.next.y = self.next.x, self.next.y
|
|
576
|
+
|
|
577
|
+
self.sprite.remove_block(self)
|
|
578
|
+
|
|
579
|
+
def delete_chain(self):
|
|
580
|
+
"""
|
|
581
|
+
Delete all blocks in the attached blockchain (and self)
|
|
582
|
+
"""
|
|
583
|
+
for _block in self.attached_chain:
|
|
584
|
+
_block.delete_single_block()
|