koro 2.0.0rc3__py3-none-any.whl → 2.0.2__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 CHANGED
@@ -1,845 +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")
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")