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,609 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import warnings
|
|
5
|
+
from io import BytesIO, TextIOWrapper
|
|
6
|
+
from typing import Optional, Any, BinaryIO
|
|
7
|
+
from zipfile import ZipFile
|
|
8
|
+
from typing import Iterable, TYPE_CHECKING
|
|
9
|
+
from . import base, project, vlb, asset, comment, prim, block, commons, build_defaulting
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from . import asset
|
|
12
|
+
|
|
13
|
+
class Sprite(base.ProjectSubcomponent, base.JSONExtractable):
|
|
14
|
+
_local_globals: list[base.NamedIDComponent]
|
|
15
|
+
asset_data: list[asset.AssetFile]
|
|
16
|
+
def __init__(self, is_stage: bool = False, name: str = '', _current_costume: int = 1, _layer_order: Optional[int] = None,
|
|
17
|
+
_volume: int = 100,
|
|
18
|
+
_broadcasts: Optional[list[vlb.Broadcast]] = None,
|
|
19
|
+
_variables: Optional[list[vlb.Variable]] = None, _lists: Optional[list[vlb.List]] = None,
|
|
20
|
+
_costumes: Optional[list[asset.Costume]] = None, _sounds: Optional[list[asset.Sound]] = None,
|
|
21
|
+
_comments: Optional[list[comment.Comment]] = None, _prims: Optional[dict[str, prim.Prim]] = None,
|
|
22
|
+
_blocks: Optional[dict[str, block.Block]] = None,
|
|
23
|
+
# Stage only:
|
|
24
|
+
_tempo: int | float = 60, _video_state: str = "off", _video_transparency: int | float = 50,
|
|
25
|
+
_text_to_speech_language: str = "en", _visible: bool = True,
|
|
26
|
+
# Sprite only:
|
|
27
|
+
_x: int | float = 0, _y: int | float = 0, _size: int | float = 100, _direction: int | float = 90,
|
|
28
|
+
_draggable: bool = False, _rotation_style: str = "all around",
|
|
29
|
+
|
|
30
|
+
*, _project: Optional[project.Project] = None):
|
|
31
|
+
"""
|
|
32
|
+
Represents a sprite or the stage (known internally as a Target)
|
|
33
|
+
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets
|
|
34
|
+
"""
|
|
35
|
+
# Defaulting for list parameters
|
|
36
|
+
if _broadcasts is None:
|
|
37
|
+
_broadcasts = []
|
|
38
|
+
if _variables is None:
|
|
39
|
+
_variables = []
|
|
40
|
+
if _lists is None:
|
|
41
|
+
_lists = []
|
|
42
|
+
if _costumes is None:
|
|
43
|
+
_costumes = []
|
|
44
|
+
if _sounds is None:
|
|
45
|
+
_sounds = []
|
|
46
|
+
if _comments is None:
|
|
47
|
+
_comments = []
|
|
48
|
+
if _prims is None:
|
|
49
|
+
_prims = {}
|
|
50
|
+
if _blocks is None:
|
|
51
|
+
_blocks = {}
|
|
52
|
+
|
|
53
|
+
self.is_stage = is_stage
|
|
54
|
+
self.name = name
|
|
55
|
+
self.current_costume = _current_costume
|
|
56
|
+
self.layer_order = _layer_order
|
|
57
|
+
self.volume = _volume
|
|
58
|
+
|
|
59
|
+
self.broadcasts = _broadcasts
|
|
60
|
+
self.variables = _variables
|
|
61
|
+
self.lists = _lists
|
|
62
|
+
self._local_globals = []
|
|
63
|
+
|
|
64
|
+
self.costumes = _costumes
|
|
65
|
+
self.sounds = _sounds
|
|
66
|
+
|
|
67
|
+
self.comments = _comments
|
|
68
|
+
self.prims = _prims
|
|
69
|
+
self.blocks = _blocks
|
|
70
|
+
|
|
71
|
+
self.tempo = _tempo
|
|
72
|
+
self.video_state = _video_state
|
|
73
|
+
self.video_transparency = _video_transparency
|
|
74
|
+
self.text_to_speech_language = _text_to_speech_language
|
|
75
|
+
self.visible = _visible
|
|
76
|
+
self.x, self.y = _x, _y
|
|
77
|
+
self.size = _size
|
|
78
|
+
self.direction = _direction
|
|
79
|
+
self.draggable = _draggable
|
|
80
|
+
self.rotation_style = _rotation_style
|
|
81
|
+
|
|
82
|
+
self.asset_data = []
|
|
83
|
+
|
|
84
|
+
super().__init__(_project)
|
|
85
|
+
|
|
86
|
+
# Assign sprite
|
|
87
|
+
for iterable in (self.vlbs, self.comments, self.assets, self.prims.values(), self.blocks.values()):
|
|
88
|
+
for sub_component in iterable:
|
|
89
|
+
sub_component.sprite = self
|
|
90
|
+
|
|
91
|
+
def __repr__(self):
|
|
92
|
+
return f"Sprite<{self.name}>"
|
|
93
|
+
|
|
94
|
+
def __enter__(self):
|
|
95
|
+
build_defaulting.stack_add_sprite(self)
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
99
|
+
build_defaulting.pop_sprite(self)
|
|
100
|
+
|
|
101
|
+
def link_subcomponents(self):
|
|
102
|
+
self.link_prims()
|
|
103
|
+
self.link_blocks()
|
|
104
|
+
self.link_comments()
|
|
105
|
+
self.link_vlbs()
|
|
106
|
+
|
|
107
|
+
def link_vlbs(self):
|
|
108
|
+
for _vlb in self.vlbs:
|
|
109
|
+
_vlb.sprite = self
|
|
110
|
+
|
|
111
|
+
def link_prims(self):
|
|
112
|
+
"""
|
|
113
|
+
Link primitives to corresponding VLB objects (requires project attribute)
|
|
114
|
+
"""
|
|
115
|
+
for _prim in self.prims.values():
|
|
116
|
+
_prim.link_using_sprite()
|
|
117
|
+
|
|
118
|
+
def link_blocks(self):
|
|
119
|
+
"""
|
|
120
|
+
Link blocks to sprite/to other blocks
|
|
121
|
+
"""
|
|
122
|
+
for _block_id, _block in self.blocks.items():
|
|
123
|
+
_block.link_using_sprite()
|
|
124
|
+
|
|
125
|
+
def link_comments(self):
|
|
126
|
+
for _comment in self.comments:
|
|
127
|
+
_comment.link_using_sprite()
|
|
128
|
+
|
|
129
|
+
def add_local_global(self, _vlb: base.NamedIDComponent):
|
|
130
|
+
"""
|
|
131
|
+
Add a global variable/list to this sprite (for when an overarching project/stage is not available)
|
|
132
|
+
"""
|
|
133
|
+
self._local_globals.append(_vlb)
|
|
134
|
+
_vlb.sprite = self
|
|
135
|
+
|
|
136
|
+
def add_variable(self, _variable: vlb.Variable):
|
|
137
|
+
self.variables.append(_variable)
|
|
138
|
+
_variable.sprite = self
|
|
139
|
+
|
|
140
|
+
def add_list(self, _list: vlb.List):
|
|
141
|
+
self.lists.append(_list)
|
|
142
|
+
_list.sprite = self
|
|
143
|
+
|
|
144
|
+
def add_broadcast(self, _broadcast: vlb.Broadcast):
|
|
145
|
+
self.broadcasts.append(_broadcast)
|
|
146
|
+
_broadcast.sprite = self
|
|
147
|
+
|
|
148
|
+
def add_vlb(self, _vlb: base.NamedIDComponent):
|
|
149
|
+
if isinstance(_vlb, vlb.Variable):
|
|
150
|
+
self.add_variable(_vlb)
|
|
151
|
+
elif isinstance(_vlb, vlb.List):
|
|
152
|
+
self.add_list(_vlb)
|
|
153
|
+
elif isinstance(_vlb, vlb.Broadcast):
|
|
154
|
+
self.add_broadcast(_vlb)
|
|
155
|
+
else:
|
|
156
|
+
warnings.warn(f"Invalid 'VLB' {_vlb} of type: {type(_vlb)}")
|
|
157
|
+
|
|
158
|
+
def add_block(self, _block: block.Block | prim.Prim) -> block.Block | prim.Prim:
|
|
159
|
+
if _block.sprite is self:
|
|
160
|
+
if _block in self.blocks.values():
|
|
161
|
+
return _block
|
|
162
|
+
|
|
163
|
+
_block.sprite = self
|
|
164
|
+
new_id = self.new_id
|
|
165
|
+
|
|
166
|
+
if isinstance(_block, block.Block):
|
|
167
|
+
self.blocks[new_id] = _block
|
|
168
|
+
_block.id = new_id
|
|
169
|
+
_block.link_using_sprite()
|
|
170
|
+
|
|
171
|
+
elif isinstance(_block, prim.Prim):
|
|
172
|
+
self.prims[new_id] = _block
|
|
173
|
+
_block.link_using_sprite()
|
|
174
|
+
|
|
175
|
+
return _block
|
|
176
|
+
|
|
177
|
+
def add_chain(self, *chain: block.Block | prim.Prim) -> block.Block | prim.Prim:
|
|
178
|
+
"""
|
|
179
|
+
Adds a list of blocks to the sprite **AND RETURNS THE FIRST BLOCK**
|
|
180
|
+
:param chain:
|
|
181
|
+
:return:
|
|
182
|
+
"""
|
|
183
|
+
chain = tuple(chain)
|
|
184
|
+
|
|
185
|
+
_prev = self.add_block(chain[0])
|
|
186
|
+
|
|
187
|
+
for _block in chain[1:]:
|
|
188
|
+
if not isinstance(_prev, block.Block) or not isinstance(_block, block.Block):
|
|
189
|
+
continue
|
|
190
|
+
_prev = _prev.attach_block(_block)
|
|
191
|
+
|
|
192
|
+
return chain[0]
|
|
193
|
+
|
|
194
|
+
def add_comment(self, _comment: comment.Comment) -> comment.Comment:
|
|
195
|
+
_comment.sprite = self
|
|
196
|
+
if _comment.id is None:
|
|
197
|
+
_comment.id = self.new_id
|
|
198
|
+
|
|
199
|
+
self.comments.append(_comment)
|
|
200
|
+
_comment.link_using_sprite()
|
|
201
|
+
|
|
202
|
+
return _comment
|
|
203
|
+
|
|
204
|
+
def remove_block(self, _block: block.Block):
|
|
205
|
+
for key, val in self.blocks.items():
|
|
206
|
+
if val is _block:
|
|
207
|
+
del self.blocks[key]
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def folder(self):
|
|
212
|
+
return commons.get_folder_name(self.name)
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def name_nfldr(self):
|
|
216
|
+
return commons.get_name_nofldr(self.name)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def vlbs(self) -> list[base.NamedIDComponent]:
|
|
220
|
+
"""
|
|
221
|
+
:return: All vlbs associated with the sprite. No local globals are added
|
|
222
|
+
"""
|
|
223
|
+
vlbs: list[base.NamedIDComponent] = []
|
|
224
|
+
vlbs.extend(self.variables)
|
|
225
|
+
vlbs.extend(self.lists)
|
|
226
|
+
vlbs.extend(self.broadcasts)
|
|
227
|
+
return vlbs
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def assets(self) -> list[asset.Costume | asset.Sound]:
|
|
231
|
+
return self.costumes + self.sounds
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def stage(self) -> Sprite:
|
|
235
|
+
return self.project.stage
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def new_id(self):
|
|
239
|
+
return commons.gen_id()
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def from_json(data: dict):
|
|
243
|
+
_is_stage = data["isStage"]
|
|
244
|
+
_name = data["name"]
|
|
245
|
+
_current_costume = data.get("currentCostume", 1)
|
|
246
|
+
_layer_order = data.get("layerOrder", 1)
|
|
247
|
+
_volume = data.get("volume", 100)
|
|
248
|
+
|
|
249
|
+
# Read VLB
|
|
250
|
+
def read_idcomponent(attr_name: str, cls: type[base.IDComponent]):
|
|
251
|
+
_vlbs = []
|
|
252
|
+
for _vlb_id, _vlb_data in data.get(attr_name, {}).items():
|
|
253
|
+
_vlbs.append(cls.from_json((_vlb_id, _vlb_data)))
|
|
254
|
+
return _vlbs
|
|
255
|
+
|
|
256
|
+
_variables = read_idcomponent("variables", vlb.Variable)
|
|
257
|
+
_lists = read_idcomponent("lists", vlb.List)
|
|
258
|
+
_broadcasts = read_idcomponent("broadcasts", vlb.Broadcast)
|
|
259
|
+
|
|
260
|
+
# Read assets
|
|
261
|
+
_costumes = []
|
|
262
|
+
for _costume_data in data.get("costumes", []):
|
|
263
|
+
_costumes.append(asset.Costume.from_json(_costume_data))
|
|
264
|
+
_sounds = []
|
|
265
|
+
for _sound_data in data.get("sounds", []):
|
|
266
|
+
_sounds.append(asset.Sound.from_json(_sound_data))
|
|
267
|
+
|
|
268
|
+
# Read comments
|
|
269
|
+
_comments = read_idcomponent("comments", comment.Comment)
|
|
270
|
+
|
|
271
|
+
# Read blocks/prims
|
|
272
|
+
_prims = {}
|
|
273
|
+
_blocks = {}
|
|
274
|
+
for _block_id, _block_data in data.get("blocks", {}).items():
|
|
275
|
+
if isinstance(_block_data, list):
|
|
276
|
+
# Prim
|
|
277
|
+
_prims[_block_id] = prim.Prim.from_json(_block_data)
|
|
278
|
+
else:
|
|
279
|
+
# Block
|
|
280
|
+
_blocks[_block_id] = block.Block.from_json(_block_data)
|
|
281
|
+
|
|
282
|
+
# Stage/sprite specific vars
|
|
283
|
+
_tempo, _video_state, _video_transparency, _text_to_speech_language = (None,) * 4
|
|
284
|
+
_visible, _x, _y, _size, _direction, _draggable, _rotation_style = (None,) * 7
|
|
285
|
+
if _is_stage:
|
|
286
|
+
_tempo = data.get("tempo", 0)
|
|
287
|
+
_video_state = data.get("videoState", "off")
|
|
288
|
+
_video_transparency = data.get("videoTransparency", 0)
|
|
289
|
+
_text_to_speech_language = data.get("textToSpeechLanguage", "en")
|
|
290
|
+
else:
|
|
291
|
+
_visible = data["visible"]
|
|
292
|
+
_x = data.get("x", 0)
|
|
293
|
+
_y = data.get("y", 0)
|
|
294
|
+
_size = data.get("size", 100)
|
|
295
|
+
_direction = data.get("direction", 90)
|
|
296
|
+
_draggable = data.get("draggable", False)
|
|
297
|
+
_rotation_style = data.get("rotationStyle", "all-around")
|
|
298
|
+
|
|
299
|
+
return Sprite(_is_stage, _name, _current_costume, _layer_order, _volume, _broadcasts, _variables, _lists,
|
|
300
|
+
_costumes,
|
|
301
|
+
_sounds, _comments, _prims, _blocks,
|
|
302
|
+
|
|
303
|
+
_tempo, _video_state, _video_transparency, _text_to_speech_language,
|
|
304
|
+
_visible, _x, _y, _size, _direction, _draggable, _rotation_style
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def to_json(self) -> dict:
|
|
308
|
+
_json = {
|
|
309
|
+
"isStage": self.is_stage,
|
|
310
|
+
"name": self.name,
|
|
311
|
+
"currentCostume": self.current_costume,
|
|
312
|
+
"volume": self.volume,
|
|
313
|
+
"layerOrder": self.layer_order,
|
|
314
|
+
|
|
315
|
+
"variables": {_variable.id: _variable.to_json() for _variable in self.variables},
|
|
316
|
+
"lists": {_list.id: _list.to_json() for _list in self.lists},
|
|
317
|
+
"broadcasts": {_broadcast.id: _broadcast.to_json() for _broadcast in self.broadcasts},
|
|
318
|
+
|
|
319
|
+
"blocks": {_block_id: _block.to_json() for _block_id, _block in (self.blocks | self.prims).items()},
|
|
320
|
+
"comments": {_comment.id: _comment.to_json() for _comment in self.comments},
|
|
321
|
+
|
|
322
|
+
"costumes": [_costume.to_json() for _costume in self.costumes],
|
|
323
|
+
"sounds": [_sound.to_json() for _sound in self.sounds]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if self.is_stage:
|
|
327
|
+
_json.update({
|
|
328
|
+
"tempo": self.tempo,
|
|
329
|
+
"videoTransparency": self.video_transparency,
|
|
330
|
+
"videoState": self.video_state,
|
|
331
|
+
"textToSpeechLanguage": self.text_to_speech_language
|
|
332
|
+
})
|
|
333
|
+
else:
|
|
334
|
+
_json.update({
|
|
335
|
+
"visible": self.visible,
|
|
336
|
+
|
|
337
|
+
"x": self.x, "y": self.y,
|
|
338
|
+
"size": self.size,
|
|
339
|
+
"direction": self.direction,
|
|
340
|
+
|
|
341
|
+
"draggable": self.draggable,
|
|
342
|
+
"rotationStyle": self.rotation_style
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
return _json
|
|
346
|
+
|
|
347
|
+
# Finding/getting from list/dict attributes
|
|
348
|
+
def find_asset(self, value: str, by: str = "name", multiple: bool = False, a_type: Optional[type]=None) -> None | asset.Asset | asset.Sound | asset.Costume | list[asset.Asset | asset.Sound | asset.Costume]:
|
|
349
|
+
if a_type is None:
|
|
350
|
+
a_type = asset.Asset
|
|
351
|
+
|
|
352
|
+
_ret = []
|
|
353
|
+
by = by.lower()
|
|
354
|
+
for _asset in self.assets:
|
|
355
|
+
if not isinstance(_asset, a_type):
|
|
356
|
+
continue
|
|
357
|
+
|
|
358
|
+
# Defaulting
|
|
359
|
+
compare = getattr(_asset, by)
|
|
360
|
+
|
|
361
|
+
if compare == value:
|
|
362
|
+
if multiple:
|
|
363
|
+
_ret.append(_asset)
|
|
364
|
+
else:
|
|
365
|
+
return _asset
|
|
366
|
+
|
|
367
|
+
if multiple:
|
|
368
|
+
return _ret
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
def find_variable(self, value: str, by: str = "name", multiple: bool = False) -> None | vlb.Variable | list[vlb.Variable]:
|
|
372
|
+
_ret = []
|
|
373
|
+
by = by.lower()
|
|
374
|
+
for _variable in self.variables + self._local_globals:
|
|
375
|
+
if not isinstance(_variable, vlb.Variable):
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
if by == "id":
|
|
379
|
+
compare = _variable.id
|
|
380
|
+
else:
|
|
381
|
+
# Defaulting
|
|
382
|
+
compare = _variable.name
|
|
383
|
+
if compare == value:
|
|
384
|
+
if multiple:
|
|
385
|
+
_ret.append(_variable)
|
|
386
|
+
else:
|
|
387
|
+
return _variable
|
|
388
|
+
# Search in stage for global variables
|
|
389
|
+
if self.project:
|
|
390
|
+
if not self.is_stage:
|
|
391
|
+
if multiple:
|
|
392
|
+
_ret += self.stage.find_variable(value, by, True)
|
|
393
|
+
else:
|
|
394
|
+
return self.stage.find_variable(value, by)
|
|
395
|
+
|
|
396
|
+
if multiple:
|
|
397
|
+
return _ret
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
def find_list(self, value: str, by: str = "name", multiple: bool = False) -> None | vlb.List | list[vlb.List]:
|
|
401
|
+
_ret = []
|
|
402
|
+
by = by.lower()
|
|
403
|
+
for _list in self.lists + self._local_globals:
|
|
404
|
+
if not isinstance(_list, vlb.List):
|
|
405
|
+
continue
|
|
406
|
+
if by == "id":
|
|
407
|
+
compare = _list.id
|
|
408
|
+
else:
|
|
409
|
+
# Defaulting
|
|
410
|
+
compare = _list.name
|
|
411
|
+
if compare == value:
|
|
412
|
+
if multiple:
|
|
413
|
+
_ret.append(_list)
|
|
414
|
+
else:
|
|
415
|
+
return _list
|
|
416
|
+
# Search in stage for global lists
|
|
417
|
+
if self.project:
|
|
418
|
+
if not self.is_stage:
|
|
419
|
+
if multiple:
|
|
420
|
+
_ret += self.stage.find_list(value, by, True)
|
|
421
|
+
else:
|
|
422
|
+
return self.stage.find_list(value, by)
|
|
423
|
+
|
|
424
|
+
if multiple:
|
|
425
|
+
return _ret
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
def find_broadcast(self, value: str, by: str = "name", multiple: bool = False) -> None | vlb.Broadcast | list[
|
|
429
|
+
vlb.Broadcast]:
|
|
430
|
+
_ret = []
|
|
431
|
+
by = by.lower()
|
|
432
|
+
for _broadcast in self.broadcasts + self._local_globals:
|
|
433
|
+
if not isinstance(_broadcast, vlb.Broadcast):
|
|
434
|
+
continue
|
|
435
|
+
if by == "id":
|
|
436
|
+
compare = _broadcast.id
|
|
437
|
+
else:
|
|
438
|
+
# Defaulting
|
|
439
|
+
compare = _broadcast.name
|
|
440
|
+
if compare == value:
|
|
441
|
+
if multiple:
|
|
442
|
+
_ret.append(_broadcast)
|
|
443
|
+
else:
|
|
444
|
+
return _broadcast
|
|
445
|
+
# Search in stage for global broadcasts
|
|
446
|
+
if self.project:
|
|
447
|
+
if not self.is_stage:
|
|
448
|
+
if multiple:
|
|
449
|
+
_ret += self.stage.find_broadcast(value, by, True)
|
|
450
|
+
else:
|
|
451
|
+
return self.stage.find_broadcast(value, by)
|
|
452
|
+
|
|
453
|
+
if multiple:
|
|
454
|
+
return _ret
|
|
455
|
+
return None
|
|
456
|
+
|
|
457
|
+
def find_vlb(self, value: str, by: str = "name",
|
|
458
|
+
multiple: bool = False) -> vlb.Variable | vlb.List | vlb.Broadcast | list[
|
|
459
|
+
vlb.Variable | vlb.List | vlb.Broadcast]:
|
|
460
|
+
if multiple:
|
|
461
|
+
return self.find_variable(value, by, True) + \
|
|
462
|
+
self.find_list(value, by, True) + \
|
|
463
|
+
self.find_broadcast(value, by, True)
|
|
464
|
+
else:
|
|
465
|
+
_ret = self.find_variable(value, by)
|
|
466
|
+
if _ret is not None:
|
|
467
|
+
return _ret
|
|
468
|
+
_ret = self.find_list(value, by)
|
|
469
|
+
if _ret is not None:
|
|
470
|
+
return _ret
|
|
471
|
+
return self.find_broadcast(value, by)
|
|
472
|
+
|
|
473
|
+
def find_block(self, value: str | Any, by: str, multiple: bool = False) -> None | block.Block | prim.Prim | list[
|
|
474
|
+
block.Block | prim.Prim]:
|
|
475
|
+
_ret = []
|
|
476
|
+
by = by.lower()
|
|
477
|
+
for _block_id, _block in (self.blocks | self.prims).items():
|
|
478
|
+
_block: block.Block | prim.Prim
|
|
479
|
+
|
|
480
|
+
is_block = isinstance(_block, block.Block)
|
|
481
|
+
is_prim = isinstance(_block, prim.Prim)
|
|
482
|
+
|
|
483
|
+
compare = None
|
|
484
|
+
if by == "id":
|
|
485
|
+
compare = _block_id
|
|
486
|
+
elif by == "argument ids":
|
|
487
|
+
if is_prim:
|
|
488
|
+
continue
|
|
489
|
+
|
|
490
|
+
if _block.mutation is not None:
|
|
491
|
+
compare = _block.mutation.argument_ids
|
|
492
|
+
elif by == "opcode":
|
|
493
|
+
if is_prim:
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
# Defaulting
|
|
497
|
+
compare = _block.opcode
|
|
498
|
+
else:
|
|
499
|
+
if is_block:
|
|
500
|
+
compare = _block.opcode
|
|
501
|
+
else:
|
|
502
|
+
compare = _block.value
|
|
503
|
+
|
|
504
|
+
if compare == value:
|
|
505
|
+
if multiple:
|
|
506
|
+
_ret.append(_block)
|
|
507
|
+
else:
|
|
508
|
+
return _block
|
|
509
|
+
# Search in stage for global variables
|
|
510
|
+
if self.project:
|
|
511
|
+
if not self.is_stage:
|
|
512
|
+
if multiple:
|
|
513
|
+
_ret += self.stage.find_block(value, by, True)
|
|
514
|
+
else:
|
|
515
|
+
return self.stage.find_block(value, by)
|
|
516
|
+
|
|
517
|
+
if multiple:
|
|
518
|
+
return _ret
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
def export(self, fp: Optional[str] = None, *, export_as_zip: bool = True):
|
|
522
|
+
if fp is None:
|
|
523
|
+
fp = commons.sanitize_fn(f"{self.name}.sprite3")
|
|
524
|
+
|
|
525
|
+
data = self.to_json()
|
|
526
|
+
|
|
527
|
+
if export_as_zip:
|
|
528
|
+
with ZipFile(fp, "w") as archive:
|
|
529
|
+
for _asset in self.assets:
|
|
530
|
+
asset_file = _asset.asset_file
|
|
531
|
+
if asset_file.filename not in archive.namelist():
|
|
532
|
+
archive.writestr(asset_file.filename, asset_file.data)
|
|
533
|
+
|
|
534
|
+
archive.writestr("sprite.json", json.dumps(data))
|
|
535
|
+
else:
|
|
536
|
+
with open(fp, "w") as json_file:
|
|
537
|
+
json.dump(data, json_file)
|
|
538
|
+
|
|
539
|
+
@property
|
|
540
|
+
def all_ids(self):
|
|
541
|
+
ret = []
|
|
542
|
+
for _vlb in self.vlbs + self._local_globals:
|
|
543
|
+
ret.append(_vlb.id)
|
|
544
|
+
for iterator in self.blocks.keys(), self.prims.keys():
|
|
545
|
+
ret += list(iterator)
|
|
546
|
+
|
|
547
|
+
return ret
|
|
548
|
+
@staticmethod
|
|
549
|
+
def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None):
|
|
550
|
+
_dir_for_name = None
|
|
551
|
+
|
|
552
|
+
if _name is None:
|
|
553
|
+
if hasattr(data, "name"):
|
|
554
|
+
_dir_for_name = data.name
|
|
555
|
+
|
|
556
|
+
if not isinstance(_name, str) and _name is not None:
|
|
557
|
+
_name = str(_name)
|
|
558
|
+
|
|
559
|
+
if isinstance(data, bytes):
|
|
560
|
+
data = BytesIO(data)
|
|
561
|
+
|
|
562
|
+
elif isinstance(data, str):
|
|
563
|
+
_dir_for_name = data
|
|
564
|
+
data = open(data, "rb")
|
|
565
|
+
|
|
566
|
+
if _name is None and _dir_for_name is not None:
|
|
567
|
+
# Remove any directory names and the file extension
|
|
568
|
+
_name = _dir_for_name.split('/')[-1]
|
|
569
|
+
_name = '.'.join(_name.split('.')[:-1])
|
|
570
|
+
|
|
571
|
+
asset_data = []
|
|
572
|
+
with data:
|
|
573
|
+
# For if the sprite3 is just JSON (e.g. if it's exported from scratchattach)
|
|
574
|
+
if commons.is_valid_json(data):
|
|
575
|
+
json_str = data
|
|
576
|
+
|
|
577
|
+
else:
|
|
578
|
+
with ZipFile(data) as archive:
|
|
579
|
+
json_str = archive.read("sprite.json")
|
|
580
|
+
|
|
581
|
+
# Also load assets
|
|
582
|
+
if load_assets:
|
|
583
|
+
|
|
584
|
+
for filename in archive.namelist():
|
|
585
|
+
if filename != "sprite.json":
|
|
586
|
+
md5_hash = filename.split('.')[0]
|
|
587
|
+
|
|
588
|
+
asset_data.append(
|
|
589
|
+
asset.AssetFile(filename, archive.read(filename), md5_hash)
|
|
590
|
+
)
|
|
591
|
+
else:
|
|
592
|
+
warnings.warn(
|
|
593
|
+
"Loading sb3 without loading assets. When exporting the project, there may be errors due to assets not being uploaded to the Scratch website")
|
|
594
|
+
|
|
595
|
+
return _name, asset_data, json_str
|
|
596
|
+
|
|
597
|
+
@classmethod
|
|
598
|
+
def from_sprite3(cls, data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None):
|
|
599
|
+
"""
|
|
600
|
+
Load a project from an .sb3 file/bytes/file path
|
|
601
|
+
"""
|
|
602
|
+
_name, asset_data, json_str = cls.load_json(data, load_assets, _name)
|
|
603
|
+
data = json.loads(json_str)
|
|
604
|
+
|
|
605
|
+
sprite = cls.from_json(data)
|
|
606
|
+
# Sprites already have names, so we probably don't want to set it
|
|
607
|
+
# sprite.name = _name
|
|
608
|
+
sprite.asset_data = asset_data
|
|
609
|
+
return sprite
|