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