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