Re-reverse coordinate system to match OpenGL.
[python-bulletml.git] / bulletml / parser.py
1 """BulletML parser.
2
3 This is based on the format described at
4 http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/bulletml_ref_e.html.
5
6 Unless you are adding support for new tags, the only class you should
7 care about in here is BulletML.
8 """
9
10 from __future__ import division
11
12 import math
13
14 from xml.etree.ElementTree import ElementTree
15
16 # Python 3 moved this for no really good reason.
17 try:
18 from sys import intern
19 except ImportError:
20 pass
21
22 try:
23 from io import StringIO
24 except ImportError:
25 try:
26 from cStringIO import StringIO
27 except ImportError:
28 from StringIO import StringIO
29
30 from bulletml.errors import Error
31 from bulletml.expr import NumberDef, INumberDef
32
33
34 __all__ = ["ParseError", "BulletML"]
35
36 class ParseError(Error):
37 """Raised when an error occurs parsing the XML structure."""
38 pass
39
40 def realtag(element):
41 """Strip namespace poop off the front of a tag."""
42 try:
43 return element.tag.rsplit('}', 1)[1]
44 except ValueError:
45 return element.tag
46
47 class ParamList(object):
48 """List of parameter definitions."""
49
50 def __init__(self, params=()):
51 self.params = list(params)
52
53 @classmethod
54 def FromXML(cls, doc, element):
55 """Construct using an ElementTree-style element."""
56 return cls([NumberDef(subelem.text) for subelem in element
57 if realtag(subelem) == "param"])
58
59 def __call__(self, params, rank):
60 return [param(params, rank) for param in self.params]
61
62 def __repr__(self):
63 return "%s(%r)" % (type(self).__name__, self.params)
64
65 class Direction(object):
66 """Raw direction value."""
67
68 VALID_TYPES = ["relative", "absolute", "aim", "sequence"]
69
70 def __init__(self, type, value):
71 if type not in self.VALID_TYPES:
72 raise ValueError("invalid type %r" % type)
73 self.type = intern(type)
74 self.value = value
75
76 def __getstate__(self):
77 return [('type', self.type), ('value', self.value.expr)]
78
79 def __setstate__(self, state):
80 state = dict(state)
81 self.__init__(state["type"], NumberDef(state["value"]))
82
83 @classmethod
84 def FromXML(cls, doc, element, default="absolute"):
85 """Construct using an ElementTree-style element."""
86 return cls(element.get("type", default), NumberDef(element.text))
87
88 def __call__(self, params, rank):
89 return (math.radians(self.value(params, rank)), self.type)
90
91 def __repr__(self):
92 return "%s(%r, type=%r)" % (
93 type(self).__name__, self.value, self.type)
94
95 class ChangeDirection(object):
96 """Direction change over time."""
97
98 def __init__(self, term, direction):
99 self.term = term
100 self.direction = direction
101
102 def __getstate__(self):
103 return [('frames', self.term.expr),
104 ('type', self.direction.type),
105 ('value', self.direction.value.expr)]
106
107 def __setstate__(self, state):
108 state = dict(state)
109 self.__init__(INumberDef(state["frames"]),
110 Direction(state["type"], NumberDef(state["value"])))
111
112 @classmethod
113 def FromXML(cls, doc, element):
114 """Construct using an ElementTree-style element."""
115 for subelem in element.getchildren():
116 tag = realtag(subelem)
117 if tag == "direction":
118 direction = Direction.FromXML(doc, subelem)
119 elif tag == "term":
120 term = INumberDef(subelem.text)
121 try:
122 return cls(term, direction)
123 except UnboundLocalError as exc:
124 raise ParseError(str(exc))
125
126 def __call__(self, params, rank):
127 return self.term(params, rank), self.direction(params, rank)
128
129 def __repr__(self):
130 return "%s(term=%r, direction=%r)" % (
131 type(self).__name__, self.term, self.direction)
132
133 class Speed(object):
134 """Raw speed value."""
135
136 VALID_TYPES = ["relative", "absolute", "sequence"]
137
138 def __init__(self, type, value):
139 if type not in self.VALID_TYPES:
140 raise ValueError("invalid type %r" % type)
141 self.type = intern(type)
142 self.value = value
143
144 def __getstate__(self):
145 return [('type', self.type), ('value', self.value.expr)]
146
147 def __setstate__(self, state):
148 state = dict(state)
149 self.__init__(state["type"], NumberDef(state["value"]))
150
151 @classmethod
152 def FromXML(cls, doc, element):
153 """Construct using an ElementTree-style element."""
154 return cls(element.get("type", "absolute"), NumberDef(element.text))
155
156 def __call__(self, params, rank):
157 return (self.value(params, rank), self.type)
158
159 def __repr__(self):
160 return "%s(%r, type=%r)" % (type(self).__name__, self.value, self.type)
161
162 class ChangeSpeed(object):
163 """Speed change over time."""
164
165 def __init__(self, term, speed):
166 self.term = term
167 self.speed = speed
168
169 def __getstate__(self):
170 return [('frames', self.term.expr),
171 ('type', self.speed.type),
172 ('value', self.speed.value.expr)]
173
174 def __setstate__(self, state):
175 state = dict(state)
176 self.__init__(INumberDef(state["frames"]),
177 Speed(state["type"], NumberDef(state["value"])))
178
179 @classmethod
180 def FromXML(cls, doc, element):
181 """Construct using an ElementTree-style element."""
182 for subelem in element.getchildren():
183 tag = realtag(subelem)
184 if tag == "speed":
185 speed = Speed.FromXML(doc, subelem)
186 elif tag == "term":
187 term = INumberDef(subelem.text)
188 try:
189 return cls(term, speed)
190 except UnboundLocalError as exc:
191 raise ParseError(str(exc))
192
193 def __call__(self, params, rank):
194 return self.term(params, rank), self.speed(params, rank)
195
196 def __repr__(self):
197 return "%s(term=%r, speed=%r)" % (
198 type(self).__name__, self.term, self.speed)
199
200 class Wait(object):
201 """Wait for some frames."""
202
203 def __init__(self, frames):
204 self.frames = frames
205
206 def __getstate__(self):
207 return dict(frames=self.frames.expr)
208
209 def __setstate__(self, state):
210 self.__init__(INumberDef(state["frames"]))
211
212 @classmethod
213 def FromXML(cls, doc, element):
214 """Construct using an ElementTree-style element."""
215 return cls(INumberDef(element.text))
216
217 def __call__(self, params, rank):
218 return self.frames(params, rank)
219
220 def __repr__(self):
221 return "%s(%r)" % (type(self).__name__, self.frames)
222
223 class Tag(object):
224 """Set a bullet tag."""
225
226 def __init__(self, tag):
227 self.tag = tag
228
229 def __getstate__(self):
230 return dict(tag=self.tag)
231
232 def __setstate__(self, state):
233 self.__init__(state["tag"])
234
235 @classmethod
236 def FromXML(cls, doc, element):
237 """Construct using an ElementTree-style element."""
238 return cls(element.text)
239
240 class Untag(object):
241 """Unset a bullet tag."""
242
243 def __init__(self, tag):
244 self.tag = tag
245
246 def __getstate__(self):
247 return dict(tag=self.tag)
248
249 def __setstate__(self, state):
250 self.__init__(state["tag"])
251
252 @classmethod
253 def FromXML(cls, doc, element):
254 """Construct using an ElementTree-style element."""
255 return cls(element.text)
256
257 class Appearance(object):
258 """Set a bullet appearance."""
259
260 def __init__(self, appearance):
261 self.appearance = appearance
262
263 def __getstate__(self):
264 return dict(appearance=self.appearance)
265
266 def __setstate__(self, state):
267 self.__init__(state["appearance"])
268
269 @classmethod
270 def FromXML(cls, doc, element):
271 """Construct using an ElementTree-style element."""
272 return cls(element.text)
273
274 class Vanish(object):
275 """Make the owner disappear."""
276
277 def __init__(self):
278 pass
279
280 @classmethod
281 def FromXML(cls, doc, element):
282 """Construct using an ElementTree-style element."""
283 return cls()
284
285 def __repr__(self):
286 return "%s()" % (type(self).__name__)
287
288 class Repeat(object):
289 """Repeat an action definition."""
290
291 def __init__(self, times, action):
292 self.times = times
293 self.action = action
294
295 def __getstate__(self):
296 return [('times', self.times.expr), ('action', self.action)]
297
298 def __setstate__(self, state):
299 state = dict(state)
300 self.__init__(INumberDef(state["times"]), state["action"])
301
302 @classmethod
303 def FromXML(cls, doc, element):
304 """Construct using an ElementTree-style element."""
305 for subelem in element.getchildren():
306 tag = realtag(subelem)
307 if tag == "times":
308 times = INumberDef(subelem.text)
309 elif tag == "action":
310 action = ActionDef.FromXML(doc, subelem)
311 elif tag == "actionRef":
312 action = ActionRef.FromXML(doc, subelem)
313 try:
314 return cls(times, action)
315 except UnboundLocalError as exc:
316 raise ParseError(str(exc))
317
318 def __call__(self, params, rank):
319 return self.times(params, rank), self.action(params, rank)
320
321 def __repr__(self):
322 return "%s(%r, %r)" % (type(self).__name__, self.times, self.action)
323
324 class Accel(object):
325 """Accelerate over some time."""
326
327 horizontal = None
328 vertical = None
329
330 def __init__(self, term, horizontal=None, vertical=None):
331 self.term = term
332 self.horizontal = horizontal
333 self.vertical = vertical
334
335 def __getstate__(self):
336 state = [('frames', self.term.expr)]
337 if self.horizontal:
338 state.append(('horizontal', self.horizontal))
339 if self.vertical:
340 state.append(('vertical', self.vertical))
341 return state
342
343 def __setstate__(self, state):
344 state = dict(state)
345 self.__init__(INumberDef(state["frames"]), state.get("horizontal"),
346 state.get("vertical"))
347
348 @classmethod
349 def FromXML(cls, doc, element):
350 """Construct using an ElementTree-style element."""
351 horizontal = None
352 vertical = None
353
354 for subelem in element.getchildren():
355 tag = realtag(subelem)
356 if tag == "term":
357 term = INumberDef(subelem.text)
358 elif tag == "horizontal":
359 horizontal = Speed.FromXML(doc, subelem)
360 elif tag == "vertical":
361 vertical = Speed.FromXML(doc, subelem)
362
363 try:
364 return cls(term, horizontal, vertical)
365 except AttributeError:
366 raise ParseError
367
368 def __call__(self, params, rank):
369 frames = self.term(params, rank)
370 horizontal = self.horizontal and self.horizontal(params, rank)
371 vertical = self.vertical and self.vertical(params, rank)
372 return frames, horizontal, vertical
373
374 def __repr__(self):
375 return "%s(%r, horizontal=%r, vertical=%r)" % (
376 type(self).__name__, self.term, self.horizontal, self.vertical)
377
378 class BulletDef(object):
379 """Bullet definition."""
380
381 def __init__(self, actions=(), direction=None, speed=None, tags=(),
382 appearance=None):
383 self.direction = direction
384 self.speed = speed
385 self.actions = list(actions)
386 self.tags = set(tags)
387 self.appearance = appearance
388
389 def __getstate__(self):
390 state = []
391 if self.direction:
392 state.append(("direction", self.direction))
393 if self.speed:
394 state.append(("speed", self.speed))
395 if self.actions:
396 state.append(("actions", self.actions))
397 if self.tags:
398 state.append(("tags", list(self.tags)))
399 if self.appearance:
400 state.append(("appearance", self.appearance))
401 return state
402
403 def __setstate__(self, state):
404 state = dict(state)
405 self.__init__(**state)
406
407 @classmethod
408 def FromXML(cls, doc, element):
409 """Construct using an ElementTree-style element."""
410 actions = []
411 speed = None
412 direction = None
413 tags = set()
414 for subelem in element.getchildren():
415 tag = realtag(subelem)
416 if tag == "direction":
417 direction = Direction.FromXML(doc, subelem)
418 elif tag == "speed":
419 speed = Speed.FromXML(doc, subelem)
420 elif tag == "action":
421 actions.append(ActionDef.FromXML(doc, subelem))
422 elif tag == "actionRef":
423 actions.append(ActionRef.FromXML(doc, subelem))
424 elif tag == "tag":
425 tags.add(subelem.text)
426 dfn = cls(actions, direction, speed, tags)
427 doc._bullets[element.get("label")] = dfn
428 return dfn
429
430 def __call__(self, params, rank):
431 actions = [action(params, rank) for action in self.actions]
432 return (
433 self.direction and self.direction(params, rank),
434 self.speed and self.speed(params, rank),
435 self.tags,
436 self.appearance,
437 actions)
438
439 def __repr__(self):
440 return "%s(direction=%r, speed=%r, actions=%r)" % (
441 type(self).__name__, self.direction, self.speed, self.actions)
442
443 class BulletRef(object):
444 """Create a bullet by name with parameters."""
445
446 def __init__(self, bullet, params=None):
447 self.bullet = bullet
448 self.params = ParamList() if params is None else params
449
450 def __getstate__(self):
451 state = []
452 if self.params.params:
453 params = [param.expr for param in self.params.params]
454 state.append(("params", params))
455 state.append(('bullet', self.bullet))
456 return state
457
458 def __setstate__(self, state):
459 state = dict(state)
460 bullet = state["bullet"]
461 params = [NumberDef(param) for param in state.get("params", [])]
462 self.__init__(bullet, ParamList(params))
463
464 @classmethod
465 def FromXML(cls, doc, element):
466 """Construct using an ElementTree-style element."""
467 bullet = cls(element.get("label"), ParamList.FromXML(doc, element))
468 doc._bullet_refs.append(bullet)
469 return bullet
470
471 def __call__(self, params, rank):
472 return self.bullet(self.params(params, rank), rank)
473
474 def __repr__(self):
475 return "%s(params=%r, bullet=%r)" % (
476 type(self).__name__, self.params, self.bullet)
477
478 class ActionDef(object):
479 """Action definition.
480
481 To support parsing new actions, add tags to
482 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
483 FromXML classmethod, which take the BulletML instance and
484 ElementTree element as arguments.
485 """
486
487 # This is self-referential, so it's filled in later.
488 CONSTRUCTORS = dict()
489
490 def __init__(self, actions):
491 self.actions = list(actions)
492
493 def __getstate__(self):
494 return dict(actions=self.actions)
495
496 def __setstate__(self, state):
497 state = dict(state)
498 self.__init__(state["actions"])
499
500 @classmethod
501 def FromXML(cls, doc, element):
502 """Construct using an ElementTree-style element."""
503 actions = []
504 for subelem in element.getchildren():
505 tag = realtag(subelem)
506 try:
507 ctr = cls.CONSTRUCTORS[tag]
508 except KeyError:
509 continue
510 else:
511 actions.append(ctr.FromXML(doc, subelem))
512 dfn = cls(actions)
513 doc._actions[element.get("label")] = dfn
514 return dfn
515
516 def __call__(self, params, rank):
517 return self.actions, params
518
519 def __repr__(self):
520 return "%s(%r)" % (type(self).__name__, self.actions)
521
522 class ActionRef(object):
523 """Run an action by name with parameters."""
524
525 def __init__(self, action, params=None):
526 self.action = action
527 self.params = params or ParamList()
528
529 def __getstate__(self):
530 state = []
531 if self.params.params:
532 params = [param.expr for param in self.params.params]
533 state.append(("params", params))
534 state.append(('action', self.action))
535 return state
536
537 def __setstate__(self, state):
538 state = dict(state)
539 action = state["action"]
540 params = [NumberDef(param) for param in state.get("params", [])]
541 self.__init__(action, ParamList(params))
542
543 @classmethod
544 def FromXML(cls, doc, element):
545 """Construct using an ElementTree-style element."""
546 action = cls(element.get("label"), ParamList.FromXML(doc, element))
547 doc._action_refs.append(action)
548 return action
549
550 def __call__(self, params, rank):
551 return self.action(self.params(params, rank), rank)
552
553 def __repr__(self):
554 return "%s(params=%r, action=%r)" % (
555 type(self).__name__, self.params, self.action)
556
557 class Offset(object):
558 """Provide an offset to a bullet's initial position."""
559
560 VALID_TYPES = ["relative", "absolute"]
561
562 def __init__(self, type, x, y):
563 if type not in self.VALID_TYPES:
564 raise ValueError("invalid type %r" % type)
565 self.type = intern(type)
566 self.x = x
567 self.y = y
568
569 def __getstate__(self):
570 state = [('type', self.type)]
571 if self.x:
572 state.append(('x', self.x.expr))
573 if self.y:
574 state.append(('y', self.y.expr))
575 return state
576
577 def __setstate__(self, state):
578 state = dict(state)
579 x = NumberDef(state["x"]) if "x" in state else None
580 y = NumberDef(state["y"]) if "y" in state else None
581 self.__init__(state["type"], x, y)
582
583 @classmethod
584 def FromXML(cls, doc, element):
585 """Construct using an ElementTree-style element."""
586 type = element.get("type", "relative")
587 x = None
588 y = None
589 for subelem in element:
590 tag = realtag(subelem)
591 if tag == "x":
592 x = NumberDef(subelem.text)
593 elif tag == "y":
594 y = NumberDef(subelem.text)
595 return cls(type, x, y)
596
597 def __call__(self, params, rank):
598 return (self.x(params, rank) if self.x else 0,
599 self.y(params, rank) if self.y else 0)
600
601 class FireDef(object):
602 """Fire definition (creates a bullet)."""
603
604 def __init__(self, bullet, direction=None, speed=None, offset=None,
605 tags=(), appearance=None):
606 self.bullet = bullet
607 self.direction = direction
608 self.speed = speed
609 self.offset = offset
610 self.tags = set(tags)
611 self.appearance = appearance
612
613 def __getstate__(self):
614 state = []
615 if self.direction:
616 state.append(("direction", self.direction))
617 if self.speed:
618 state.append(("speed", self.speed))
619 if self.offset:
620 state.append(("offset", self.offset))
621 if self.tags:
622 state.append(("tags", list(self.tags)))
623 if self.appearance:
624 state.append(("appearance", self.appearance))
625 try:
626 params = self.bullet.params
627 except AttributeError:
628 state.append(('bullet', self.bullet))
629 else:
630 if params.params:
631 state.append(('bullet', self.bullet))
632 else:
633 # Strip out empty BulletRefs.
634 state.append(('bullet', self.bullet.bullet))
635 return state
636
637 def __setstate__(self, state):
638 state = dict(state)
639 self.__init__(**state)
640
641 @classmethod
642 def FromXML(cls, doc, element):
643 """Construct using an ElementTree-style element."""
644 direction = None
645 speed = None
646 offset = None
647 tags = set()
648 appearance = None
649
650 for subelem in element.getchildren():
651 tag = realtag(subelem)
652 if tag == "direction":
653 direction = Direction.FromXML(doc, subelem, "aim")
654 elif tag == "speed":
655 speed = Speed.FromXML(doc, subelem)
656 elif tag == "bullet":
657 bullet = BulletDef.FromXML(doc, subelem)
658 elif tag == "bulletRef":
659 bullet = BulletRef.FromXML(doc, subelem)
660 elif tag == "offset":
661 offset = Offset.FromXML(doc, subelem)
662 elif tag == "tag":
663 tags.add(subelem.text)
664 elif tag == "appearance":
665 appearance = subelem.text
666 try:
667 fire = cls(bullet, direction, speed, offset, tags, appearance)
668 except UnboundLocalError as exc:
669 raise ParseError(str(exc))
670 else:
671 doc._fires[element.get("label")] = fire
672 return fire
673
674 def __call__(self, params, rank):
675 direction, speed, tags, appearance, actions = self.bullet(params, rank)
676 if self.direction:
677 direction = self.direction(params, rank)
678 if self.speed:
679 speed = self.speed(params, rank)
680 tags = tags.union(self.tags)
681 if self.appearance:
682 appearance = self.appearance
683 return direction, speed, self.offset, tags, appearance, actions
684
685 def __repr__(self):
686 return "%s(direction=%r, speed=%r, bullet=%r)" % (
687 type(self).__name__, self.direction, self.speed, self.bullet)
688
689 class FireRef(object):
690 """Fire a bullet by name with parameters."""
691
692 def __init__(self, fire, params=None):
693 self.fire = fire
694 self.params = params or ParamList()
695
696 def __getstate__(self):
697 state = []
698 if self.params.params:
699 params = [param.expr for param in self.params.params]
700 state.append(("params", params))
701 state.append(('fire', self.fire))
702 return state
703
704 def __setstate__(self, state):
705 state = dict(state)
706 fire = state["fire"]
707 params = [NumberDef(param) for param in state.get("params", [])]
708 self.__init__(fire, ParamList(params))
709
710 @classmethod
711 def FromXML(cls, doc, element):
712 """Construct using an ElementTree-style element."""
713 fired = cls(element.get("label"), ParamList.FromXML(doc, element))
714 doc._fire_refs.append(fired)
715 return fired
716
717 def __call__(self, params, rank):
718 return self.fire(self.params(params, rank), rank)
719
720 def __repr__(self):
721 return "%s(params=%r, fire=%r)" % (
722 type(self).__name__, self.params, self.fire)
723
724 class BulletML(object):
725 """BulletML document.
726
727 A BulletML document is a collection of top-level actions and the
728 base game type.
729
730 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
731 its parsing. It maps tag names to classes with a FromXML
732 classmethod, which take the BulletML instance and ElementTree
733 element as arguments.
734
735 """
736
737 CONSTRUCTORS = dict(
738 bullet=BulletDef,
739 action=ActionDef,
740 fire=FireDef,
741 )
742
743 def __init__(self, type="none", actions=None):
744 self.type = intern(type)
745 self.actions = [] if actions is None else actions
746
747 def __getstate__(self):
748 return [('type', self.type), ('actions', self.actions)]
749
750 def __setstate__(self, state):
751 state = dict(state)
752 self.__init__(state["type"], actions=state.get("actions"))
753
754 @classmethod
755 def FromXML(cls, source):
756 """Return a BulletML instance based on XML."""
757 if not hasattr(source, 'read'):
758 source = StringIO(source)
759
760 tree = ElementTree()
761 root = tree.parse(source)
762
763 doc = cls(type=root.get("type", "none"))
764
765 doc._bullets = {}
766 doc._actions = {}
767 doc._fires = {}
768 doc._bullet_refs = []
769 doc._action_refs = []
770 doc._fire_refs = []
771
772 for element in root.getchildren():
773 tag = realtag(element)
774 if tag in doc.CONSTRUCTORS:
775 doc.CONSTRUCTORS[tag].FromXML(doc, element)
776
777 try:
778 for ref in doc._bullet_refs:
779 ref.bullet = doc._bullets[ref.bullet]
780 for ref in doc._fire_refs:
781 ref.fire = doc._fires[ref.fire]
782 for ref in doc._action_refs:
783 ref.action = doc._actions[ref.action]
784 except KeyError as exc:
785 raise ParseError("unknown reference %s" % exc)
786
787 doc.actions = [act for name, act in doc._actions.items()
788 if name and name.startswith("top")]
789
790 del(doc._bullet_refs)
791 del(doc._action_refs)
792 del(doc._fire_refs)
793 del(doc._bullets)
794 del(doc._actions)
795 del(doc._fires)
796
797 return doc
798
799 @classmethod
800 def FromYAML(cls, source):
801 """Create a BulletML instance based on YAML."""
802
803 # Late import to avoid a circular dependency.
804 try:
805 import bulletml.bulletyaml
806 import yaml
807 except ImportError:
808 raise ParseError("PyYAML is not available")
809 else:
810 try:
811 return yaml.load(source)
812 except Exception as exc:
813 raise ParseError(str(exc))
814
815 @classmethod
816 def FromDocument(cls, source):
817 """Create a BulletML instance based on a seekable file or string.
818
819 This attempts to autodetect if the stream is XML or YAML.
820 """
821 if not hasattr(source, 'read'):
822 source = StringIO(source)
823 start = source.read(1)
824 source.seek(0)
825 if start == "<":
826 return cls.FromXML(source)
827 elif start == "!" or start == "#":
828 return cls.FromYAML(source)
829 else:
830 raise ParseError("unknown initial character %r" % start)
831
832 def __repr__(self):
833 return "%s(type=%r, actions=%r)" % (
834 type(self).__name__, self.type, self.actions)
835
836 ActionDef.CONSTRUCTORS = dict(
837 repeat=Repeat,
838 fire=FireDef,
839 fireRef=FireRef,
840 changeSpeed=ChangeSpeed,
841 changeDirection=ChangeDirection,
842 accel=Accel,
843 wait=Wait,
844 vanish=Vanish,
845 tag=Tag,
846 appearance=Appearance,
847 untag=Untag,
848 action=ActionDef,
849 actionRef=ActionRef)