koro 1.1.6__py3-none-any.whl → 2.0.0rc2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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")