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