koro 2.0.0rc2__py3-none-any.whl → 2.0.0rc3__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
koro/slot/save.py CHANGED
@@ -12,6 +12,8 @@ from .xml import XmlSlot
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from _typeshed import StrOrBytesPath
15
+ else:
16
+ StrOrBytesPath = Any
15
17
 
16
18
 
17
19
  __all__ = ["EditorPage", "get_slots", "SaveSlot"]
koro/slot/xml.py CHANGED
@@ -1,8 +1,7 @@
1
- # mypy: disable-error-code=union-attr
2
-
3
1
  from collections.abc import Iterator, Sequence
4
2
  from io import StringIO
5
3
  from itertools import chain
4
+ from os import SEEK_END
6
5
  from typing import Final
7
6
  from xml.etree.ElementTree import Element, fromstring
8
7
 
@@ -13,6 +12,7 @@ from ..stage.part import (
13
12
  BasePart,
14
13
  BlinkingTile,
15
14
  Bumper,
15
+ TextBox,
16
16
  Cannon,
17
17
  ConveyorBelt,
18
18
  DashTunnel,
@@ -64,7 +64,7 @@ class XmlSlot(FileSlot):
64
64
  """Behavior is undefined when passed invalid stage data"""
65
65
 
66
66
  def get_values(element: Element, /, tag: str) -> Sequence[str]:
67
- return element.find(tag).text.strip().split()
67
+ return element.find(tag).text.strip().split() # type: ignore[union-attr]
68
68
 
69
69
  def get_pos_rot(element: Element, /) -> Iterator[float]:
70
70
  return chain(
@@ -75,19 +75,23 @@ class XmlSlot(FileSlot):
75
75
  root: Final[Element] = fromstring(
76
76
  data.decode("shift_jis", "xmlcharrefreplace").replace(
77
77
  '<?xml version="1.0" encoding="SHIFT_JIS"?>',
78
- '<?xml version="1.0"?><body>',
78
+ '<?xml version="1.0"?>\n<body>',
79
79
  )
80
80
  + "</body>"
81
81
  )
82
82
  editinfo: Final[Element] = root.find("EDITINFO") # type: ignore[assignment]
83
83
  output: Final[Stage] = Stage(
84
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())),
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]
88
92
  )
89
93
  groups: Final[dict[int, dict[int, Element]]] = {}
90
- for elem in root.find("STAGEDATA"):
94
+ for elem in root.find("STAGEDATA") or ():
91
95
  match elem.tag:
92
96
  case "EDIT_LIGHT" | "EDIT_BG_NORMAL":
93
97
  continue
@@ -110,387 +114,444 @@ class XmlSlot(FileSlot):
110
114
  case "EDIT_GIM_GOAL":
111
115
  output.add(Goal(*get_pos_rot(elem)))
112
116
  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
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
494
555
  for group in groups.values():
495
556
  match DeviceModel(int(get_values(group[0], "model")[1])):
496
557
  case DeviceModel.EndMagnet:
@@ -521,7 +582,7 @@ class XmlSlot(FileSlot):
521
582
  **dict(
522
583
  zip(
523
584
  ("dest_x", "dest_y", "dest_z"),
524
- map(float, get_values(group[0], "anmmov1")),
585
+ map(float, get_values(group[0], "anmmov0")),
525
586
  )
526
587
  ),
527
588
  **dict(
@@ -531,8 +592,8 @@ class XmlSlot(FileSlot):
531
592
  "return_y_pos",
532
593
  "return_z_pos",
533
594
  "return_x_rot",
534
- "return_y_rotation",
535
- "return_z_rotation",
595
+ "return_y_rot",
596
+ "return_z_rot",
536
597
  ),
537
598
  get_pos_rot(group[1]),
538
599
  strict=True,
@@ -541,7 +602,7 @@ class XmlSlot(FileSlot):
541
602
  **dict(
542
603
  zip(
543
604
  ("return_dest_x", "return_dest_y", "return_dest_z"),
544
- map(float, get_values(group[1], "anmmov1")),
605
+ map(float, get_values(group[1], "anmmov0")),
545
606
  )
546
607
  ),
547
608
  ) # type: ignore[misc]
@@ -617,7 +678,7 @@ class XmlSlot(FileSlot):
617
678
  return DeviceModel.SlidingTile
618
679
  elif isinstance(device, ConveyorBelt):
619
680
  return DeviceModel.ConveyorBelt
620
- elif isinstance(device, DashTunnel):
681
+ elif isinstance(device, (DashTunnel, TextBox)):
621
682
  return device.shape
622
683
  elif isinstance(device, SeesawBlock):
623
684
  if device.auto:
@@ -677,17 +738,17 @@ class XmlSlot(FileSlot):
677
738
 
678
739
  def device_data(device: BasePart, /) -> str:
679
740
  if isinstance(device, ProgressMarker):
680
- return f"<hook> {(device._progress - 1) // 2} 0 </hook>"
741
+ return f"<hook> {(device._progress - 1) // 2} 0 </hook>\n"
681
742
  elif isinstance(device, MovingTile):
682
743
  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>"
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>"
684
745
  )
685
746
  if device.walls:
686
747
  match device.shape:
687
748
  case PartModel.Tile20x20:
688
- return f"<hook> {DeviceModel.MovingTile20x20Wall.value} {device.walls.value} </hook>{anmmov}"
749
+ return f"<hook> {DeviceModel.MovingTile20x20Wall.value} {device.walls.value} </hook>\n{anmmov}\n"
689
750
  case PartModel.TileA30x30:
690
- return f"<hook> {DeviceModel.MovingTile30x30Wall.value} {device.walls.value} </hook>{anmmov}"
751
+ return f"<hook> {DeviceModel.MovingTile30x30Wall.value} {device.walls.value} </hook>\n{anmmov}\n"
691
752
  return anmmov
692
753
  else:
693
754
  return ""
@@ -703,81 +764,82 @@ class XmlSlot(FileSlot):
703
764
  return 7
704
765
 
705
766
  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>'
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'
707
768
  ) as output:
769
+ output.seek(0, SEEK_END)
708
770
  group: int = 1
709
771
  for part in stage:
710
772
  if isinstance(part, Magnet):
711
773
  for i, segment in enumerate(part):
712
774
  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>'
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'
714
776
  )
715
777
  group += 1
716
778
  elif isinstance(part, ToyTrain):
717
779
  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>'
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'
719
781
  )
720
782
  for i, track in enumerate(part, 1):
721
783
  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>'
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'
723
785
  )
724
786
  group += 1
725
787
  elif isinstance(part, Warp):
726
788
  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>'
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'
728
790
  )
729
791
  group += 1
730
792
  else:
731
793
  if isinstance(part, Start):
732
- output.write("<EDIT_GIM_START>")
794
+ output.write("<EDIT_GIM_START>\n")
733
795
  elif isinstance(part, Goal):
734
- output.write("<EDIT_GIM_GOAL>")
796
+ output.write("<EDIT_GIM_GOAL>\n")
735
797
  elif isinstance(part, Part):
736
798
  if isinstance(part.shape, DecorationModel):
737
- output.write("<EDIT_MAP_EXT>")
799
+ output.write("<EDIT_MAP_EXT>\n")
738
800
  else:
739
- output.write("<EDIT_MAP_NORMAL>")
801
+ output.write("<EDIT_MAP_NORMAL>\n")
740
802
  else:
741
- output.write("<EDIT_GIM_NORMAL>")
803
+ output.write("<EDIT_GIM_NORMAL>\n")
742
804
  if isinstance(part, Part):
743
805
  if isinstance(part.shape, DecorationModel):
744
806
  output.write(
745
- f'<model> "EME_{stage.theme.value:02}.bin" {part.shape.value} </model>'
807
+ f'<model> "EME_{stage.theme.value:02}.bin" {part.shape.value} </model>\n'
746
808
  )
747
809
  else:
748
810
  output.write(
749
- f'<model> "EMB_{stage.theme.value:02}.bin" {part.shape.value} </model>'
811
+ f'<model> "EMB_{stage.theme.value:02}.bin" {part.shape.value} </model>\n'
750
812
  )
751
813
  elif isinstance(part, Start):
752
814
  output.write(
753
- f'<model> "EGB_{stage.theme.value:02}.bin" 0 </model>'
815
+ f'<model> "EGB_{stage.theme.value:02}.bin" 0 </model>\n'
754
816
  )
755
817
  elif isinstance(part, Goal):
756
818
  output.write(
757
- f'<model> "EGB_{stage.theme.value:02}.bin" 1 </model>'
819
+ f'<model> "EGB_{stage.theme.value:02}.bin" 1 </model>\n'
758
820
  )
759
821
  else:
760
822
  output.write(
761
- f'<model> "EGB_{stage.theme.value:02}.bin" {anmtype(part).value} </model>'
823
+ f'<model> "EGB_{stage.theme.value:02}.bin" {anmtype(part).value} </model>\n'
762
824
  )
763
825
  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>"
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"
765
827
  )
766
828
  try:
767
- output.write(f"<anmtype> {anmtype(part).value} </anmtype>")
829
+ output.write(f"<anmtype> {anmtype(part).value} </anmtype>\n")
768
830
  except ValueError:
769
831
  pass
770
832
  output.write(device_data(part))
771
833
  if isinstance(part, Start):
772
- output.write("</EDIT_GIM_START>")
834
+ output.write("</EDIT_GIM_START>\n")
773
835
  elif isinstance(part, Goal):
774
- output.write("</EDIT_GIM_GOAL>")
836
+ output.write("</EDIT_GIM_GOAL>\n")
775
837
  elif isinstance(part, Part):
776
838
  if isinstance(part.shape, DecorationModel):
777
- output.write("</EDIT_MAP_EXT>")
839
+ output.write("</EDIT_MAP_EXT>\n")
778
840
  else:
779
- output.write("</EDIT_MAP_NORMAL>")
841
+ output.write("</EDIT_MAP_NORMAL>\n")
780
842
  else:
781
- output.write("</EDIT_GIM_NORMAL>")
843
+ output.write("</EDIT_GIM_NORMAL>\n")
782
844
  output.write("</STAGEDATA>")
783
845
  return output.getvalue().encode("shift_jis", "xmlcharrefreplace")
koro/stage/model.py CHANGED
@@ -277,6 +277,10 @@ class DeviceModel(Enum):
277
277
  """The blue line used by moving tiles"""
278
278
  OrangeLine = 50
279
279
  """The orange line used by switch moving tiles"""
280
+ CubicTextBox = 92
281
+ """A cubic region that opens a text box"""
282
+ WallTextBox = 93
283
+ """A tall, wide, and thin region that opens a text box"""
280
284
  QuestionMark = 94
281
285
  """An animated question mark with a glow beneath it"""
282
286
 
koro/stage/part.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from abc import ABC, abstractmethod
4
4
  from collections.abc import Iterable, MutableSequence
5
5
  from enum import Enum, unique, Flag
6
+ from operator import index
6
7
  from sys import maxsize
7
8
  from typing import Any, Final, Iterator, Literal, Self, SupportsIndex, overload
8
9
 
@@ -328,7 +329,7 @@ class MovingTile(BasePart):
328
329
  self.dest_x = dest_x
329
330
  self.dest_y = dest_y
330
331
  self.dest_z = dest_z
331
- self.shape = shape
332
+ self._shape = shape
332
333
  self.speed = speed # type: ignore[assignment]
333
334
  self.switch = switch
334
335
  self.walls = walls
@@ -456,7 +457,9 @@ class MovingTile(BasePart):
456
457
 
457
458
  @switch.setter
458
459
  def switch(self, value: bool, /) -> None:
459
- if self.shape in frozenset({PartModel.FunnelPipe, PartModel.StraightPipe}):
460
+ if value and self.shape in frozenset(
461
+ {PartModel.FunnelPipe, PartModel.StraightPipe}
462
+ ):
460
463
  raise ValueError("Moving pipes cannot be switches")
461
464
  else:
462
465
  self._switch = value
@@ -467,7 +470,9 @@ class MovingTile(BasePart):
467
470
 
468
471
  @walls.setter
469
472
  def walls(self, value: Walls, /) -> None:
470
- if self.shape in frozenset({PartModel.Tile20x20, PartModel.TileA30x30}):
473
+ if not value or self.shape in frozenset(
474
+ {PartModel.Tile20x20, PartModel.TileA30x30}
475
+ ):
471
476
  self._walls = value
472
477
  else:
473
478
  raise ValueError("Invalid shape for wall attachment")
@@ -1680,6 +1685,51 @@ class MelodyTile(BasePart):
1680
1685
  return f"{type(self).__name__}({self.x_pos!r}, {self.y_pos!r}, {self.z_pos!r}, {self.x_rot!r}, {self.y_rot!r}, {self.z_rot!r}, note={self.note!r})"
1681
1686
 
1682
1687
 
1688
+ class TextBox(BasePart):
1689
+ __slots__ = ("_shape", "_text_id")
1690
+
1691
+ _shape: Literal[DeviceModel.CubicTextBox, DeviceModel.WallTextBox]
1692
+ _text_id: int
1693
+
1694
+ def __init__(
1695
+ self,
1696
+ x_pos: float,
1697
+ y_pos: float,
1698
+ z_pos: float,
1699
+ x_rot: float,
1700
+ y_rot: float,
1701
+ z_rot: float,
1702
+ *,
1703
+ shape: Literal[DeviceModel.CubicTextBox, DeviceModel.WallTextBox],
1704
+ text_id: SupportsIndex,
1705
+ ) -> None:
1706
+ super().__init__(x_pos, y_pos, z_pos, x_rot, y_rot, z_rot)
1707
+ self.shape = shape
1708
+ self.text_id = text_id # type: ignore[assignment]
1709
+
1710
+ @property
1711
+ def cost(self) -> Literal[0]:
1712
+ return 0
1713
+
1714
+ @property
1715
+ def shape(self) -> Literal[DeviceModel.CubicTextBox, DeviceModel.WallTextBox]:
1716
+ return self._shape
1717
+
1718
+ @shape.setter
1719
+ def shape(
1720
+ self, value: Literal[DeviceModel.CubicTextBox, DeviceModel.WallTextBox], /
1721
+ ) -> None:
1722
+ self._shape = value
1723
+
1724
+ @property
1725
+ def text_id(self) -> int:
1726
+ return self._text_id
1727
+
1728
+ @text_id.setter
1729
+ def text_id(self, value: SupportsIndex, /) -> None:
1730
+ self._text_id = index(value)
1731
+
1732
+
1683
1733
  class KororinCapsule(BasePart):
1684
1734
  __slots__ = ()
1685
1735
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: koro
3
- Version: 2.0.0rc2
3
+ Version: 2.0.0rc3
4
4
  Summary: Tools for manipulating levels made in Marble Saga: Kororinpa
5
5
  Home-page: https://github.com/DigitalDetective47/koro
6
6
  Author: DigitalDetective47
@@ -0,0 +1,14 @@
1
+ koro/__init__.py,sha256=xS3s862eN55uy83K2Dwchurhh9Euh1te2ZPia2lRVLI,200
2
+ koro/slot/__init__.py,sha256=ldVeVLwXIgPZJsCp4-ix78ewRgsAKVwa8BnpcdnpfIc,419
3
+ koro/slot/bin.py,sha256=D_v8RWoSz4OQ2KezQnVCGX1st6oCcC10vowHD_QjlBg,6659
4
+ koro/slot/file.py,sha256=GqixeShwrZ6N4YRmlX46fF5OV7zV_fkg9S_r1zewd6U,1698
5
+ koro/slot/save.py,sha256=GYL3ZqSLmXZWVy21j52aHzTc7sbgnyBoNdooKsXdR3k,4582
6
+ koro/slot/xml.py,sha256=C2g1OlGDwhlVocEeUg7tYxQf1mfRVNopH0yH1UG1Cvw,42992
7
+ koro/stage/__init__.py,sha256=D3OnCSnS5MTTEfYwUOKeUUTwP1R2Q2yqzAUrE6wyC0k,2510
8
+ koro/stage/model.py,sha256=h2d9w68iXjQHcpVpEK61QjbYq9p9diRv-aCTiUgpW9c,8717
9
+ koro/stage/part.py,sha256=I6JHMaftTd6XtX0fHj2vxTtWmOE66vFsmt8vDLFka7A,48640
10
+ koro-2.0.0rc3.dist-info/LICENSE,sha256=Q2ptU2E48gOsMzhYz_kqVovmJ5d1KzrblLyqD2_-fvY,1235
11
+ koro-2.0.0rc3.dist-info/METADATA,sha256=6MOvd43f2utmsdAmuWtUWTCcUxnauya0ok4U0uGaP7A,857
12
+ koro-2.0.0rc3.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
13
+ koro-2.0.0rc3.dist-info/top_level.txt,sha256=Msq6ssMwv56hnBQqwaTdJJkxM7x7BZ-3JfQbkepQAEY,5
14
+ koro-2.0.0rc3.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- koro/__init__.py,sha256=xS3s862eN55uy83K2Dwchurhh9Euh1te2ZPia2lRVLI,200
2
- koro/slot/__init__.py,sha256=ldVeVLwXIgPZJsCp4-ix78ewRgsAKVwa8BnpcdnpfIc,419
3
- koro/slot/bin.py,sha256=D_v8RWoSz4OQ2KezQnVCGX1st6oCcC10vowHD_QjlBg,6659
4
- koro/slot/file.py,sha256=GqixeShwrZ6N4YRmlX46fF5OV7zV_fkg9S_r1zewd6U,1698
5
- koro/slot/save.py,sha256=G8BN2YwWtwJMG8b_7HCtmRYexS8lswV1SEsHMNpQtzU,4549
6
- koro/slot/xml.py,sha256=XmKPlyc-vXRyJY2Tn3E60i2D5OHPYn0YlIBJtLtOWwg,40840
7
- koro/stage/__init__.py,sha256=D3OnCSnS5MTTEfYwUOKeUUTwP1R2Q2yqzAUrE6wyC0k,2510
8
- koro/stage/model.py,sha256=927aOmUKdQVCJfxs2l1U5upfh37yY3hSQug-cKnCcVo,8561
9
- koro/stage/part.py,sha256=yaVSkjA9Snkv-aEANsPyZZVCbbIQakIrjv8nK4rFSEQ,47349
10
- koro-2.0.0rc2.dist-info/LICENSE,sha256=Q2ptU2E48gOsMzhYz_kqVovmJ5d1KzrblLyqD2_-fvY,1235
11
- koro-2.0.0rc2.dist-info/METADATA,sha256=kIntIatispnk2v6n_FxzNNOBhb_KoadK7soK4jdaZjQ,857
12
- koro-2.0.0rc2.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
13
- koro-2.0.0rc2.dist-info/top_level.txt,sha256=Msq6ssMwv56hnBQqwaTdJJkxM7x7BZ-3JfQbkepQAEY,5
14
- koro-2.0.0rc2.dist-info/RECORD,,