koro 1.1.6__py3-none-any.whl → 2.0.0rc2__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.
koro/slot/xml.py ADDED
@@ -0,0 +1,783 @@
1
+ # mypy: disable-error-code=union-attr
2
+
3
+ from collections.abc import Iterator, Sequence
4
+ from io import StringIO
5
+ from itertools import chain
6
+ from typing import Final
7
+ from xml.etree.ElementTree import Element, fromstring
8
+
9
+ from ..stage import EditUser, Stage, Theme
10
+ from ..stage.model import DecorationModel, DeviceModel, PartModel
11
+ from ..stage.part import (
12
+ Ant,
13
+ BasePart,
14
+ BlinkingTile,
15
+ Bumper,
16
+ Cannon,
17
+ ConveyorBelt,
18
+ DashTunnel,
19
+ Drawbridge,
20
+ Fan,
21
+ FixedSpeedDevice,
22
+ Gear,
23
+ Goal,
24
+ GreenCrystal,
25
+ KororinCapsule,
26
+ Magnet,
27
+ MagnetSegment,
28
+ MagnifyingGlass,
29
+ MelodyTile,
30
+ MovementTiming,
31
+ MovingCurve,
32
+ MovingTile,
33
+ Part,
34
+ Press,
35
+ ProgressMarker,
36
+ Punch,
37
+ Scissors,
38
+ SeesawBlock,
39
+ SizeTunnel,
40
+ SlidingTile,
41
+ Speed,
42
+ Spring,
43
+ Start,
44
+ Thorn,
45
+ TimedDevice,
46
+ ToyTrain,
47
+ TrainTrack,
48
+ Turntable,
49
+ UpsideDownBall,
50
+ UpsideDownStageDevice,
51
+ Walls,
52
+ Warp,
53
+ )
54
+ from .file import FileSlot
55
+
56
+ __all__ = ["XmlSlot"]
57
+
58
+
59
+ class XmlSlot(FileSlot):
60
+ __slots__ = ()
61
+
62
+ @staticmethod
63
+ def deserialize(data: bytes) -> Stage:
64
+ """Behavior is undefined when passed invalid stage data"""
65
+
66
+ def get_values(element: Element, /, tag: str) -> Sequence[str]:
67
+ return element.find(tag).text.strip().split()
68
+
69
+ def get_pos_rot(element: Element, /) -> Iterator[float]:
70
+ return chain(
71
+ map(float, get_values(element, "pos")),
72
+ map(float, get_values(element, "rot")),
73
+ )
74
+
75
+ root: Final[Element] = fromstring(
76
+ data.decode("shift_jis", "xmlcharrefreplace").replace(
77
+ '<?xml version="1.0" encoding="SHIFT_JIS"?>',
78
+ '<?xml version="1.0"?><body>',
79
+ )
80
+ + "</body>"
81
+ )
82
+ editinfo: Final[Element] = root.find("EDITINFO") # type: ignore[assignment]
83
+ output: Final[Stage] = Stage(
84
+ (),
85
+ edit_user=EditUser(int(editinfo.find("EDITUSER").text.strip())),
86
+ theme=Theme(int(editinfo.find("THEME").text.strip())),
87
+ tilt_lock=bool(int(editinfo.find("LOCK").text.strip())),
88
+ )
89
+ groups: Final[dict[int, dict[int, Element]]] = {}
90
+ for elem in root.find("STAGEDATA"):
91
+ match elem.tag:
92
+ case "EDIT_LIGHT" | "EDIT_BG_NORMAL":
93
+ continue
94
+ case "EDIT_MAP_NORMAL":
95
+ output.add(
96
+ Part(
97
+ *get_pos_rot(elem),
98
+ shape=PartModel(int(get_values(elem, "model")[1])),
99
+ )
100
+ )
101
+ case "EDIT_MAP_EXT":
102
+ output.add(
103
+ Part(
104
+ *get_pos_rot(elem),
105
+ shape=DecorationModel(int(get_values(elem, "model")[1])),
106
+ )
107
+ )
108
+ case "EDIT_GIM_START":
109
+ output.add(Start(*get_pos_rot(elem)))
110
+ case "EDIT_GIM_GOAL":
111
+ output.add(Goal(*get_pos_rot(elem)))
112
+ case "EDIT_GIM_NORMAL":
113
+ if (
114
+ elem.find("group") is None
115
+ or elem.find("group").text != " 0 -1 "
116
+ ):
117
+ match DeviceModel(int(get_values(elem, "model")[1])):
118
+ case DeviceModel.Crystal:
119
+ output.add(
120
+ ProgressMarker(
121
+ *get_pos_rot(elem),
122
+ progress=int(get_values(elem, "hook")[0]) * 2 + 1, # type: ignore[arg-type]
123
+ )
124
+ )
125
+ case DeviceModel.Respawn:
126
+ output.add(
127
+ ProgressMarker(
128
+ *get_pos_rot(elem),
129
+ progress=int(get_values(elem, "hook")[0]) * 2 + 2, # type: ignore[arg-type]
130
+ )
131
+ )
132
+ case DeviceModel.MovingTile10x10:
133
+ output.add(
134
+ MovingTile(
135
+ *get_pos_rot(elem),
136
+ **dict(
137
+ zip(
138
+ ("dest_x", "dest_y", "dest_z"),
139
+ map(float, get_values(elem, "anmmov1")),
140
+ )
141
+ ), # type: ignore[arg-type]
142
+ shape=PartModel.Tile10x10,
143
+ speed=float(get_values(elem, "anmspeed")[0]),
144
+ )
145
+ )
146
+ case DeviceModel.MovingTile20x20:
147
+ output.add(
148
+ MovingTile(
149
+ *get_pos_rot(elem),
150
+ **dict(
151
+ zip(
152
+ ("dest_x", "dest_y", "dest_z"),
153
+ map(float, get_values(elem, "anmmov1")),
154
+ )
155
+ ), # type: ignore[arg-type]
156
+ shape=PartModel.Tile20x20,
157
+ speed=float(get_values(elem, "anmspeed")[0]),
158
+ walls=Walls(int(get_values(elem, "hook")[1])),
159
+ )
160
+ )
161
+ case DeviceModel.MovingTile30x30:
162
+ output.add(
163
+ MovingTile(
164
+ *get_pos_rot(elem),
165
+ **dict(
166
+ zip(
167
+ ("dest_x", "dest_y", "dest_z"),
168
+ map(float, get_values(elem, "anmmov1")),
169
+ )
170
+ ), # type: ignore[arg-type]
171
+ shape=PartModel.TileA30x30,
172
+ speed=float(get_values(elem, "anmspeed")[0]),
173
+ walls=Walls(int(get_values(elem, "hook")[1])),
174
+ )
175
+ )
176
+ case DeviceModel.MovingTile90x90A:
177
+ output.add(
178
+ MovingTile(
179
+ *get_pos_rot(elem),
180
+ **dict(
181
+ zip(
182
+ ("dest_x", "dest_y", "dest_z"),
183
+ map(float, get_values(elem, "anmmov1")),
184
+ )
185
+ ), # type: ignore[arg-type]
186
+ shape=PartModel.Tile90x90,
187
+ speed=float(get_values(elem, "anmspeed")[0]),
188
+ )
189
+ )
190
+ case DeviceModel.MovingTile90x90B:
191
+ output.add(
192
+ MovingTile(
193
+ *get_pos_rot(elem),
194
+ **dict(
195
+ zip(
196
+ ("dest_x", "dest_y", "dest_z"),
197
+ map(float, get_values(elem, "anmmov1")),
198
+ )
199
+ ), # type: ignore[arg-type]
200
+ shape=PartModel.HoleB90x90,
201
+ speed=float(get_values(elem, "anmspeed")[0]),
202
+ )
203
+ )
204
+ case DeviceModel.MovingTile10x10Switch:
205
+ output.add(
206
+ MovingTile(
207
+ *get_pos_rot(elem),
208
+ **dict(
209
+ zip(
210
+ ("dest_x", "dest_y", "dest_z"),
211
+ map(float, get_values(elem, "anmmov1")),
212
+ )
213
+ ), # type: ignore[arg-type]
214
+ shape=PartModel.Tile10x10,
215
+ speed=float(get_values(elem, "anmspeed")[0]),
216
+ switch=True,
217
+ )
218
+ )
219
+ case DeviceModel.MovingTile20x20Switch:
220
+ output.add(
221
+ MovingTile(
222
+ *get_pos_rot(elem),
223
+ **dict(
224
+ zip(
225
+ ("dest_x", "dest_y", "dest_z"),
226
+ map(float, get_values(elem, "anmmov1")),
227
+ )
228
+ ), # type: ignore[arg-type]
229
+ shape=PartModel.Tile20x20,
230
+ speed=float(get_values(elem, "anmspeed")[0]),
231
+ switch=True,
232
+ walls=Walls(int(get_values(elem, "hook")[1])),
233
+ )
234
+ )
235
+ case DeviceModel.MovingTile30x30Switch:
236
+ output.add(
237
+ MovingTile(
238
+ *get_pos_rot(elem),
239
+ **dict(
240
+ zip(
241
+ ("dest_x", "dest_y", "dest_z"),
242
+ map(float, get_values(elem, "anmmov1")),
243
+ )
244
+ ), # type: ignore[arg-type]
245
+ shape=PartModel.TileA30x30,
246
+ speed=float(get_values(elem, "anmspeed")[0]),
247
+ switch=True,
248
+ walls=Walls(int(get_values(elem, "hook")[1])),
249
+ )
250
+ )
251
+ case DeviceModel.MovingTile90x90ASwitch:
252
+ output.add(
253
+ MovingTile(
254
+ *get_pos_rot(elem),
255
+ **dict(
256
+ zip(
257
+ ("dest_x", "dest_y", "dest_z"),
258
+ map(float, get_values(elem, "anmmov1")),
259
+ )
260
+ ), # type: ignore[arg-type]
261
+ shape=PartModel.Tile90x90,
262
+ speed=float(get_values(elem, "anmspeed")[0]),
263
+ switch=True,
264
+ )
265
+ )
266
+ case DeviceModel.MovingTile90x90BSwitch:
267
+ output.add(
268
+ MovingTile(
269
+ *get_pos_rot(elem),
270
+ **dict(
271
+ zip(
272
+ ("dest_x", "dest_y", "dest_z"),
273
+ map(float, get_values(elem, "anmmov1")),
274
+ )
275
+ ), # type: ignore[arg-type]
276
+ shape=PartModel.HoleB90x90,
277
+ speed=float(get_values(elem, "anmspeed")[0]),
278
+ switch=True,
279
+ )
280
+ )
281
+ case DeviceModel.MovingFunnelPipe:
282
+ output.add(
283
+ MovingTile(
284
+ *get_pos_rot(elem),
285
+ **dict(
286
+ zip(
287
+ ("dest_x", "dest_y", "dest_z"),
288
+ map(float, get_values(elem, "anmmov1")),
289
+ )
290
+ ), # type: ignore[arg-type]
291
+ shape=PartModel.FunnelPipe,
292
+ speed=float(get_values(elem, "anmspeed")[0]),
293
+ )
294
+ )
295
+ case DeviceModel.MovingStraightPipe:
296
+ output.add(
297
+ MovingTile(
298
+ *get_pos_rot(elem),
299
+ **dict(
300
+ zip(
301
+ ("dest_x", "dest_y", "dest_z"),
302
+ map(float, get_values(elem, "anmmov1")),
303
+ )
304
+ ), # type: ignore[arg-type]
305
+ shape=PartModel.StraightPipe,
306
+ speed=float(get_values(elem, "anmspeed")[0]),
307
+ )
308
+ )
309
+ case DeviceModel.MovingCurveS:
310
+ output.add(
311
+ MovingCurve(
312
+ *get_pos_rot(elem),
313
+ shape=PartModel.CurveS,
314
+ speed=Speed(int(get_values(elem, "sts")[0])),
315
+ )
316
+ )
317
+ case DeviceModel.MovingCurveM:
318
+ output.add(
319
+ MovingCurve(
320
+ *get_pos_rot(elem),
321
+ shape=PartModel.CurveM,
322
+ speed=Speed(int(get_values(elem, "sts")[0])),
323
+ )
324
+ )
325
+ case DeviceModel.MovingCurveL:
326
+ output.add(
327
+ MovingCurve(
328
+ *get_pos_rot(elem),
329
+ shape=PartModel.CurveL,
330
+ speed=Speed(int(get_values(elem, "sts")[0])),
331
+ )
332
+ )
333
+ case DeviceModel.SlidingTile:
334
+ output.add(SlidingTile(*get_pos_rot(elem)))
335
+ case DeviceModel.ConveyorBelt:
336
+ output.add(
337
+ ConveyorBelt(
338
+ *get_pos_rot(elem),
339
+ reversing=get_values(elem, "sts")[0] == "39",
340
+ )
341
+ )
342
+ case DeviceModel.DashTunnelA:
343
+ output.add(
344
+ DashTunnel(
345
+ *get_pos_rot(elem),
346
+ shape=DeviceModel.DashTunnelA,
347
+ )
348
+ )
349
+ case DeviceModel.DashTunnelB:
350
+ output.add(
351
+ DashTunnel(
352
+ *get_pos_rot(elem),
353
+ shape=DeviceModel.DashTunnelB,
354
+ )
355
+ )
356
+ case DeviceModel.SeesawLBlock:
357
+ output.add(
358
+ SeesawBlock(
359
+ *get_pos_rot(elem),
360
+ shape=DeviceModel.SeesawLBlock,
361
+ )
362
+ )
363
+ case DeviceModel.SeesawIBlock:
364
+ output.add(
365
+ SeesawBlock(
366
+ *get_pos_rot(elem),
367
+ shape=DeviceModel.SeesawIBlock,
368
+ )
369
+ )
370
+ case DeviceModel.AutoSeesawLBlock:
371
+ output.add(
372
+ SeesawBlock(
373
+ *get_pos_rot(elem),
374
+ auto=True,
375
+ shape=DeviceModel.SeesawLBlock,
376
+ )
377
+ )
378
+ case DeviceModel.AutoSeesawIBlock:
379
+ output.add(
380
+ SeesawBlock(
381
+ *get_pos_rot(elem),
382
+ auto=True,
383
+ shape=DeviceModel.SeesawIBlock,
384
+ )
385
+ )
386
+ case DeviceModel.Cannon:
387
+ output.add(Cannon(*get_pos_rot(elem)))
388
+ case DeviceModel.Drawbridge:
389
+ output.add(Drawbridge(*get_pos_rot(elem)))
390
+ case DeviceModel.Turntable:
391
+ output.add(
392
+ Turntable(
393
+ *get_pos_rot(elem),
394
+ speed=Speed(int(get_values(elem, "sts")[0])),
395
+ )
396
+ )
397
+ case DeviceModel.Bumper:
398
+ output.add(Bumper(*get_pos_rot(elem)))
399
+ case DeviceModel.PowerfulBumper:
400
+ output.add(Bumper(*get_pos_rot(elem), powerful=True))
401
+ case DeviceModel.Thorn:
402
+ output.add(Thorn(*get_pos_rot(elem)))
403
+ case DeviceModel.Gear:
404
+ output.add(
405
+ Gear(
406
+ *get_pos_rot(elem),
407
+ speed=Speed(int(get_values(elem, "sts")[0])),
408
+ )
409
+ )
410
+ case DeviceModel.Fan:
411
+ output.add(Fan(*get_pos_rot(elem)))
412
+ case DeviceModel.PowerfulFan:
413
+ output.add(
414
+ Fan(
415
+ *get_pos_rot(elem),
416
+ wind_pattern=DeviceModel.PowerfulFan,
417
+ )
418
+ )
419
+ case DeviceModel.TimerFan:
420
+ output.add(
421
+ Fan(
422
+ *get_pos_rot(elem),
423
+ wind_pattern=DeviceModel.TimerFan,
424
+ )
425
+ )
426
+ case DeviceModel.Spring:
427
+ output.add(Spring(*get_pos_rot(elem)))
428
+ case DeviceModel.Punch:
429
+ output.add(
430
+ Punch(
431
+ *get_pos_rot(elem),
432
+ timing=MovementTiming(
433
+ int(get_values(elem, "sts")[0])
434
+ ),
435
+ )
436
+ )
437
+ case DeviceModel.Press:
438
+ output.add(
439
+ Press(
440
+ *get_pos_rot(elem),
441
+ timing=MovementTiming(
442
+ int(get_values(elem, "sts")[0])
443
+ ),
444
+ )
445
+ )
446
+ case DeviceModel.Scissors:
447
+ output.add(
448
+ Scissors(
449
+ *get_pos_rot(elem),
450
+ timing=MovementTiming(
451
+ int(get_values(elem, "sts")[0])
452
+ ),
453
+ )
454
+ )
455
+ case DeviceModel.MagnifyingGlass:
456
+ output.add(MagnifyingGlass(*get_pos_rot(elem)))
457
+ case DeviceModel.UpsideDownStageDevice:
458
+ output.add(UpsideDownStageDevice(*get_pos_rot(elem)))
459
+ case DeviceModel.UpsideDownBall:
460
+ output.add(UpsideDownBall(*get_pos_rot(elem)))
461
+ case DeviceModel.SmallTunnel:
462
+ output.add(
463
+ SizeTunnel(
464
+ *get_pos_rot(elem), size=DeviceModel.SmallTunnel
465
+ )
466
+ )
467
+ case DeviceModel.BigTunnel:
468
+ output.add(
469
+ SizeTunnel(
470
+ *get_pos_rot(elem), size=DeviceModel.BigTunnel
471
+ )
472
+ )
473
+ case DeviceModel.BlinkingTile:
474
+ output.add(
475
+ BlinkingTile(
476
+ *get_pos_rot(elem),
477
+ timing=MovementTiming(
478
+ int(get_values(elem, "sts")[0])
479
+ ),
480
+ )
481
+ )
482
+ case DeviceModel.KororinCapsule:
483
+ output.add(KororinCapsule(*get_pos_rot(elem)))
484
+ case DeviceModel.GreenCrystal:
485
+ output.add(GreenCrystal(*get_pos_rot(elem)))
486
+ case DeviceModel.Ant:
487
+ output.add(Ant(*get_pos_rot(elem)))
488
+ case model if model.name.startswith("MelodyTile"):
489
+ output.add(MelodyTile(*get_pos_rot(elem), note=model)) # type: ignore[arg-type]
490
+ else:
491
+ groups.setdefault(int(get_values(elem, "group")[0]), {})[
492
+ int(get_values(elem, "group")[1])
493
+ ] = elem
494
+ for group in groups.values():
495
+ match DeviceModel(int(get_values(group[0], "model")[1])):
496
+ case DeviceModel.EndMagnet:
497
+ m: Magnet = Magnet()
498
+ for _, elem in sorted(group.items()):
499
+ m.append(
500
+ MagnetSegment(
501
+ *get_pos_rot(elem),
502
+ shape=DeviceModel(int(get_values(elem, "model")[1])), # type: ignore[arg-type]
503
+ )
504
+ )
505
+ output.add(m)
506
+ case DeviceModel.ToyTrain:
507
+ t: ToyTrain = ToyTrain(*get_pos_rot(group[0]))
508
+ for i, elem in sorted(group.items()):
509
+ if i:
510
+ t.append(
511
+ TrainTrack(
512
+ *get_pos_rot(elem),
513
+ shape=DeviceModel(int(get_values(elem, "model")[1])), # type: ignore[arg-type]
514
+ )
515
+ )
516
+ output.add(t)
517
+ case DeviceModel.Warp:
518
+ output.add(
519
+ Warp(
520
+ *get_pos_rot(group[0]),
521
+ **dict(
522
+ zip(
523
+ ("dest_x", "dest_y", "dest_z"),
524
+ map(float, get_values(group[0], "anmmov1")),
525
+ )
526
+ ),
527
+ **dict(
528
+ zip(
529
+ (
530
+ "return_x_pos",
531
+ "return_y_pos",
532
+ "return_z_pos",
533
+ "return_x_rot",
534
+ "return_y_rotation",
535
+ "return_z_rotation",
536
+ ),
537
+ get_pos_rot(group[1]),
538
+ strict=True,
539
+ )
540
+ ),
541
+ **dict(
542
+ zip(
543
+ ("return_dest_x", "return_dest_y", "return_dest_z"),
544
+ map(float, get_values(group[1], "anmmov1")),
545
+ )
546
+ ),
547
+ ) # type: ignore[misc]
548
+ )
549
+ return output
550
+
551
+ @staticmethod
552
+ def serialize(stage: Stage, /) -> bytes:
553
+ def minify(value: float, /) -> str:
554
+ """Removes the decimal point from floats representing integers."""
555
+ return str(int(value) if value.is_integer() else value)
556
+
557
+ def serialize_numbers(*values: float) -> str:
558
+ """Does not include leading or trailing spaces."""
559
+ return " ".join(minify(value) for value in values)
560
+
561
+ def anmtype(device: BasePart, /) -> DeviceModel:
562
+ if isinstance(device, ProgressMarker):
563
+ return (
564
+ DeviceModel.Crystal if device.progress % 2 else DeviceModel.Respawn
565
+ )
566
+ elif isinstance(device, MovingTile):
567
+ match device.shape:
568
+ case PartModel.Tile10x10:
569
+ return (
570
+ DeviceModel.MovingTile10x10Switch
571
+ if device.switch
572
+ else DeviceModel.MovingTile10x10
573
+ )
574
+ case PartModel.Tile20x20:
575
+ return (
576
+ DeviceModel.MovingTile20x20Switch
577
+ if device.switch
578
+ else DeviceModel.MovingTile20x20
579
+ )
580
+ case PartModel.TileA30x30:
581
+ return (
582
+ DeviceModel.MovingTile30x30Switch
583
+ if device.switch
584
+ else DeviceModel.MovingTile30x30
585
+ )
586
+ case PartModel.TileA30x90:
587
+ return (
588
+ DeviceModel.MovingTile30x90Switch
589
+ if device.switch
590
+ else DeviceModel.MovingTile30x90
591
+ )
592
+ case PartModel.Tile90x90:
593
+ return (
594
+ DeviceModel.MovingTile90x90ASwitch
595
+ if device.switch
596
+ else DeviceModel.MovingTile90x90A
597
+ )
598
+ case PartModel.HoleB90x90:
599
+ return (
600
+ DeviceModel.MovingTile90x90BSwitch
601
+ if device.switch
602
+ else DeviceModel.MovingTile90x90B
603
+ )
604
+ case PartModel.FunnelPipe:
605
+ return DeviceModel.MovingFunnelPipe
606
+ case PartModel.StraightPipe:
607
+ return DeviceModel.MovingStraightPipe
608
+ elif isinstance(device, MovingCurve):
609
+ match device.shape:
610
+ case PartModel.CurveS:
611
+ return DeviceModel.MovingCurveS
612
+ case PartModel.CurveM:
613
+ return DeviceModel.MovingCurveM
614
+ case PartModel.CurveL:
615
+ return DeviceModel.MovingCurveL
616
+ elif isinstance(device, SlidingTile):
617
+ return DeviceModel.SlidingTile
618
+ elif isinstance(device, ConveyorBelt):
619
+ return DeviceModel.ConveyorBelt
620
+ elif isinstance(device, DashTunnel):
621
+ return device.shape
622
+ elif isinstance(device, SeesawBlock):
623
+ if device.auto:
624
+ match device.shape:
625
+ case DeviceModel.SeesawLBlock:
626
+ return DeviceModel.AutoSeesawLBlock
627
+ case DeviceModel.SeesawIBlock:
628
+ return DeviceModel.AutoSeesawIBlock
629
+ else:
630
+ return device.shape
631
+ elif isinstance(device, Cannon):
632
+ return DeviceModel.Cannon
633
+ elif isinstance(device, Drawbridge):
634
+ return DeviceModel.Drawbridge
635
+ elif isinstance(device, Turntable):
636
+ return DeviceModel.Turntable
637
+ elif isinstance(device, Bumper):
638
+ return (
639
+ DeviceModel.PowerfulBumper
640
+ if device.powerful
641
+ else DeviceModel.Bumper
642
+ )
643
+ elif isinstance(device, Thorn):
644
+ return DeviceModel.Thorn
645
+ elif isinstance(device, Gear):
646
+ return DeviceModel.Gear
647
+ elif isinstance(device, Fan):
648
+ return device.wind_pattern
649
+ elif isinstance(device, Spring):
650
+ return DeviceModel.Spring
651
+ elif isinstance(device, Punch):
652
+ return DeviceModel.Punch
653
+ elif isinstance(device, Press):
654
+ return DeviceModel.Press
655
+ elif isinstance(device, Scissors):
656
+ return DeviceModel.Scissors
657
+ elif isinstance(device, MagnifyingGlass):
658
+ return DeviceModel.MagnifyingGlass
659
+ elif isinstance(device, UpsideDownStageDevice):
660
+ return DeviceModel.UpsideDownStageDevice
661
+ elif isinstance(device, UpsideDownBall):
662
+ return DeviceModel.UpsideDownBall
663
+ elif isinstance(device, SizeTunnel):
664
+ return device.size
665
+ elif isinstance(device, BlinkingTile):
666
+ return DeviceModel.BlinkingTile
667
+ elif isinstance(device, MelodyTile):
668
+ return device.note
669
+ elif isinstance(device, KororinCapsule):
670
+ return DeviceModel.KororinCapsule
671
+ elif isinstance(device, GreenCrystal):
672
+ return DeviceModel.GreenCrystal
673
+ elif isinstance(device, Ant):
674
+ return DeviceModel.Ant
675
+ else:
676
+ raise ValueError(f"part {device!r} does not have a known anmtype")
677
+
678
+ def device_data(device: BasePart, /) -> str:
679
+ if isinstance(device, ProgressMarker):
680
+ return f"<hook> {(device._progress - 1) // 2} 0 </hook>"
681
+ elif isinstance(device, MovingTile):
682
+ anmmov: Final[str] = (
683
+ f"<anmspd> {minify(device.speed)} 0 </anmspeed><anmmov0> {serialize_numbers(device.x_pos, device.y_pos, device.z_pos)} </anmmov0><anmmov1> {serialize_numbers(device.dest_x, device.dest_y, device.dest_z)} </anmmov1>"
684
+ )
685
+ if device.walls:
686
+ match device.shape:
687
+ case PartModel.Tile20x20:
688
+ return f"<hook> {DeviceModel.MovingTile20x20Wall.value} {device.walls.value} </hook>{anmmov}"
689
+ case PartModel.TileA30x30:
690
+ return f"<hook> {DeviceModel.MovingTile30x30Wall.value} {device.walls.value} </hook>{anmmov}"
691
+ return anmmov
692
+ else:
693
+ return ""
694
+
695
+ def sts(part: BasePart, /) -> int:
696
+ if isinstance(part, FixedSpeedDevice):
697
+ return part.speed.value
698
+ elif isinstance(part, ConveyorBelt):
699
+ return 39 if part.reversing else 23
700
+ elif isinstance(part, TimedDevice):
701
+ return part.timing.value
702
+ else:
703
+ return 7
704
+
705
+ with StringIO(
706
+ f'<?xml version="1.0" encoding="SHIFT_JIS"?><EDITINFO><THEME> {stage.theme.value} </THEME><LOCK> {int(stage.tilt_lock)} </LOCK><EDITUSER> {stage.edit_user.value} </EDITUSER></EDITINFO><STAGEDATA><EDIT_BG_NORMAL><model> "EBB_{stage.theme.value:02}.bin 0 </model></EDIT_BG_NORMAL>'
707
+ ) as output:
708
+ group: int = 1
709
+ for part in stage:
710
+ if isinstance(part, Magnet):
711
+ for i, segment in enumerate(part):
712
+ output.write(
713
+ f'<EDIT_GIM_NORMAL><model> "EGB_{stage.theme.value:02}.bin" {segment.shape} </model><pos> {serialize_numbers(segment.x_pos, segment.y_pos, segment.z_pos)} </pos><rot> {serialize_numbers(segment.x_rot, segment.y_rot, segment.z_rot)} </rot><sts> 7 </sts><group> {group} {i} </group></EDIT_GIM_NORMAL>'
714
+ )
715
+ group += 1
716
+ elif isinstance(part, ToyTrain):
717
+ output.write(
718
+ f'<EDIT_GIM_NORMAL><model> "EGB_{stage.theme.value:02}.bin" {DeviceModel.ToyTrain.value} </model><pos> {serialize_numbers(part.x_pos, part.y_pos, part.z_pos)} </pos><rot> {part.x_rot, part.y_rot, part.z_rot} </rot><sts> 7 </sts><group> {group} 0 </group></EDIT_GIM_NORMAL>'
719
+ )
720
+ for i, track in enumerate(part, 1):
721
+ output.write(
722
+ f'<EDIT_GIM_NORMAL><model> "EGB_{stage.theme.value:02}.bin" {track.shape} </model><pos> {serialize_numbers(track.x_pos, track.y_pos, track.z_pos)} </pos><rot> {serialize_numbers(track.x_rot, track.y_rot, track.z_rot)} </rot><sts> 7 </sts><group> {group} {i} </group></EDIT_GIM_NORMAL>'
723
+ )
724
+ group += 1
725
+ elif isinstance(part, Warp):
726
+ output.write(
727
+ f'<EDIT_GIM_NORMAL><model> "EGB_{stage.theme.value:02}.bin" {DeviceModel.Warp.value} </model><pos> {serialize_numbers(part.x_pos, part.y_pos, part.z_pos)} </pos><rot> {part.x_rot, part.y_rot, part.z_rot} </rot><sts> 7 </sts><anmmov0> {serialize_numbers(part.dest_x, part.dest_y, part.dest_z)} </anmmov0><group> {group} 0 </group></EDIT_GIM_NORMAL><EDIT_GIM_NORMAL><model> "EGB_{stage.theme.value:02}.bin" {DeviceModel.Warp.value} </model><pos> {serialize_numbers(part.return_x_pos, part.return_y_pos, part.return_z_pos)} </pos><rot> {part.return_x_rot, part.return_y_rot, part.return_z_rot} </rot><sts> 7 </sts><anmmov0> {serialize_numbers(part.return_dest_x, part.return_dest_y, part.return_dest_z)} </anmmov0><group> {group} 0 </group></EDIT_GIM_NORMAL>'
728
+ )
729
+ group += 1
730
+ else:
731
+ if isinstance(part, Start):
732
+ output.write("<EDIT_GIM_START>")
733
+ elif isinstance(part, Goal):
734
+ output.write("<EDIT_GIM_GOAL>")
735
+ elif isinstance(part, Part):
736
+ if isinstance(part.shape, DecorationModel):
737
+ output.write("<EDIT_MAP_EXT>")
738
+ else:
739
+ output.write("<EDIT_MAP_NORMAL>")
740
+ else:
741
+ output.write("<EDIT_GIM_NORMAL>")
742
+ if isinstance(part, Part):
743
+ if isinstance(part.shape, DecorationModel):
744
+ output.write(
745
+ f'<model> "EME_{stage.theme.value:02}.bin" {part.shape.value} </model>'
746
+ )
747
+ else:
748
+ output.write(
749
+ f'<model> "EMB_{stage.theme.value:02}.bin" {part.shape.value} </model>'
750
+ )
751
+ elif isinstance(part, Start):
752
+ output.write(
753
+ f'<model> "EGB_{stage.theme.value:02}.bin" 0 </model>'
754
+ )
755
+ elif isinstance(part, Goal):
756
+ output.write(
757
+ f'<model> "EGB_{stage.theme.value:02}.bin" 1 </model>'
758
+ )
759
+ else:
760
+ output.write(
761
+ f'<model> "EGB_{stage.theme.value:02}.bin" {anmtype(part).value} </model>'
762
+ )
763
+ output.write(
764
+ f"<pos> {serialize_numbers(part.x_pos, part.y_pos, part.z_pos)} </pos><rot> {part.x_rot, part.y_rot, part.z_rot} </rot><sts> {sts(part)} </sts>"
765
+ )
766
+ try:
767
+ output.write(f"<anmtype> {anmtype(part).value} </anmtype>")
768
+ except ValueError:
769
+ pass
770
+ output.write(device_data(part))
771
+ if isinstance(part, Start):
772
+ output.write("</EDIT_GIM_START>")
773
+ elif isinstance(part, Goal):
774
+ output.write("</EDIT_GIM_GOAL>")
775
+ elif isinstance(part, Part):
776
+ if isinstance(part.shape, DecorationModel):
777
+ output.write("</EDIT_MAP_EXT>")
778
+ else:
779
+ output.write("</EDIT_MAP_NORMAL>")
780
+ else:
781
+ output.write("</EDIT_GIM_NORMAL>")
782
+ output.write("</STAGEDATA>")
783
+ return output.getvalue().encode("shift_jis", "xmlcharrefreplace")