bacommon.bs

BombSquad specific bits.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""BombSquad specific bits."""
  4
  5from __future__ import annotations
  6
  7import datetime
  8from enum import Enum
  9from dataclasses import dataclass, field
 10from typing import Annotated, override, assert_never
 11
 12from efro.util import pairs_to_flat
 13from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
 14from efro.message import Message, Response
 15
 16# Token counts for our various packs.
 17TOKENS1_COUNT = 50
 18TOKENS2_COUNT = 500
 19TOKENS3_COUNT = 1200
 20TOKENS4_COUNT = 2600
 21
 22
 23@ioprepped
 24@dataclass
 25class PrivatePartyMessage(Message):
 26    """Message asking about info we need for private-party UI."""
 27
 28    need_datacode: Annotated[bool, IOAttrs('d')]
 29
 30    @override
 31    @classmethod
 32    def get_response_types(cls) -> list[type[Response] | None]:
 33        return [PrivatePartyResponse]
 34
 35
 36@ioprepped
 37@dataclass
 38class PrivatePartyResponse(Response):
 39    """Here's that private party UI info you asked for, boss."""
 40
 41    success: Annotated[bool, IOAttrs('s')]
 42    tokens: Annotated[int, IOAttrs('t')]
 43    gold_pass: Annotated[bool, IOAttrs('g')]
 44    datacode: Annotated[str | None, IOAttrs('d')]
 45
 46
 47class ClassicChestAppearance(Enum):
 48    """Appearances bombsquad classic chests can have."""
 49
 50    UNKNOWN = 'u'
 51    DEFAULT = 'd'
 52    L1 = 'l1'
 53    L2 = 'l2'
 54    L3 = 'l3'
 55    L4 = 'l4'
 56    L5 = 'l5'
 57    L6 = 'l6'
 58
 59    @property
 60    def pretty_name(self) -> str:
 61        """Pretty name for the chest in English."""
 62        # pylint: disable=too-many-return-statements
 63        cls = type(self)
 64
 65        if self is cls.UNKNOWN:
 66            return 'Unknown Chest'
 67        if self is cls.DEFAULT:
 68            return 'Chest'
 69        if self is cls.L1:
 70            return 'L1 Chest'
 71        if self is cls.L2:
 72            return 'L2 Chest'
 73        if self is cls.L3:
 74            return 'L3 Chest'
 75        if self is cls.L4:
 76            return 'L4 Chest'
 77        if self is cls.L5:
 78            return 'L5 Chest'
 79        if self is cls.L6:
 80            return 'L6 Chest'
 81
 82        assert_never(self)
 83
 84
 85@ioprepped
 86@dataclass
 87class ClassicAccountLiveData:
 88    """Live account data fed to the client in the bs classic app mode."""
 89
 90    @dataclass
 91    class Chest:
 92        """A lovely chest."""
 93
 94        appearance: Annotated[
 95            ClassicChestAppearance,
 96            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
 97        ]
 98        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
 99        ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]
100
101    class LeagueType(Enum):
102        """Type of league we are in."""
103
104        BRONZE = 'b'
105        SILVER = 's'
106        GOLD = 'g'
107        DIAMOND = 'd'
108
109    tickets: Annotated[int, IOAttrs('ti')]
110
111    tokens: Annotated[int, IOAttrs('to')]
112    gold_pass: Annotated[bool, IOAttrs('g')]
113    remove_ads: Annotated[bool, IOAttrs('r')]
114
115    achievements: Annotated[int, IOAttrs('a')]
116    achievements_total: Annotated[int, IOAttrs('at')]
117
118    league_type: Annotated[LeagueType | None, IOAttrs('lt')]
119    league_num: Annotated[int | None, IOAttrs('ln')]
120    league_rank: Annotated[int | None, IOAttrs('lr')]
121
122    level: Annotated[int, IOAttrs('lv')]
123    xp: Annotated[int, IOAttrs('xp')]
124    xpmax: Annotated[int, IOAttrs('xpm')]
125
126    inbox_count: Annotated[int, IOAttrs('ibc')]
127    inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
128
129    chests: Annotated[dict[str, Chest], IOAttrs('c')]
130
131
132class DisplayItemTypeID(Enum):
133    """Type ID for each of our subclasses."""
134
135    UNKNOWN = 'u'
136    TICKETS = 't'
137    TOKENS = 'k'
138    TEST = 's'
139    CHEST = 'c'
140
141
142class DisplayItem(IOMultiType[DisplayItemTypeID]):
143    """Some amount of something that can be shown or described.
144
145    Used to depict chest contents or other rewards or prices.
146    """
147
148    @override
149    @classmethod
150    def get_type_id(cls) -> DisplayItemTypeID:
151        # Require child classes to supply this themselves. If we did a
152        # full type registry/lookup here it would require us to import
153        # everything and would prevent lazy loading.
154        raise NotImplementedError()
155
156    @override
157    @classmethod
158    def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
159        """Return the subclass for each of our type-ids."""
160        # pylint: disable=cyclic-import
161
162        t = DisplayItemTypeID
163        if type_id is t.UNKNOWN:
164            return UnknownDisplayItem
165        if type_id is t.TICKETS:
166            return TicketsDisplayItem
167        if type_id is t.TOKENS:
168            return TokensDisplayItem
169        if type_id is t.TEST:
170            return TestDisplayItem
171        if type_id is t.CHEST:
172            return ChestDisplayItem
173
174        # Important to make sure we provide all types.
175        assert_never(type_id)
176
177    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
178        """Return a string description and subs for the item.
179
180        These decriptions are baked into the DisplayItemWrapper and
181        should be accessed from there when available. This allows
182        clients to give descriptions even for newer display items they
183        don't recognize.
184        """
185        raise NotImplementedError()
186
187    # Implement fallbacks so client can digest item lists even if they
188    # contain unrecognized stuff. DisplayItemWrapper contains basic
189    # baked down info that they can still use in such cases.
190    @override
191    @classmethod
192    def get_unknown_type_fallback(cls) -> DisplayItem:
193        return UnknownDisplayItem()
194
195
196@ioprepped
197@dataclass
198class UnknownDisplayItem(DisplayItem):
199    """Something we don't know how to display."""
200
201    @override
202    @classmethod
203    def get_type_id(cls) -> DisplayItemTypeID:
204        return DisplayItemTypeID.UNKNOWN
205
206    @override
207    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
208        import logging
209
210        # Make noise but don't break.
211        logging.exception(
212            'UnknownDisplayItem.get_description() should never be called.'
213            ' Always access descriptions on the DisplayItemWrapper.'
214        )
215        return 'Unknown', []
216
217
218@ioprepped
219@dataclass
220class TicketsDisplayItem(DisplayItem):
221    """Some amount of tickets."""
222
223    count: Annotated[int, IOAttrs('c')]
224
225    @override
226    @classmethod
227    def get_type_id(cls) -> DisplayItemTypeID:
228        return DisplayItemTypeID.TICKETS
229
230    @override
231    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
232        return '${C} Tickets', [('${C}', str(self.count))]
233
234
235@ioprepped
236@dataclass
237class TokensDisplayItem(DisplayItem):
238    """Some amount of tokens."""
239
240    count: Annotated[int, IOAttrs('c')]
241
242    @override
243    @classmethod
244    def get_type_id(cls) -> DisplayItemTypeID:
245        return DisplayItemTypeID.TOKENS
246
247    @override
248    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
249        return '${C} Tokens', [('${C}', str(self.count))]
250
251
252@ioprepped
253@dataclass
254class TestDisplayItem(DisplayItem):
255    """Fills usable space for a display-item - good for calibration."""
256
257    @override
258    @classmethod
259    def get_type_id(cls) -> DisplayItemTypeID:
260        return DisplayItemTypeID.TEST
261
262    @override
263    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
264        return 'Test Display Item Here', []
265
266
267@ioprepped
268@dataclass
269class ChestDisplayItem(DisplayItem):
270    """Display a chest."""
271
272    appearance: Annotated[ClassicChestAppearance, IOAttrs('a')]
273
274    @override
275    @classmethod
276    def get_type_id(cls) -> DisplayItemTypeID:
277        return DisplayItemTypeID.CHEST
278
279    @override
280    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
281        return self.appearance.pretty_name, []
282
283
284@ioprepped
285@dataclass
286class DisplayItemWrapper:
287    """Wraps a DisplayItem and common info."""
288
289    item: Annotated[DisplayItem, IOAttrs('i')]
290    description: Annotated[str, IOAttrs('d')]
291    description_subs: Annotated[list[str] | None, IOAttrs('s')]
292
293    @classmethod
294    def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
295        """Convenience method to wrap a DisplayItem."""
296        desc, subs = item.get_description()
297        return DisplayItemWrapper(item, desc, pairs_to_flat(subs))
298
299
300@ioprepped
301@dataclass
302class ChestInfoMessage(Message):
303    """Request info about a chest."""
304
305    chest_id: Annotated[str, IOAttrs('i')]
306
307    @override
308    @classmethod
309    def get_response_types(cls) -> list[type[Response] | None]:
310        return [ChestInfoResponse]
311
312
313@ioprepped
314@dataclass
315class ChestInfoResponse(Response):
316    """Here's that chest info you asked for, boss."""
317
318    @dataclass
319    class Chest:
320        """A lovely chest."""
321
322        @dataclass
323        class PrizeSet:
324            """A possible set of prizes for this chest."""
325
326            weight: Annotated[float, IOAttrs('w')]
327            contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
328
329        appearance: Annotated[
330            ClassicChestAppearance,
331            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
332        ]
333
334        # How much it costs to unlock *now*.
335        unlock_tokens: Annotated[int, IOAttrs('tk')]
336
337        # When it unlocks on its own.
338        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
339
340        # Possible prizes we contain.
341        prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
342
343        # Are ads allowed now?
344        ad_allow: Annotated[bool, IOAttrs('aa')]
345
346    chest: Annotated[Chest | None, IOAttrs('c')]
347    user_tokens: Annotated[int | None, IOAttrs('t')]
348
349
350class ClientUITypeID(Enum):
351    """Type ID for each of our subclasses."""
352
353    UNKNOWN = 'u'
354    BASIC = 'b'
355
356
357class ClientUI(IOMultiType[ClientUITypeID]):
358    """Defines some user interface on the client."""
359
360    @override
361    @classmethod
362    def get_type_id(cls) -> ClientUITypeID:
363        # Require child classes to supply this themselves. If we did a
364        # full type registry/lookup here it would require us to import
365        # everything and would prevent lazy loading.
366        raise NotImplementedError()
367
368    @override
369    @classmethod
370    def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
371        """Return the subclass for each of our type-ids."""
372        # pylint: disable=cyclic-import
373        out: type[ClientUI]
374
375        t = ClientUITypeID
376        if type_id is t.UNKNOWN:
377            out = UnknownClientUI
378        elif type_id is t.BASIC:
379            out = BasicClientUI
380        else:
381            # Important to make sure we provide all types.
382            assert_never(type_id)
383        return out
384
385    @override
386    @classmethod
387    def get_unknown_type_fallback(cls) -> ClientUI:
388        # If we encounter some future message type we don't know
389        # anything about, drop in a placeholder.
390        return UnknownClientUI()
391
392
393@ioprepped
394@dataclass
395class UnknownClientUI(ClientUI):
396    """Fallback type for unrecognized entries."""
397
398    @override
399    @classmethod
400    def get_type_id(cls) -> ClientUITypeID:
401        return ClientUITypeID.UNKNOWN
402
403
404class BasicClientUIComponentTypeID(Enum):
405    """Type ID for each of our subclasses."""
406
407    UNKNOWN = 'u'
408    TEXT = 't'
409    LINK = 'l'
410    BS_CLASSIC_TOURNEY_RESULT = 'ct'
411    DISPLAY_ITEMS = 'di'
412    EXPIRE_TIME = 'd'
413
414
415class BasicClientUIComponent(IOMultiType[BasicClientUIComponentTypeID]):
416    """Top level class for our multitype."""
417
418    @override
419    @classmethod
420    def get_type_id(cls) -> BasicClientUIComponentTypeID:
421        # Require child classes to supply this themselves. If we did a
422        # full type registry/lookup here it would require us to import
423        # everything and would prevent lazy loading.
424        raise NotImplementedError()
425
426    @override
427    @classmethod
428    def get_type(
429        cls, type_id: BasicClientUIComponentTypeID
430    ) -> type[BasicClientUIComponent]:
431        """Return the subclass for each of our type-ids."""
432        # pylint: disable=cyclic-import
433
434        t = BasicClientUIComponentTypeID
435        if type_id is t.UNKNOWN:
436            return BasicClientUIComponentUnknown
437        if type_id is t.TEXT:
438            return BasicClientUIComponentText
439        if type_id is t.LINK:
440            return BasicClientUIComponentLink
441        if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
442            return BasicClientUIBsClassicTourneyResult
443        if type_id is t.DISPLAY_ITEMS:
444            return BasicClientUIDisplayItems
445        if type_id is t.EXPIRE_TIME:
446            return BasicClientUIExpireTime
447
448        # Important to make sure we provide all types.
449        assert_never(type_id)
450
451    @override
452    @classmethod
453    def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
454        # If we encounter some future message type we don't know
455        # anything about, drop in a placeholder.
456        return BasicClientUIComponentUnknown()
457
458
459@ioprepped
460@dataclass
461class BasicClientUIComponentUnknown(BasicClientUIComponent):
462    """An unknown basic client component type.
463
464    In practice these should never show up since the master-server
465    generates these on the fly for the client and so should not send
466    clients one they can't digest.
467    """
468
469    @override
470    @classmethod
471    def get_type_id(cls) -> BasicClientUIComponentTypeID:
472        return BasicClientUIComponentTypeID.UNKNOWN
473
474
475@ioprepped
476@dataclass
477class BasicClientUIComponentText(BasicClientUIComponent):
478    """Show some text in the inbox message."""
479
480    text: Annotated[str, IOAttrs('t')]
481    subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
482        default_factory=list
483    )
484    scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0
485    color: Annotated[
486        tuple[float, float, float, float], IOAttrs('c', store_default=False)
487    ] = (1.0, 1.0, 1.0, 1.0)
488    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
489    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
490
491    @override
492    @classmethod
493    def get_type_id(cls) -> BasicClientUIComponentTypeID:
494        return BasicClientUIComponentTypeID.TEXT
495
496
497@ioprepped
498@dataclass
499class BasicClientUIComponentLink(BasicClientUIComponent):
500    """Show a link in the inbox message."""
501
502    url: Annotated[str, IOAttrs('u')]
503    label: Annotated[str, IOAttrs('l')]
504    subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
505        default_factory=list
506    )
507    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
508    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
509
510    @override
511    @classmethod
512    def get_type_id(cls) -> BasicClientUIComponentTypeID:
513        return BasicClientUIComponentTypeID.LINK
514
515
516@ioprepped
517@dataclass
518class BasicClientUIBsClassicTourneyResult(BasicClientUIComponent):
519    """Show info about a classic tourney."""
520
521    tournament_id: Annotated[str, IOAttrs('t')]
522    game: Annotated[str, IOAttrs('g')]
523    players: Annotated[int, IOAttrs('p')]
524    rank: Annotated[int, IOAttrs('r')]
525    trophy: Annotated[str | None, IOAttrs('tr')]
526    prizes: Annotated[list[DisplayItemWrapper], IOAttrs('pr')]
527
528    @override
529    @classmethod
530    def get_type_id(cls) -> BasicClientUIComponentTypeID:
531        return BasicClientUIComponentTypeID.BS_CLASSIC_TOURNEY_RESULT
532
533
534@ioprepped
535@dataclass
536class BasicClientUIDisplayItems(BasicClientUIComponent):
537    """Show some display-items."""
538
539    items: Annotated[list[DisplayItemWrapper], IOAttrs('d')]
540    width: Annotated[float, IOAttrs('w')] = 100.0
541    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
542    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
543
544    @override
545    @classmethod
546    def get_type_id(cls) -> BasicClientUIComponentTypeID:
547        return BasicClientUIComponentTypeID.DISPLAY_ITEMS
548
549
550@ioprepped
551@dataclass
552class BasicClientUIExpireTime(BasicClientUIComponent):
553    """Show expire-time."""
554
555    time: Annotated[datetime.datetime, IOAttrs('d')]
556    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
557    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
558
559    @override
560    @classmethod
561    def get_type_id(cls) -> BasicClientUIComponentTypeID:
562        return BasicClientUIComponentTypeID.EXPIRE_TIME
563
564
565@ioprepped
566@dataclass
567class BasicClientUI(ClientUI):
568    """A basic UI for the client."""
569
570    class ButtonLabel(Enum):
571        """Distinct button labels we support."""
572
573        UNKNOWN = 'u'
574        OK = 'o'
575        APPLY = 'a'
576        CANCEL = 'c'
577        ACCEPT = 'ac'
578        DECLINE = 'dn'
579        IGNORE = 'ig'
580        CLAIM = 'cl'
581        DISCARD = 'd'
582
583    class InteractionStyle(Enum):
584        """Overall interaction styles we support."""
585
586        UNKNOWN = 'u'
587        BUTTON_POSITIVE = 'p'
588        BUTTON_POSITIVE_NEGATIVE = 'pn'
589
590    components: Annotated[list[BasicClientUIComponent], IOAttrs('s')]
591
592    interaction_style: Annotated[
593        InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN)
594    ] = InteractionStyle.BUTTON_POSITIVE
595
596    button_label_positive: Annotated[
597        ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN)
598    ] = ButtonLabel.OK
599
600    button_label_negative: Annotated[
601        ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN)
602    ] = ButtonLabel.CANCEL
603
604    @override
605    @classmethod
606    def get_type_id(cls) -> ClientUITypeID:
607        return ClientUITypeID.BASIC
608
609    def contains_unknown_elements(self) -> bool:
610        """Whether something within us is an unknown type or enum."""
611        return (
612            self.interaction_style is self.InteractionStyle.UNKNOWN
613            or self.button_label_positive is self.ButtonLabel.UNKNOWN
614            or self.button_label_negative is self.ButtonLabel.UNKNOWN
615            or any(
616                c.get_type_id() is BasicClientUIComponentTypeID.UNKNOWN
617                for c in self.components
618            )
619        )
620
621
622@ioprepped
623@dataclass
624class ClientUIWrapper:
625    """Wrapper for a ClientUI and its common data."""
626
627    id: Annotated[str, IOAttrs('i')]
628    createtime: Annotated[datetime.datetime, IOAttrs('c')]
629    ui: Annotated[ClientUI, IOAttrs('e')]
630
631
632@ioprepped
633@dataclass
634class InboxRequestMessage(Message):
635    """Message requesting our inbox."""
636
637    @override
638    @classmethod
639    def get_response_types(cls) -> list[type[Response] | None]:
640        return [InboxRequestResponse]
641
642
643@ioprepped
644@dataclass
645class InboxRequestResponse(Response):
646    """Here's that inbox contents you asked for, boss."""
647
648    wrappers: Annotated[list[ClientUIWrapper], IOAttrs('w')]
649
650    # Printable error if something goes wrong.
651    error: Annotated[str | None, IOAttrs('e')] = None
652
653
654class ClientUIAction(Enum):
655    """Types of actions we can run."""
656
657    BUTTON_PRESS_POSITIVE = 'p'
658    BUTTON_PRESS_NEGATIVE = 'n'
659
660
661class ClientEffectTypeID(Enum):
662    """Type ID for each of our subclasses."""
663
664    UNKNOWN = 'u'
665    SCREEN_MESSAGE = 'm'
666    SOUND = 's'
667    DELAY = 'd'
668    CHEST_WAIT_TIME_ANIMATION = 't'
669    TICKETS_ANIMATION = 'ta'
670    TOKENS_ANIMATION = 'toa'
671
672
673class ClientEffect(IOMultiType[ClientEffectTypeID]):
674    """Something that can happen on the client.
675
676    This can include screen messages, sounds, visual effects, etc.
677    """
678
679    @override
680    @classmethod
681    def get_type_id(cls) -> ClientEffectTypeID:
682        # Require child classes to supply this themselves. If we did a
683        # full type registry/lookup here it would require us to import
684        # everything and would prevent lazy loading.
685        raise NotImplementedError()
686
687    @override
688    @classmethod
689    def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
690        """Return the subclass for each of our type-ids."""
691        # pylint: disable=cyclic-import
692        # pylint: disable=too-many-return-statements
693
694        t = ClientEffectTypeID
695        if type_id is t.UNKNOWN:
696            return ClientEffectUnknown
697        if type_id is t.SCREEN_MESSAGE:
698            return ClientEffectScreenMessage
699        if type_id is t.SOUND:
700            return ClientEffectSound
701        if type_id is t.DELAY:
702            return ClientEffectDelay
703        if type_id is t.CHEST_WAIT_TIME_ANIMATION:
704            return ClientEffectChestWaitTimeAnimation
705        if type_id is t.TICKETS_ANIMATION:
706            return ClientEffectTicketsAnimation
707        if type_id is t.TOKENS_ANIMATION:
708            return ClientEffectTokensAnimation
709
710        # Important to make sure we provide all types.
711        assert_never(type_id)
712
713    @override
714    @classmethod
715    def get_unknown_type_fallback(cls) -> ClientEffect:
716        # If we encounter some future message type we don't know
717        # anything about, drop in a placeholder.
718        return ClientEffectUnknown()
719
720
721@ioprepped
722@dataclass
723class ClientEffectUnknown(ClientEffect):
724    """Fallback substitute for types we don't recognize."""
725
726    @override
727    @classmethod
728    def get_type_id(cls) -> ClientEffectTypeID:
729        return ClientEffectTypeID.UNKNOWN
730
731
732@ioprepped
733@dataclass
734class ClientEffectScreenMessage(ClientEffect):
735    """Display a screen-message."""
736
737    message: Annotated[str, IOAttrs('m')]
738    subs: Annotated[list[str], IOAttrs('s')]
739    color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0)
740
741    @override
742    @classmethod
743    def get_type_id(cls) -> ClientEffectTypeID:
744        return ClientEffectTypeID.SCREEN_MESSAGE
745
746
747@ioprepped
748@dataclass
749class ClientEffectSound(ClientEffect):
750    """Play a sound."""
751
752    class Sound(Enum):
753        """Sounds that can be made alongside the message."""
754
755        UNKNOWN = 'u'
756        CASH_REGISTER = 'c'
757        ERROR = 'e'
758        POWER_DOWN = 'p'
759        GUN_COCKING = 'g'
760
761    sound: Annotated[Sound, IOAttrs('s', enum_fallback=Sound.UNKNOWN)]
762    volume: Annotated[float, IOAttrs('v')] = 1.0
763
764    @override
765    @classmethod
766    def get_type_id(cls) -> ClientEffectTypeID:
767        return ClientEffectTypeID.SOUND
768
769
770@ioprepped
771@dataclass
772class ClientEffectChestWaitTimeAnimation(ClientEffect):
773    """Animate chest wait time changing."""
774
775    chestid: Annotated[str, IOAttrs('c')]
776    duration: Annotated[float, IOAttrs('u')]
777    startvalue: Annotated[datetime.datetime, IOAttrs('o')]
778    endvalue: Annotated[datetime.datetime, IOAttrs('n')]
779
780    @override
781    @classmethod
782    def get_type_id(cls) -> ClientEffectTypeID:
783        return ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION
784
785
786@ioprepped
787@dataclass
788class ClientEffectTicketsAnimation(ClientEffect):
789    """Animate tickets count."""
790
791    duration: Annotated[float, IOAttrs('u')]
792    startvalue: Annotated[int, IOAttrs('s')]
793    endvalue: Annotated[int, IOAttrs('e')]
794
795    @override
796    @classmethod
797    def get_type_id(cls) -> ClientEffectTypeID:
798        return ClientEffectTypeID.TICKETS_ANIMATION
799
800
801@ioprepped
802@dataclass
803class ClientEffectTokensAnimation(ClientEffect):
804    """Animate tokens count."""
805
806    duration: Annotated[float, IOAttrs('u')]
807    startvalue: Annotated[int, IOAttrs('s')]
808    endvalue: Annotated[int, IOAttrs('e')]
809
810    @override
811    @classmethod
812    def get_type_id(cls) -> ClientEffectTypeID:
813        return ClientEffectTypeID.TOKENS_ANIMATION
814
815
816@ioprepped
817@dataclass
818class ClientEffectDelay(ClientEffect):
819    """Delay effect processing."""
820
821    seconds: Annotated[float, IOAttrs('s')]
822
823    @override
824    @classmethod
825    def get_type_id(cls) -> ClientEffectTypeID:
826        return ClientEffectTypeID.DELAY
827
828
829@ioprepped
830@dataclass
831class ClientUIActionMessage(Message):
832    """Do something to a client ui."""
833
834    id: Annotated[str, IOAttrs('i')]
835    action: Annotated[ClientUIAction, IOAttrs('a')]
836
837    @override
838    @classmethod
839    def get_response_types(cls) -> list[type[Response] | None]:
840        return [ClientUIActionResponse]
841
842
843@ioprepped
844@dataclass
845class ClientUIActionResponse(Response):
846    """Did something to that inbox entry, boss."""
847
848    class ErrorType(Enum):
849        """Types of errors that may have occurred."""
850
851        # Probably a future error type we don't recognize.
852        UNKNOWN = 'u'
853
854        # Something went wrong on the server, but specifics are not
855        # relevant.
856        INTERNAL = 'i'
857
858        # The entry expired on the server. In various cases such as 'ok'
859        # buttons this can generally be ignored.
860        EXPIRED = 'e'
861
862    error_type: Annotated[
863        ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN)
864    ]
865
866    # User facing error message in the case of errors.
867    error_message: Annotated[str | None, IOAttrs('em')]
868
869    effects: Annotated[list[ClientEffect], IOAttrs('fx')]
870
871
872@ioprepped
873@dataclass
874class ScoreSubmitMessage(Message):
875    """Let the server know we got some score in something."""
876
877    score_token: Annotated[str, IOAttrs('t')]
878
879    @override
880    @classmethod
881    def get_response_types(cls) -> list[type[Response] | None]:
882        return [ScoreSubmitResponse]
883
884
885@ioprepped
886@dataclass
887class ScoreSubmitResponse(Response):
888    """Did something to that inbox entry, boss."""
889
890    # Things we should show on our end.
891    effects: Annotated[list[ClientEffect], IOAttrs('fx')]
892
893
894@ioprepped
895@dataclass
896class ChestActionMessage(Message):
897    """Request action about a chest."""
898
899    class Action(Enum):
900        """Types of actions we can request."""
901
902        # Unlocking (for free or with tokens).
903        UNLOCK = 'u'
904
905        # Watched an ad to reduce wait.
906        AD = 'ad'
907
908    action: Annotated[Action, IOAttrs('a')]
909
910    # Tokens we are paying (only applies to unlock).
911    token_payment: Annotated[int, IOAttrs('t')]
912
913    chest_id: Annotated[str, IOAttrs('i')]
914
915    @override
916    @classmethod
917    def get_response_types(cls) -> list[type[Response] | None]:
918        return [ChestActionResponse]
919
920
921@ioprepped
922@dataclass
923class ChestActionResponse(Response):
924    """Here's the results of that action you asked for, boss."""
925
926    # Tokens that were actually charged.
927    tokens_charged: Annotated[int, IOAttrs('t')] = 0
928
929    # If present, signifies the chest has been opened and we should show
930    # the user this stuff that was in it.
931    contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None
932
933    # If contents are present, which of the chest's prize-sets they
934    # represent.
935    prizeindex: Annotated[int, IOAttrs('i')] = 0
936
937    # Printable error if something goes wrong.
938    error: Annotated[str | None, IOAttrs('e')] = None
939
940    # Printable warning. Shown in orange with an error sound. Does not
941    # mean the action failed; only that there's something to tell the
942    # users such as 'It looks like you are faking ad views; stop it or
943    # you won't have ad options anymore.'
944    warning: Annotated[str | None, IOAttrs('w', store_default=False)] = None
945
946    # Printable success message. Shown in green with a cash-register
947    # sound. Can be used for things like successful wait reductions via
948    # ad views. Used in builds earlier than 22311; can remove once
949    # 22311+ is ubiquitous.
950    success_msg: Annotated[str | None, IOAttrs('s', store_default=False)] = None
951
952    # Effects to show on the client. Replaces warning and success_msg in
953    # build 22311 or newer.
954    effects: Annotated[
955        list[ClientEffect], IOAttrs('fx', store_default=False)
956    ] = field(default_factory=list)
TOKENS1_COUNT = 50
TOKENS2_COUNT = 500
TOKENS3_COUNT = 1200
TOKENS4_COUNT = 2600
@ioprepped
@dataclass
class PrivatePartyMessage(efro.message._message.Message):
24@ioprepped
25@dataclass
26class PrivatePartyMessage(Message):
27    """Message asking about info we need for private-party UI."""
28
29    need_datacode: Annotated[bool, IOAttrs('d')]
30
31    @override
32    @classmethod
33    def get_response_types(cls) -> list[type[Response] | None]:
34        return [PrivatePartyResponse]

Message asking about info we need for private-party UI.

PrivatePartyMessage( need_datacode: Annotated[bool, <efro.dataclassio.IOAttrs object>])
need_datacode: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x107197e30>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
31    @override
32    @classmethod
33    def get_response_types(cls) -> list[type[Response] | None]:
34        return [PrivatePartyResponse]

Return all Response types this Message can return when sent.

The default implementation specifies a None return type.

@ioprepped
@dataclass
class PrivatePartyResponse(efro.message._message.Response):
37@ioprepped
38@dataclass
39class PrivatePartyResponse(Response):
40    """Here's that private party UI info you asked for, boss."""
41
42    success: Annotated[bool, IOAttrs('s')]
43    tokens: Annotated[int, IOAttrs('t')]
44    gold_pass: Annotated[bool, IOAttrs('g')]
45    datacode: Annotated[str | None, IOAttrs('d')]

Here's that private party UI info you asked for, boss.

PrivatePartyResponse( success: Annotated[bool, <efro.dataclassio.IOAttrs object>], tokens: Annotated[int, <efro.dataclassio.IOAttrs object>], gold_pass: Annotated[bool, <efro.dataclassio.IOAttrs object>], datacode: Annotated[str | None, <efro.dataclassio.IOAttrs object>])
success: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x10721ed50>]
tokens: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10721fb90>]
gold_pass: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x10721c4d0>]
datacode: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x10721c710>]
class ClassicChestAppearance(enum.Enum):
48class ClassicChestAppearance(Enum):
49    """Appearances bombsquad classic chests can have."""
50
51    UNKNOWN = 'u'
52    DEFAULT = 'd'
53    L1 = 'l1'
54    L2 = 'l2'
55    L3 = 'l3'
56    L4 = 'l4'
57    L5 = 'l5'
58    L6 = 'l6'
59
60    @property
61    def pretty_name(self) -> str:
62        """Pretty name for the chest in English."""
63        # pylint: disable=too-many-return-statements
64        cls = type(self)
65
66        if self is cls.UNKNOWN:
67            return 'Unknown Chest'
68        if self is cls.DEFAULT:
69            return 'Chest'
70        if self is cls.L1:
71            return 'L1 Chest'
72        if self is cls.L2:
73            return 'L2 Chest'
74        if self is cls.L3:
75            return 'L3 Chest'
76        if self is cls.L4:
77            return 'L4 Chest'
78        if self is cls.L5:
79            return 'L5 Chest'
80        if self is cls.L6:
81            return 'L6 Chest'
82
83        assert_never(self)

Appearances bombsquad classic chests can have.

pretty_name: str
60    @property
61    def pretty_name(self) -> str:
62        """Pretty name for the chest in English."""
63        # pylint: disable=too-many-return-statements
64        cls = type(self)
65
66        if self is cls.UNKNOWN:
67            return 'Unknown Chest'
68        if self is cls.DEFAULT:
69            return 'Chest'
70        if self is cls.L1:
71            return 'L1 Chest'
72        if self is cls.L2:
73            return 'L2 Chest'
74        if self is cls.L3:
75            return 'L3 Chest'
76        if self is cls.L4:
77            return 'L4 Chest'
78        if self is cls.L5:
79            return 'L5 Chest'
80        if self is cls.L6:
81            return 'L6 Chest'
82
83        assert_never(self)

Pretty name for the chest in English.

@ioprepped
@dataclass
class ClassicAccountLiveData:
 86@ioprepped
 87@dataclass
 88class ClassicAccountLiveData:
 89    """Live account data fed to the client in the bs classic app mode."""
 90
 91    @dataclass
 92    class Chest:
 93        """A lovely chest."""
 94
 95        appearance: Annotated[
 96            ClassicChestAppearance,
 97            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
 98        ]
 99        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
100        ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]
101
102    class LeagueType(Enum):
103        """Type of league we are in."""
104
105        BRONZE = 'b'
106        SILVER = 's'
107        GOLD = 'g'
108        DIAMOND = 'd'
109
110    tickets: Annotated[int, IOAttrs('ti')]
111
112    tokens: Annotated[int, IOAttrs('to')]
113    gold_pass: Annotated[bool, IOAttrs('g')]
114    remove_ads: Annotated[bool, IOAttrs('r')]
115
116    achievements: Annotated[int, IOAttrs('a')]
117    achievements_total: Annotated[int, IOAttrs('at')]
118
119    league_type: Annotated[LeagueType | None, IOAttrs('lt')]
120    league_num: Annotated[int | None, IOAttrs('ln')]
121    league_rank: Annotated[int | None, IOAttrs('lr')]
122
123    level: Annotated[int, IOAttrs('lv')]
124    xp: Annotated[int, IOAttrs('xp')]
125    xpmax: Annotated[int, IOAttrs('xpm')]
126
127    inbox_count: Annotated[int, IOAttrs('ibc')]
128    inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
129
130    chests: Annotated[dict[str, Chest], IOAttrs('c')]

Live account data fed to the client in the bs classic app mode.

ClassicAccountLiveData( tickets: Annotated[int, <efro.dataclassio.IOAttrs object>], tokens: Annotated[int, <efro.dataclassio.IOAttrs object>], gold_pass: Annotated[bool, <efro.dataclassio.IOAttrs object>], remove_ads: Annotated[bool, <efro.dataclassio.IOAttrs object>], achievements: Annotated[int, <efro.dataclassio.IOAttrs object>], achievements_total: Annotated[int, <efro.dataclassio.IOAttrs object>], league_type: Annotated[ClassicAccountLiveData.LeagueType | None, <efro.dataclassio.IOAttrs object>], league_num: Annotated[int | None, <efro.dataclassio.IOAttrs object>], league_rank: Annotated[int | None, <efro.dataclassio.IOAttrs object>], level: Annotated[int, <efro.dataclassio.IOAttrs object>], xp: Annotated[int, <efro.dataclassio.IOAttrs object>], xpmax: Annotated[int, <efro.dataclassio.IOAttrs object>], inbox_count: Annotated[int, <efro.dataclassio.IOAttrs object>], inbox_count_is_max: Annotated[bool, <efro.dataclassio.IOAttrs object>], chests: Annotated[dict[str, ClassicAccountLiveData.Chest], <efro.dataclassio.IOAttrs object>])
tickets: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10721e570>]
tokens: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10721eae0>]
gold_pass: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x10721ec30>]
remove_ads: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x10721edb0>]
achievements: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10721eed0>]
achievements_total: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10721eff0>]
league_type: Annotated[ClassicAccountLiveData.LeagueType | None, <efro.dataclassio.IOAttrs object at 0x10721f0b0>]
league_num: Annotated[int | None, <efro.dataclassio.IOAttrs object at 0x10721f260>]
league_rank: Annotated[int | None, <efro.dataclassio.IOAttrs object at 0x10721f3b0>]
level: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10721f530>]
xp: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10721f680>]
xpmax: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10721f7a0>]
inbox_count: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10721f8f0>]
inbox_count_is_max: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x10721fa40>]
chests: Annotated[dict[str, ClassicAccountLiveData.Chest], <efro.dataclassio.IOAttrs object at 0x10721f9b0>]
@dataclass
class ClassicAccountLiveData.Chest:
 91    @dataclass
 92    class Chest:
 93        """A lovely chest."""
 94
 95        appearance: Annotated[
 96            ClassicChestAppearance,
 97            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
 98        ]
 99        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
100        ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]

A lovely chest.

ClassicAccountLiveData.Chest( appearance: Annotated[ClassicChestAppearance, <efro.dataclassio.IOAttrs object>], unlock_time: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object>], ad_allow_time: Annotated[datetime.datetime | None, <efro.dataclassio.IOAttrs object>])
appearance: Annotated[ClassicChestAppearance, <efro.dataclassio.IOAttrs object at 0x1071e74a0>]
unlock_time: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x1071e5bb0>]
ad_allow_time: Annotated[datetime.datetime | None, <efro.dataclassio.IOAttrs object at 0x1071e5c70>]
class ClassicAccountLiveData.LeagueType(enum.Enum):
102    class LeagueType(Enum):
103        """Type of league we are in."""
104
105        BRONZE = 'b'
106        SILVER = 's'
107        GOLD = 'g'
108        DIAMOND = 'd'

Type of league we are in.

BRONZE = <LeagueType.BRONZE: 'b'>
SILVER = <LeagueType.SILVER: 's'>
GOLD = <LeagueType.GOLD: 'g'>
DIAMOND = <LeagueType.DIAMOND: 'd'>
class DisplayItemTypeID(enum.Enum):
133class DisplayItemTypeID(Enum):
134    """Type ID for each of our subclasses."""
135
136    UNKNOWN = 'u'
137    TICKETS = 't'
138    TOKENS = 'k'
139    TEST = 's'
140    CHEST = 'c'

Type ID for each of our subclasses.

UNKNOWN = <DisplayItemTypeID.UNKNOWN: 'u'>
TICKETS = <DisplayItemTypeID.TICKETS: 't'>
TOKENS = <DisplayItemTypeID.TOKENS: 'k'>
TEST = <DisplayItemTypeID.TEST: 's'>
CHEST = <DisplayItemTypeID.CHEST: 'c'>
class DisplayItem(efro.dataclassio._base.IOMultiType[bacommon.bs.DisplayItemTypeID]):
143class DisplayItem(IOMultiType[DisplayItemTypeID]):
144    """Some amount of something that can be shown or described.
145
146    Used to depict chest contents or other rewards or prices.
147    """
148
149    @override
150    @classmethod
151    def get_type_id(cls) -> DisplayItemTypeID:
152        # Require child classes to supply this themselves. If we did a
153        # full type registry/lookup here it would require us to import
154        # everything and would prevent lazy loading.
155        raise NotImplementedError()
156
157    @override
158    @classmethod
159    def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
160        """Return the subclass for each of our type-ids."""
161        # pylint: disable=cyclic-import
162
163        t = DisplayItemTypeID
164        if type_id is t.UNKNOWN:
165            return UnknownDisplayItem
166        if type_id is t.TICKETS:
167            return TicketsDisplayItem
168        if type_id is t.TOKENS:
169            return TokensDisplayItem
170        if type_id is t.TEST:
171            return TestDisplayItem
172        if type_id is t.CHEST:
173            return ChestDisplayItem
174
175        # Important to make sure we provide all types.
176        assert_never(type_id)
177
178    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
179        """Return a string description and subs for the item.
180
181        These decriptions are baked into the DisplayItemWrapper and
182        should be accessed from there when available. This allows
183        clients to give descriptions even for newer display items they
184        don't recognize.
185        """
186        raise NotImplementedError()
187
188    # Implement fallbacks so client can digest item lists even if they
189    # contain unrecognized stuff. DisplayItemWrapper contains basic
190    # baked down info that they can still use in such cases.
191    @override
192    @classmethod
193    def get_unknown_type_fallback(cls) -> DisplayItem:
194        return UnknownDisplayItem()

Some amount of something that can be shown or described.

Used to depict chest contents or other rewards or prices.

@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
149    @override
150    @classmethod
151    def get_type_id(cls) -> DisplayItemTypeID:
152        # Require child classes to supply this themselves. If we did a
153        # full type registry/lookup here it would require us to import
154        # everything and would prevent lazy loading.
155        raise NotImplementedError()

Return the type-id for this subclass.

@override
@classmethod
def get_type( cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
157    @override
158    @classmethod
159    def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
160        """Return the subclass for each of our type-ids."""
161        # pylint: disable=cyclic-import
162
163        t = DisplayItemTypeID
164        if type_id is t.UNKNOWN:
165            return UnknownDisplayItem
166        if type_id is t.TICKETS:
167            return TicketsDisplayItem
168        if type_id is t.TOKENS:
169            return TokensDisplayItem
170        if type_id is t.TEST:
171            return TestDisplayItem
172        if type_id is t.CHEST:
173            return ChestDisplayItem
174
175        # Important to make sure we provide all types.
176        assert_never(type_id)

Return the subclass for each of our type-ids.

def get_description(self) -> tuple[str, list[tuple[str, str]]]:
178    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
179        """Return a string description and subs for the item.
180
181        These decriptions are baked into the DisplayItemWrapper and
182        should be accessed from there when available. This allows
183        clients to give descriptions even for newer display items they
184        don't recognize.
185        """
186        raise NotImplementedError()

Return a string description and subs for the item.

These decriptions are baked into the DisplayItemWrapper and should be accessed from there when available. This allows clients to give descriptions even for newer display items they don't recognize.

@override
@classmethod
def get_unknown_type_fallback(cls) -> DisplayItem:
191    @override
192    @classmethod
193    def get_unknown_type_fallback(cls) -> DisplayItem:
194        return UnknownDisplayItem()

Return a fallback object in cases of unrecognized types.

This can allow newer data to remain readable in older environments. Use caution with this option, however, as it effectively modifies data.

@ioprepped
@dataclass
class UnknownDisplayItem(efro.dataclassio._base.IOMultiType[bacommon.bs.DisplayItemTypeID]):
197@ioprepped
198@dataclass
199class UnknownDisplayItem(DisplayItem):
200    """Something we don't know how to display."""
201
202    @override
203    @classmethod
204    def get_type_id(cls) -> DisplayItemTypeID:
205        return DisplayItemTypeID.UNKNOWN
206
207    @override
208    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
209        import logging
210
211        # Make noise but don't break.
212        logging.exception(
213            'UnknownDisplayItem.get_description() should never be called.'
214            ' Always access descriptions on the DisplayItemWrapper.'
215        )
216        return 'Unknown', []

Something we don't know how to display.

@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
202    @override
203    @classmethod
204    def get_type_id(cls) -> DisplayItemTypeID:
205        return DisplayItemTypeID.UNKNOWN

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
207    @override
208    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
209        import logging
210
211        # Make noise but don't break.
212        logging.exception(
213            'UnknownDisplayItem.get_description() should never be called.'
214            ' Always access descriptions on the DisplayItemWrapper.'
215        )
216        return 'Unknown', []

Return a string description and subs for the item.

These decriptions are baked into the DisplayItemWrapper and should be accessed from there when available. This allows clients to give descriptions even for newer display items they don't recognize.

@ioprepped
@dataclass
class TicketsDisplayItem(efro.dataclassio._base.IOMultiType[bacommon.bs.DisplayItemTypeID]):
219@ioprepped
220@dataclass
221class TicketsDisplayItem(DisplayItem):
222    """Some amount of tickets."""
223
224    count: Annotated[int, IOAttrs('c')]
225
226    @override
227    @classmethod
228    def get_type_id(cls) -> DisplayItemTypeID:
229        return DisplayItemTypeID.TICKETS
230
231    @override
232    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
233        return '${C} Tickets', [('${C}', str(self.count))]

Some amount of tickets.

TicketsDisplayItem(count: Annotated[int, <efro.dataclassio.IOAttrs object>])
count: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10723eed0>]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
226    @override
227    @classmethod
228    def get_type_id(cls) -> DisplayItemTypeID:
229        return DisplayItemTypeID.TICKETS

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
231    @override
232    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
233        return '${C} Tickets', [('${C}', str(self.count))]

Return a string description and subs for the item.

These decriptions are baked into the DisplayItemWrapper and should be accessed from there when available. This allows clients to give descriptions even for newer display items they don't recognize.

@ioprepped
@dataclass
class TokensDisplayItem(efro.dataclassio._base.IOMultiType[bacommon.bs.DisplayItemTypeID]):
236@ioprepped
237@dataclass
238class TokensDisplayItem(DisplayItem):
239    """Some amount of tokens."""
240
241    count: Annotated[int, IOAttrs('c')]
242
243    @override
244    @classmethod
245    def get_type_id(cls) -> DisplayItemTypeID:
246        return DisplayItemTypeID.TOKENS
247
248    @override
249    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
250        return '${C} Tokens', [('${C}', str(self.count))]

Some amount of tokens.

TokensDisplayItem(count: Annotated[int, <efro.dataclassio.IOAttrs object>])
count: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10723ff50>]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
243    @override
244    @classmethod
245    def get_type_id(cls) -> DisplayItemTypeID:
246        return DisplayItemTypeID.TOKENS

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
248    @override
249    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
250        return '${C} Tokens', [('${C}', str(self.count))]

Return a string description and subs for the item.

These decriptions are baked into the DisplayItemWrapper and should be accessed from there when available. This allows clients to give descriptions even for newer display items they don't recognize.

@ioprepped
@dataclass
class TestDisplayItem(efro.dataclassio._base.IOMultiType[bacommon.bs.DisplayItemTypeID]):
253@ioprepped
254@dataclass
255class TestDisplayItem(DisplayItem):
256    """Fills usable space for a display-item - good for calibration."""
257
258    @override
259    @classmethod
260    def get_type_id(cls) -> DisplayItemTypeID:
261        return DisplayItemTypeID.TEST
262
263    @override
264    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
265        return 'Test Display Item Here', []

Fills usable space for a display-item - good for calibration.

@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
258    @override
259    @classmethod
260    def get_type_id(cls) -> DisplayItemTypeID:
261        return DisplayItemTypeID.TEST

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
263    @override
264    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
265        return 'Test Display Item Here', []

Return a string description and subs for the item.

These decriptions are baked into the DisplayItemWrapper and should be accessed from there when available. This allows clients to give descriptions even for newer display items they don't recognize.

@ioprepped
@dataclass
class ChestDisplayItem(efro.dataclassio._base.IOMultiType[bacommon.bs.DisplayItemTypeID]):
268@ioprepped
269@dataclass
270class ChestDisplayItem(DisplayItem):
271    """Display a chest."""
272
273    appearance: Annotated[ClassicChestAppearance, IOAttrs('a')]
274
275    @override
276    @classmethod
277    def get_type_id(cls) -> DisplayItemTypeID:
278        return DisplayItemTypeID.CHEST
279
280    @override
281    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
282        return self.appearance.pretty_name, []

Display a chest.

ChestDisplayItem( appearance: Annotated[ClassicChestAppearance, <efro.dataclassio.IOAttrs object>])
appearance: Annotated[ClassicChestAppearance, <efro.dataclassio.IOAttrs object at 0x1071f15b0>]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
275    @override
276    @classmethod
277    def get_type_id(cls) -> DisplayItemTypeID:
278        return DisplayItemTypeID.CHEST

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
280    @override
281    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
282        return self.appearance.pretty_name, []

Return a string description and subs for the item.

These decriptions are baked into the DisplayItemWrapper and should be accessed from there when available. This allows clients to give descriptions even for newer display items they don't recognize.

@ioprepped
@dataclass
class DisplayItemWrapper:
285@ioprepped
286@dataclass
287class DisplayItemWrapper:
288    """Wraps a DisplayItem and common info."""
289
290    item: Annotated[DisplayItem, IOAttrs('i')]
291    description: Annotated[str, IOAttrs('d')]
292    description_subs: Annotated[list[str] | None, IOAttrs('s')]
293
294    @classmethod
295    def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
296        """Convenience method to wrap a DisplayItem."""
297        desc, subs = item.get_description()
298        return DisplayItemWrapper(item, desc, pairs_to_flat(subs))

Wraps a DisplayItem and common info.

DisplayItemWrapper( item: Annotated[DisplayItem, <efro.dataclassio.IOAttrs object>], description: Annotated[str, <efro.dataclassio.IOAttrs object>], description_subs: Annotated[list[str] | None, <efro.dataclassio.IOAttrs object>])
item: Annotated[DisplayItem, <efro.dataclassio.IOAttrs object at 0x1071f2150>]
description: Annotated[str, <efro.dataclassio.IOAttrs object at 0x1071f0710>]
description_subs: Annotated[list[str] | None, <efro.dataclassio.IOAttrs object at 0x1071f09e0>]
@classmethod
def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
294    @classmethod
295    def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
296        """Convenience method to wrap a DisplayItem."""
297        desc, subs = item.get_description()
298        return DisplayItemWrapper(item, desc, pairs_to_flat(subs))

Convenience method to wrap a DisplayItem.

@ioprepped
@dataclass
class ChestInfoMessage(efro.message._message.Message):
301@ioprepped
302@dataclass
303class ChestInfoMessage(Message):
304    """Request info about a chest."""
305
306    chest_id: Annotated[str, IOAttrs('i')]
307
308    @override
309    @classmethod
310    def get_response_types(cls) -> list[type[Response] | None]:
311        return [ChestInfoResponse]

Request info about a chest.

ChestInfoMessage(chest_id: Annotated[str, <efro.dataclassio.IOAttrs object>])
chest_id: Annotated[str, <efro.dataclassio.IOAttrs object at 0x1071f33e0>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
308    @override
309    @classmethod
310    def get_response_types(cls) -> list[type[Response] | None]:
311        return [ChestInfoResponse]

Return all Response types this Message can return when sent.

The default implementation specifies a None return type.

@ioprepped
@dataclass
class ChestInfoResponse(efro.message._message.Response):
314@ioprepped
315@dataclass
316class ChestInfoResponse(Response):
317    """Here's that chest info you asked for, boss."""
318
319    @dataclass
320    class Chest:
321        """A lovely chest."""
322
323        @dataclass
324        class PrizeSet:
325            """A possible set of prizes for this chest."""
326
327            weight: Annotated[float, IOAttrs('w')]
328            contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
329
330        appearance: Annotated[
331            ClassicChestAppearance,
332            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
333        ]
334
335        # How much it costs to unlock *now*.
336        unlock_tokens: Annotated[int, IOAttrs('tk')]
337
338        # When it unlocks on its own.
339        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
340
341        # Possible prizes we contain.
342        prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
343
344        # Are ads allowed now?
345        ad_allow: Annotated[bool, IOAttrs('aa')]
346
347    chest: Annotated[Chest | None, IOAttrs('c')]
348    user_tokens: Annotated[int | None, IOAttrs('t')]

Here's that chest info you asked for, boss.

ChestInfoResponse( chest: Annotated[ChestInfoResponse.Chest | None, <efro.dataclassio.IOAttrs object>], user_tokens: Annotated[int | None, <efro.dataclassio.IOAttrs object>])
chest: Annotated[ChestInfoResponse.Chest | None, <efro.dataclassio.IOAttrs object at 0x1071f2900>]
user_tokens: Annotated[int | None, <efro.dataclassio.IOAttrs object at 0x1071f2990>]
@dataclass
class ChestInfoResponse.Chest:
319    @dataclass
320    class Chest:
321        """A lovely chest."""
322
323        @dataclass
324        class PrizeSet:
325            """A possible set of prizes for this chest."""
326
327            weight: Annotated[float, IOAttrs('w')]
328            contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
329
330        appearance: Annotated[
331            ClassicChestAppearance,
332            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
333        ]
334
335        # How much it costs to unlock *now*.
336        unlock_tokens: Annotated[int, IOAttrs('tk')]
337
338        # When it unlocks on its own.
339        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
340
341        # Possible prizes we contain.
342        prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
343
344        # Are ads allowed now?
345        ad_allow: Annotated[bool, IOAttrs('aa')]

A lovely chest.

ChestInfoResponse.Chest( appearance: Annotated[ClassicChestAppearance, <efro.dataclassio.IOAttrs object>], unlock_tokens: Annotated[int, <efro.dataclassio.IOAttrs object>], unlock_time: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object>], prizesets: Annotated[list[ChestInfoResponse.Chest.PrizeSet], <efro.dataclassio.IOAttrs object>], ad_allow: Annotated[bool, <efro.dataclassio.IOAttrs object>])
appearance: Annotated[ClassicChestAppearance, <efro.dataclassio.IOAttrs object at 0x1071f3d70>]
unlock_tokens: Annotated[int, <efro.dataclassio.IOAttrs object at 0x1071f3fe0>]
unlock_time: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x107268080>]
prizesets: Annotated[list[ChestInfoResponse.Chest.PrizeSet], <efro.dataclassio.IOAttrs object at 0x107268110>]
ad_allow: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x107268350>]
@dataclass
class ChestInfoResponse.Chest.PrizeSet:
323        @dataclass
324        class PrizeSet:
325            """A possible set of prizes for this chest."""
326
327            weight: Annotated[float, IOAttrs('w')]
328            contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]

A possible set of prizes for this chest.

ChestInfoResponse.Chest.PrizeSet( weight: Annotated[float, <efro.dataclassio.IOAttrs object>], contents: Annotated[list[DisplayItemWrapper], <efro.dataclassio.IOAttrs object>])
weight: Annotated[float, <efro.dataclassio.IOAttrs object at 0x10726a7b0>]
contents: Annotated[list[DisplayItemWrapper], <efro.dataclassio.IOAttrs object at 0x1072692e0>]
class ClientUITypeID(enum.Enum):
351class ClientUITypeID(Enum):
352    """Type ID for each of our subclasses."""
353
354    UNKNOWN = 'u'
355    BASIC = 'b'

Type ID for each of our subclasses.

UNKNOWN = <ClientUITypeID.UNKNOWN: 'u'>
BASIC = <ClientUITypeID.BASIC: 'b'>
class ClientUI(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientUITypeID]):
358class ClientUI(IOMultiType[ClientUITypeID]):
359    """Defines some user interface on the client."""
360
361    @override
362    @classmethod
363    def get_type_id(cls) -> ClientUITypeID:
364        # Require child classes to supply this themselves. If we did a
365        # full type registry/lookup here it would require us to import
366        # everything and would prevent lazy loading.
367        raise NotImplementedError()
368
369    @override
370    @classmethod
371    def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
372        """Return the subclass for each of our type-ids."""
373        # pylint: disable=cyclic-import
374        out: type[ClientUI]
375
376        t = ClientUITypeID
377        if type_id is t.UNKNOWN:
378            out = UnknownClientUI
379        elif type_id is t.BASIC:
380            out = BasicClientUI
381        else:
382            # Important to make sure we provide all types.
383            assert_never(type_id)
384        return out
385
386    @override
387    @classmethod
388    def get_unknown_type_fallback(cls) -> ClientUI:
389        # If we encounter some future message type we don't know
390        # anything about, drop in a placeholder.
391        return UnknownClientUI()

Defines some user interface on the client.

@override
@classmethod
def get_type_id(cls) -> ClientUITypeID:
361    @override
362    @classmethod
363    def get_type_id(cls) -> ClientUITypeID:
364        # Require child classes to supply this themselves. If we did a
365        # full type registry/lookup here it would require us to import
366        # everything and would prevent lazy loading.
367        raise NotImplementedError()

Return the type-id for this subclass.

@override
@classmethod
def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
369    @override
370    @classmethod
371    def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
372        """Return the subclass for each of our type-ids."""
373        # pylint: disable=cyclic-import
374        out: type[ClientUI]
375
376        t = ClientUITypeID
377        if type_id is t.UNKNOWN:
378            out = UnknownClientUI
379        elif type_id is t.BASIC:
380            out = BasicClientUI
381        else:
382            # Important to make sure we provide all types.
383            assert_never(type_id)
384        return out

Return the subclass for each of our type-ids.

@override
@classmethod
def get_unknown_type_fallback(cls) -> ClientUI:
386    @override
387    @classmethod
388    def get_unknown_type_fallback(cls) -> ClientUI:
389        # If we encounter some future message type we don't know
390        # anything about, drop in a placeholder.
391        return UnknownClientUI()

Return a fallback object in cases of unrecognized types.

This can allow newer data to remain readable in older environments. Use caution with this option, however, as it effectively modifies data.

@ioprepped
@dataclass
class UnknownClientUI(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientUITypeID]):
394@ioprepped
395@dataclass
396class UnknownClientUI(ClientUI):
397    """Fallback type for unrecognized entries."""
398
399    @override
400    @classmethod
401    def get_type_id(cls) -> ClientUITypeID:
402        return ClientUITypeID.UNKNOWN

Fallback type for unrecognized entries.

@override
@classmethod
def get_type_id(cls) -> ClientUITypeID:
399    @override
400    @classmethod
401    def get_type_id(cls) -> ClientUITypeID:
402        return ClientUITypeID.UNKNOWN

Return the type-id for this subclass.

class BasicClientUIComponentTypeID(enum.Enum):
405class BasicClientUIComponentTypeID(Enum):
406    """Type ID for each of our subclasses."""
407
408    UNKNOWN = 'u'
409    TEXT = 't'
410    LINK = 'l'
411    BS_CLASSIC_TOURNEY_RESULT = 'ct'
412    DISPLAY_ITEMS = 'di'
413    EXPIRE_TIME = 'd'

Type ID for each of our subclasses.

BS_CLASSIC_TOURNEY_RESULT = <BasicClientUIComponentTypeID.BS_CLASSIC_TOURNEY_RESULT: 'ct'>
class BasicClientUIComponent(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
416class BasicClientUIComponent(IOMultiType[BasicClientUIComponentTypeID]):
417    """Top level class for our multitype."""
418
419    @override
420    @classmethod
421    def get_type_id(cls) -> BasicClientUIComponentTypeID:
422        # Require child classes to supply this themselves. If we did a
423        # full type registry/lookup here it would require us to import
424        # everything and would prevent lazy loading.
425        raise NotImplementedError()
426
427    @override
428    @classmethod
429    def get_type(
430        cls, type_id: BasicClientUIComponentTypeID
431    ) -> type[BasicClientUIComponent]:
432        """Return the subclass for each of our type-ids."""
433        # pylint: disable=cyclic-import
434
435        t = BasicClientUIComponentTypeID
436        if type_id is t.UNKNOWN:
437            return BasicClientUIComponentUnknown
438        if type_id is t.TEXT:
439            return BasicClientUIComponentText
440        if type_id is t.LINK:
441            return BasicClientUIComponentLink
442        if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
443            return BasicClientUIBsClassicTourneyResult
444        if type_id is t.DISPLAY_ITEMS:
445            return BasicClientUIDisplayItems
446        if type_id is t.EXPIRE_TIME:
447            return BasicClientUIExpireTime
448
449        # Important to make sure we provide all types.
450        assert_never(type_id)
451
452    @override
453    @classmethod
454    def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
455        # If we encounter some future message type we don't know
456        # anything about, drop in a placeholder.
457        return BasicClientUIComponentUnknown()

Top level class for our multitype.

@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
419    @override
420    @classmethod
421    def get_type_id(cls) -> BasicClientUIComponentTypeID:
422        # Require child classes to supply this themselves. If we did a
423        # full type registry/lookup here it would require us to import
424        # everything and would prevent lazy loading.
425        raise NotImplementedError()

Return the type-id for this subclass.

@override
@classmethod
def get_type( cls, type_id: BasicClientUIComponentTypeID) -> type[BasicClientUIComponent]:
427    @override
428    @classmethod
429    def get_type(
430        cls, type_id: BasicClientUIComponentTypeID
431    ) -> type[BasicClientUIComponent]:
432        """Return the subclass for each of our type-ids."""
433        # pylint: disable=cyclic-import
434
435        t = BasicClientUIComponentTypeID
436        if type_id is t.UNKNOWN:
437            return BasicClientUIComponentUnknown
438        if type_id is t.TEXT:
439            return BasicClientUIComponentText
440        if type_id is t.LINK:
441            return BasicClientUIComponentLink
442        if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
443            return BasicClientUIBsClassicTourneyResult
444        if type_id is t.DISPLAY_ITEMS:
445            return BasicClientUIDisplayItems
446        if type_id is t.EXPIRE_TIME:
447            return BasicClientUIExpireTime
448
449        # Important to make sure we provide all types.
450        assert_never(type_id)

Return the subclass for each of our type-ids.

@override
@classmethod
def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
452    @override
453    @classmethod
454    def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
455        # If we encounter some future message type we don't know
456        # anything about, drop in a placeholder.
457        return BasicClientUIComponentUnknown()

Return a fallback object in cases of unrecognized types.

This can allow newer data to remain readable in older environments. Use caution with this option, however, as it effectively modifies data.

@ioprepped
@dataclass
class BasicClientUIComponentUnknown(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
460@ioprepped
461@dataclass
462class BasicClientUIComponentUnknown(BasicClientUIComponent):
463    """An unknown basic client component type.
464
465    In practice these should never show up since the master-server
466    generates these on the fly for the client and so should not send
467    clients one they can't digest.
468    """
469
470    @override
471    @classmethod
472    def get_type_id(cls) -> BasicClientUIComponentTypeID:
473        return BasicClientUIComponentTypeID.UNKNOWN

An unknown basic client component type.

In practice these should never show up since the master-server generates these on the fly for the client and so should not send clients one they can't digest.

@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
470    @override
471    @classmethod
472    def get_type_id(cls) -> BasicClientUIComponentTypeID:
473        return BasicClientUIComponentTypeID.UNKNOWN

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIComponentText(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
476@ioprepped
477@dataclass
478class BasicClientUIComponentText(BasicClientUIComponent):
479    """Show some text in the inbox message."""
480
481    text: Annotated[str, IOAttrs('t')]
482    subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
483        default_factory=list
484    )
485    scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0
486    color: Annotated[
487        tuple[float, float, float, float], IOAttrs('c', store_default=False)
488    ] = (1.0, 1.0, 1.0, 1.0)
489    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
490    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
491
492    @override
493    @classmethod
494    def get_type_id(cls) -> BasicClientUIComponentTypeID:
495        return BasicClientUIComponentTypeID.TEXT

Show some text in the inbox message.

BasicClientUIComponentText( text: Annotated[str, <efro.dataclassio.IOAttrs object>], subs: Annotated[list[str], <efro.dataclassio.IOAttrs object>] = <factory>, scale: Annotated[float, <efro.dataclassio.IOAttrs object>] = 1.0, color: Annotated[tuple[float, float, float, float], <efro.dataclassio.IOAttrs object>] = (1.0, 1.0, 1.0, 1.0), spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object>] = 0.0, spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object>] = 0.0)
text: Annotated[str, <efro.dataclassio.IOAttrs object at 0x10728b980>]
subs: Annotated[list[str], <efro.dataclassio.IOAttrs object at 0x10728a240>]
scale: Annotated[float, <efro.dataclassio.IOAttrs object at 0x10728a3c0>] = 1.0
color: Annotated[tuple[float, float, float, float], <efro.dataclassio.IOAttrs object at 0x10728a540>] = (1.0, 1.0, 1.0, 1.0)
spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object at 0x10728a5d0>] = 0.0
spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object at 0x10728a6c0>] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
492    @override
493    @classmethod
494    def get_type_id(cls) -> BasicClientUIComponentTypeID:
495        return BasicClientUIComponentTypeID.TEXT

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIBsClassicTourneyResult(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
517@ioprepped
518@dataclass
519class BasicClientUIBsClassicTourneyResult(BasicClientUIComponent):
520    """Show info about a classic tourney."""
521
522    tournament_id: Annotated[str, IOAttrs('t')]
523    game: Annotated[str, IOAttrs('g')]
524    players: Annotated[int, IOAttrs('p')]
525    rank: Annotated[int, IOAttrs('r')]
526    trophy: Annotated[str | None, IOAttrs('tr')]
527    prizes: Annotated[list[DisplayItemWrapper], IOAttrs('pr')]
528
529    @override
530    @classmethod
531    def get_type_id(cls) -> BasicClientUIComponentTypeID:
532        return BasicClientUIComponentTypeID.BS_CLASSIC_TOURNEY_RESULT

Show info about a classic tourney.

BasicClientUIBsClassicTourneyResult( tournament_id: Annotated[str, <efro.dataclassio.IOAttrs object>], game: Annotated[str, <efro.dataclassio.IOAttrs object>], players: Annotated[int, <efro.dataclassio.IOAttrs object>], rank: Annotated[int, <efro.dataclassio.IOAttrs object>], trophy: Annotated[str | None, <efro.dataclassio.IOAttrs object>], prizes: Annotated[list[DisplayItemWrapper], <efro.dataclassio.IOAttrs object>])
tournament_id: Annotated[str, <efro.dataclassio.IOAttrs object at 0x107266a80>]
game: Annotated[str, <efro.dataclassio.IOAttrs object at 0x1072656d0>]
players: Annotated[int, <efro.dataclassio.IOAttrs object at 0x1072657c0>]
rank: Annotated[int, <efro.dataclassio.IOAttrs object at 0x1072658b0>]
trophy: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x107265970>]
prizes: Annotated[list[DisplayItemWrapper], <efro.dataclassio.IOAttrs object at 0x107265b50>]
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
529    @override
530    @classmethod
531    def get_type_id(cls) -> BasicClientUIComponentTypeID:
532        return BasicClientUIComponentTypeID.BS_CLASSIC_TOURNEY_RESULT

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIDisplayItems(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
535@ioprepped
536@dataclass
537class BasicClientUIDisplayItems(BasicClientUIComponent):
538    """Show some display-items."""
539
540    items: Annotated[list[DisplayItemWrapper], IOAttrs('d')]
541    width: Annotated[float, IOAttrs('w')] = 100.0
542    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
543    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
544
545    @override
546    @classmethod
547    def get_type_id(cls) -> BasicClientUIComponentTypeID:
548        return BasicClientUIComponentTypeID.DISPLAY_ITEMS

Show some display-items.

BasicClientUIDisplayItems( items: Annotated[list[DisplayItemWrapper], <efro.dataclassio.IOAttrs object>], width: Annotated[float, <efro.dataclassio.IOAttrs object>] = 100.0, spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object>] = 0.0, spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object>] = 0.0)
items: Annotated[list[DisplayItemWrapper], <efro.dataclassio.IOAttrs object at 0x107266840>]
width: Annotated[float, <efro.dataclassio.IOAttrs object at 0x107267410>] = 100.0
spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object at 0x1072675f0>] = 0.0
spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object at 0x1072676e0>] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
545    @override
546    @classmethod
547    def get_type_id(cls) -> BasicClientUIComponentTypeID:
548        return BasicClientUIComponentTypeID.DISPLAY_ITEMS

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIExpireTime(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
551@ioprepped
552@dataclass
553class BasicClientUIExpireTime(BasicClientUIComponent):
554    """Show expire-time."""
555
556    time: Annotated[datetime.datetime, IOAttrs('d')]
557    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
558    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
559
560    @override
561    @classmethod
562    def get_type_id(cls) -> BasicClientUIComponentTypeID:
563        return BasicClientUIComponentTypeID.EXPIRE_TIME

Show expire-time.

BasicClientUIExpireTime( time: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object>], spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object>] = 0.0, spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object>] = 0.0)
time: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x1073c9be0>]
spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object at 0x1073cbbc0>] = 0.0
spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object at 0x1073c8860>] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
560    @override
561    @classmethod
562    def get_type_id(cls) -> BasicClientUIComponentTypeID:
563        return BasicClientUIComponentTypeID.EXPIRE_TIME

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUI(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientUITypeID]):
566@ioprepped
567@dataclass
568class BasicClientUI(ClientUI):
569    """A basic UI for the client."""
570
571    class ButtonLabel(Enum):
572        """Distinct button labels we support."""
573
574        UNKNOWN = 'u'
575        OK = 'o'
576        APPLY = 'a'
577        CANCEL = 'c'
578        ACCEPT = 'ac'
579        DECLINE = 'dn'
580        IGNORE = 'ig'
581        CLAIM = 'cl'
582        DISCARD = 'd'
583
584    class InteractionStyle(Enum):
585        """Overall interaction styles we support."""
586
587        UNKNOWN = 'u'
588        BUTTON_POSITIVE = 'p'
589        BUTTON_POSITIVE_NEGATIVE = 'pn'
590
591    components: Annotated[list[BasicClientUIComponent], IOAttrs('s')]
592
593    interaction_style: Annotated[
594        InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN)
595    ] = InteractionStyle.BUTTON_POSITIVE
596
597    button_label_positive: Annotated[
598        ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN)
599    ] = ButtonLabel.OK
600
601    button_label_negative: Annotated[
602        ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN)
603    ] = ButtonLabel.CANCEL
604
605    @override
606    @classmethod
607    def get_type_id(cls) -> ClientUITypeID:
608        return ClientUITypeID.BASIC
609
610    def contains_unknown_elements(self) -> bool:
611        """Whether something within us is an unknown type or enum."""
612        return (
613            self.interaction_style is self.InteractionStyle.UNKNOWN
614            or self.button_label_positive is self.ButtonLabel.UNKNOWN
615            or self.button_label_negative is self.ButtonLabel.UNKNOWN
616            or any(
617                c.get_type_id() is BasicClientUIComponentTypeID.UNKNOWN
618                for c in self.components
619            )
620        )

A basic UI for the client.

BasicClientUI( components: Annotated[list[BasicClientUIComponent], <efro.dataclassio.IOAttrs object>], interaction_style: Annotated[BasicClientUI.InteractionStyle, <efro.dataclassio.IOAttrs object>] = <InteractionStyle.BUTTON_POSITIVE: 'p'>, button_label_positive: Annotated[BasicClientUI.ButtonLabel, <efro.dataclassio.IOAttrs object>] = <ButtonLabel.OK: 'o'>, button_label_negative: Annotated[BasicClientUI.ButtonLabel, <efro.dataclassio.IOAttrs object>] = <ButtonLabel.CANCEL: 'c'>)
components: Annotated[list[BasicClientUIComponent], <efro.dataclassio.IOAttrs object at 0x106429af0>]
interaction_style: Annotated[BasicClientUI.InteractionStyle, <efro.dataclassio.IOAttrs object at 0x10642a150>] = <InteractionStyle.BUTTON_POSITIVE: 'p'>
button_label_positive: Annotated[BasicClientUI.ButtonLabel, <efro.dataclassio.IOAttrs object at 0x10642a570>] = <ButtonLabel.OK: 'o'>
button_label_negative: Annotated[BasicClientUI.ButtonLabel, <efro.dataclassio.IOAttrs object at 0x10642a180>] = <ButtonLabel.CANCEL: 'c'>
@override
@classmethod
def get_type_id(cls) -> ClientUITypeID:
605    @override
606    @classmethod
607    def get_type_id(cls) -> ClientUITypeID:
608        return ClientUITypeID.BASIC

Return the type-id for this subclass.

def contains_unknown_elements(self) -> bool:
610    def contains_unknown_elements(self) -> bool:
611        """Whether something within us is an unknown type or enum."""
612        return (
613            self.interaction_style is self.InteractionStyle.UNKNOWN
614            or self.button_label_positive is self.ButtonLabel.UNKNOWN
615            or self.button_label_negative is self.ButtonLabel.UNKNOWN
616            or any(
617                c.get_type_id() is BasicClientUIComponentTypeID.UNKNOWN
618                for c in self.components
619            )
620        )

Whether something within us is an unknown type or enum.

class BasicClientUI.ButtonLabel(enum.Enum):
571    class ButtonLabel(Enum):
572        """Distinct button labels we support."""
573
574        UNKNOWN = 'u'
575        OK = 'o'
576        APPLY = 'a'
577        CANCEL = 'c'
578        ACCEPT = 'ac'
579        DECLINE = 'dn'
580        IGNORE = 'ig'
581        CLAIM = 'cl'
582        DISCARD = 'd'

Distinct button labels we support.

UNKNOWN = <ButtonLabel.UNKNOWN: 'u'>
OK = <ButtonLabel.OK: 'o'>
APPLY = <ButtonLabel.APPLY: 'a'>
CANCEL = <ButtonLabel.CANCEL: 'c'>
ACCEPT = <ButtonLabel.ACCEPT: 'ac'>
DECLINE = <ButtonLabel.DECLINE: 'dn'>
IGNORE = <ButtonLabel.IGNORE: 'ig'>
CLAIM = <ButtonLabel.CLAIM: 'cl'>
DISCARD = <ButtonLabel.DISCARD: 'd'>
class BasicClientUI.InteractionStyle(enum.Enum):
584    class InteractionStyle(Enum):
585        """Overall interaction styles we support."""
586
587        UNKNOWN = 'u'
588        BUTTON_POSITIVE = 'p'
589        BUTTON_POSITIVE_NEGATIVE = 'pn'

Overall interaction styles we support.

UNKNOWN = <InteractionStyle.UNKNOWN: 'u'>
BUTTON_POSITIVE = <InteractionStyle.BUTTON_POSITIVE: 'p'>
BUTTON_POSITIVE_NEGATIVE = <InteractionStyle.BUTTON_POSITIVE_NEGATIVE: 'pn'>
@ioprepped
@dataclass
class ClientUIWrapper:
623@ioprepped
624@dataclass
625class ClientUIWrapper:
626    """Wrapper for a ClientUI and its common data."""
627
628    id: Annotated[str, IOAttrs('i')]
629    createtime: Annotated[datetime.datetime, IOAttrs('c')]
630    ui: Annotated[ClientUI, IOAttrs('e')]

Wrapper for a ClientUI and its common data.

ClientUIWrapper( id: Annotated[str, <efro.dataclassio.IOAttrs object>], createtime: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object>], ui: Annotated[ClientUI, <efro.dataclassio.IOAttrs object>])
id: Annotated[str, <efro.dataclassio.IOAttrs object at 0x1073cbb00>]
createtime: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x1073cbd40>]
ui: Annotated[ClientUI, <efro.dataclassio.IOAttrs object at 0x1073cbe90>]
@ioprepped
@dataclass
class InboxRequestMessage(efro.message._message.Message):
633@ioprepped
634@dataclass
635class InboxRequestMessage(Message):
636    """Message requesting our inbox."""
637
638    @override
639    @classmethod
640    def get_response_types(cls) -> list[type[Response] | None]:
641        return [InboxRequestResponse]

Message requesting our inbox.

@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
638    @override
639    @classmethod
640    def get_response_types(cls) -> list[type[Response] | None]:
641        return [InboxRequestResponse]

Return all Response types this Message can return when sent.

The default implementation specifies a None return type.

@ioprepped
@dataclass
class InboxRequestResponse(efro.message._message.Response):
644@ioprepped
645@dataclass
646class InboxRequestResponse(Response):
647    """Here's that inbox contents you asked for, boss."""
648
649    wrappers: Annotated[list[ClientUIWrapper], IOAttrs('w')]
650
651    # Printable error if something goes wrong.
652    error: Annotated[str | None, IOAttrs('e')] = None

Here's that inbox contents you asked for, boss.

InboxRequestResponse( wrappers: Annotated[list[ClientUIWrapper], <efro.dataclassio.IOAttrs object>], error: Annotated[str | None, <efro.dataclassio.IOAttrs object>] = None)
wrappers: Annotated[list[ClientUIWrapper], <efro.dataclassio.IOAttrs object at 0x1073fe750>]
error: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x1073fd340>] = None
class ClientUIAction(enum.Enum):
655class ClientUIAction(Enum):
656    """Types of actions we can run."""
657
658    BUTTON_PRESS_POSITIVE = 'p'
659    BUTTON_PRESS_NEGATIVE = 'n'

Types of actions we can run.

BUTTON_PRESS_POSITIVE = <ClientUIAction.BUTTON_PRESS_POSITIVE: 'p'>
BUTTON_PRESS_NEGATIVE = <ClientUIAction.BUTTON_PRESS_NEGATIVE: 'n'>
class ClientEffectTypeID(enum.Enum):
662class ClientEffectTypeID(Enum):
663    """Type ID for each of our subclasses."""
664
665    UNKNOWN = 'u'
666    SCREEN_MESSAGE = 'm'
667    SOUND = 's'
668    DELAY = 'd'
669    CHEST_WAIT_TIME_ANIMATION = 't'
670    TICKETS_ANIMATION = 'ta'
671    TOKENS_ANIMATION = 'toa'

Type ID for each of our subclasses.

UNKNOWN = <ClientEffectTypeID.UNKNOWN: 'u'>
SCREEN_MESSAGE = <ClientEffectTypeID.SCREEN_MESSAGE: 'm'>
SOUND = <ClientEffectTypeID.SOUND: 's'>
DELAY = <ClientEffectTypeID.DELAY: 'd'>
CHEST_WAIT_TIME_ANIMATION = <ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION: 't'>
TICKETS_ANIMATION = <ClientEffectTypeID.TICKETS_ANIMATION: 'ta'>
TOKENS_ANIMATION = <ClientEffectTypeID.TOKENS_ANIMATION: 'toa'>
class ClientEffect(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
674class ClientEffect(IOMultiType[ClientEffectTypeID]):
675    """Something that can happen on the client.
676
677    This can include screen messages, sounds, visual effects, etc.
678    """
679
680    @override
681    @classmethod
682    def get_type_id(cls) -> ClientEffectTypeID:
683        # Require child classes to supply this themselves. If we did a
684        # full type registry/lookup here it would require us to import
685        # everything and would prevent lazy loading.
686        raise NotImplementedError()
687
688    @override
689    @classmethod
690    def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
691        """Return the subclass for each of our type-ids."""
692        # pylint: disable=cyclic-import
693        # pylint: disable=too-many-return-statements
694
695        t = ClientEffectTypeID
696        if type_id is t.UNKNOWN:
697            return ClientEffectUnknown
698        if type_id is t.SCREEN_MESSAGE:
699            return ClientEffectScreenMessage
700        if type_id is t.SOUND:
701            return ClientEffectSound
702        if type_id is t.DELAY:
703            return ClientEffectDelay
704        if type_id is t.CHEST_WAIT_TIME_ANIMATION:
705            return ClientEffectChestWaitTimeAnimation
706        if type_id is t.TICKETS_ANIMATION:
707            return ClientEffectTicketsAnimation
708        if type_id is t.TOKENS_ANIMATION:
709            return ClientEffectTokensAnimation
710
711        # Important to make sure we provide all types.
712        assert_never(type_id)
713
714    @override
715    @classmethod
716    def get_unknown_type_fallback(cls) -> ClientEffect:
717        # If we encounter some future message type we don't know
718        # anything about, drop in a placeholder.
719        return ClientEffectUnknown()

Something that can happen on the client.

This can include screen messages, sounds, visual effects, etc.

@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
680    @override
681    @classmethod
682    def get_type_id(cls) -> ClientEffectTypeID:
683        # Require child classes to supply this themselves. If we did a
684        # full type registry/lookup here it would require us to import
685        # everything and would prevent lazy loading.
686        raise NotImplementedError()

Return the type-id for this subclass.

@override
@classmethod
def get_type( cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
688    @override
689    @classmethod
690    def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
691        """Return the subclass for each of our type-ids."""
692        # pylint: disable=cyclic-import
693        # pylint: disable=too-many-return-statements
694
695        t = ClientEffectTypeID
696        if type_id is t.UNKNOWN:
697            return ClientEffectUnknown
698        if type_id is t.SCREEN_MESSAGE:
699            return ClientEffectScreenMessage
700        if type_id is t.SOUND:
701            return ClientEffectSound
702        if type_id is t.DELAY:
703            return ClientEffectDelay
704        if type_id is t.CHEST_WAIT_TIME_ANIMATION:
705            return ClientEffectChestWaitTimeAnimation
706        if type_id is t.TICKETS_ANIMATION:
707            return ClientEffectTicketsAnimation
708        if type_id is t.TOKENS_ANIMATION:
709            return ClientEffectTokensAnimation
710
711        # Important to make sure we provide all types.
712        assert_never(type_id)

Return the subclass for each of our type-ids.

@override
@classmethod
def get_unknown_type_fallback(cls) -> ClientEffect:
714    @override
715    @classmethod
716    def get_unknown_type_fallback(cls) -> ClientEffect:
717        # If we encounter some future message type we don't know
718        # anything about, drop in a placeholder.
719        return ClientEffectUnknown()

Return a fallback object in cases of unrecognized types.

This can allow newer data to remain readable in older environments. Use caution with this option, however, as it effectively modifies data.

@ioprepped
@dataclass
class ClientEffectUnknown(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
722@ioprepped
723@dataclass
724class ClientEffectUnknown(ClientEffect):
725    """Fallback substitute for types we don't recognize."""
726
727    @override
728    @classmethod
729    def get_type_id(cls) -> ClientEffectTypeID:
730        return ClientEffectTypeID.UNKNOWN

Fallback substitute for types we don't recognize.

@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
727    @override
728    @classmethod
729    def get_type_id(cls) -> ClientEffectTypeID:
730        return ClientEffectTypeID.UNKNOWN

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientEffectScreenMessage(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
733@ioprepped
734@dataclass
735class ClientEffectScreenMessage(ClientEffect):
736    """Display a screen-message."""
737
738    message: Annotated[str, IOAttrs('m')]
739    subs: Annotated[list[str], IOAttrs('s')]
740    color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0)
741
742    @override
743    @classmethod
744    def get_type_id(cls) -> ClientEffectTypeID:
745        return ClientEffectTypeID.SCREEN_MESSAGE

Display a screen-message.

ClientEffectScreenMessage( message: Annotated[str, <efro.dataclassio.IOAttrs object>], subs: Annotated[list[str], <efro.dataclassio.IOAttrs object>], color: Annotated[tuple[float, float, float], <efro.dataclassio.IOAttrs object>] = (1.0, 1.0, 1.0))
message: Annotated[str, <efro.dataclassio.IOAttrs object at 0x10742a5d0>]
subs: Annotated[list[str], <efro.dataclassio.IOAttrs object at 0x107429130>]
color: Annotated[tuple[float, float, float], <efro.dataclassio.IOAttrs object at 0x107429250>] = (1.0, 1.0, 1.0)
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
742    @override
743    @classmethod
744    def get_type_id(cls) -> ClientEffectTypeID:
745        return ClientEffectTypeID.SCREEN_MESSAGE

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientEffectSound(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
748@ioprepped
749@dataclass
750class ClientEffectSound(ClientEffect):
751    """Play a sound."""
752
753    class Sound(Enum):
754        """Sounds that can be made alongside the message."""
755
756        UNKNOWN = 'u'
757        CASH_REGISTER = 'c'
758        ERROR = 'e'
759        POWER_DOWN = 'p'
760        GUN_COCKING = 'g'
761
762    sound: Annotated[Sound, IOAttrs('s', enum_fallback=Sound.UNKNOWN)]
763    volume: Annotated[float, IOAttrs('v')] = 1.0
764
765    @override
766    @classmethod
767    def get_type_id(cls) -> ClientEffectTypeID:
768        return ClientEffectTypeID.SOUND

Play a sound.

ClientEffectSound( sound: Annotated[ClientEffectSound.Sound, <efro.dataclassio.IOAttrs object>], volume: Annotated[float, <efro.dataclassio.IOAttrs object>] = 1.0)
sound: Annotated[ClientEffectSound.Sound, <efro.dataclassio.IOAttrs object at 0x10742b860>]
volume: Annotated[float, <efro.dataclassio.IOAttrs object at 0x10742a300>] = 1.0
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
765    @override
766    @classmethod
767    def get_type_id(cls) -> ClientEffectTypeID:
768        return ClientEffectTypeID.SOUND

Return the type-id for this subclass.

class ClientEffectSound.Sound(enum.Enum):
753    class Sound(Enum):
754        """Sounds that can be made alongside the message."""
755
756        UNKNOWN = 'u'
757        CASH_REGISTER = 'c'
758        ERROR = 'e'
759        POWER_DOWN = 'p'
760        GUN_COCKING = 'g'

Sounds that can be made alongside the message.

UNKNOWN = <Sound.UNKNOWN: 'u'>
CASH_REGISTER = <Sound.CASH_REGISTER: 'c'>
ERROR = <Sound.ERROR: 'e'>
POWER_DOWN = <Sound.POWER_DOWN: 'p'>
GUN_COCKING = <Sound.GUN_COCKING: 'g'>
@ioprepped
@dataclass
class ClientEffectChestWaitTimeAnimation(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
771@ioprepped
772@dataclass
773class ClientEffectChestWaitTimeAnimation(ClientEffect):
774    """Animate chest wait time changing."""
775
776    chestid: Annotated[str, IOAttrs('c')]
777    duration: Annotated[float, IOAttrs('u')]
778    startvalue: Annotated[datetime.datetime, IOAttrs('o')]
779    endvalue: Annotated[datetime.datetime, IOAttrs('n')]
780
781    @override
782    @classmethod
783    def get_type_id(cls) -> ClientEffectTypeID:
784        return ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION

Animate chest wait time changing.

ClientEffectChestWaitTimeAnimation( chestid: Annotated[str, <efro.dataclassio.IOAttrs object>], duration: Annotated[float, <efro.dataclassio.IOAttrs object>], startvalue: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object>], endvalue: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object>])
chestid: Annotated[str, <efro.dataclassio.IOAttrs object at 0x10739d850>]
duration: Annotated[float, <efro.dataclassio.IOAttrs object at 0x10739f950>]
startvalue: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x10739fb90>]
endvalue: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x10739c3e0>]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
781    @override
782    @classmethod
783    def get_type_id(cls) -> ClientEffectTypeID:
784        return ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientEffectTicketsAnimation(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
787@ioprepped
788@dataclass
789class ClientEffectTicketsAnimation(ClientEffect):
790    """Animate tickets count."""
791
792    duration: Annotated[float, IOAttrs('u')]
793    startvalue: Annotated[int, IOAttrs('s')]
794    endvalue: Annotated[int, IOAttrs('e')]
795
796    @override
797    @classmethod
798    def get_type_id(cls) -> ClientEffectTypeID:
799        return ClientEffectTypeID.TICKETS_ANIMATION

Animate tickets count.

ClientEffectTicketsAnimation( duration: Annotated[float, <efro.dataclassio.IOAttrs object>], startvalue: Annotated[int, <efro.dataclassio.IOAttrs object>], endvalue: Annotated[int, <efro.dataclassio.IOAttrs object>])
duration: Annotated[float, <efro.dataclassio.IOAttrs object at 0x10739ea80>]
startvalue: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10739d640>]
endvalue: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10739d790>]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
796    @override
797    @classmethod
798    def get_type_id(cls) -> ClientEffectTypeID:
799        return ClientEffectTypeID.TICKETS_ANIMATION

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientEffectTokensAnimation(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
802@ioprepped
803@dataclass
804class ClientEffectTokensAnimation(ClientEffect):
805    """Animate tokens count."""
806
807    duration: Annotated[float, IOAttrs('u')]
808    startvalue: Annotated[int, IOAttrs('s')]
809    endvalue: Annotated[int, IOAttrs('e')]
810
811    @override
812    @classmethod
813    def get_type_id(cls) -> ClientEffectTypeID:
814        return ClientEffectTypeID.TOKENS_ANIMATION

Animate tokens count.

ClientEffectTokensAnimation( duration: Annotated[float, <efro.dataclassio.IOAttrs object>], startvalue: Annotated[int, <efro.dataclassio.IOAttrs object>], endvalue: Annotated[int, <efro.dataclassio.IOAttrs object>])
duration: Annotated[float, <efro.dataclassio.IOAttrs object at 0x10739fe60>]
startvalue: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10739e7e0>]
endvalue: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10739e990>]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
811    @override
812    @classmethod
813    def get_type_id(cls) -> ClientEffectTypeID:
814        return ClientEffectTypeID.TOKENS_ANIMATION

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientEffectDelay(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
817@ioprepped
818@dataclass
819class ClientEffectDelay(ClientEffect):
820    """Delay effect processing."""
821
822    seconds: Annotated[float, IOAttrs('s')]
823
824    @override
825    @classmethod
826    def get_type_id(cls) -> ClientEffectTypeID:
827        return ClientEffectTypeID.DELAY

Delay effect processing.

ClientEffectDelay(seconds: Annotated[float, <efro.dataclassio.IOAttrs object>])
seconds: Annotated[float, <efro.dataclassio.IOAttrs object at 0x10739f5f0>]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
824    @override
825    @classmethod
826    def get_type_id(cls) -> ClientEffectTypeID:
827        return ClientEffectTypeID.DELAY

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientUIActionMessage(efro.message._message.Message):
830@ioprepped
831@dataclass
832class ClientUIActionMessage(Message):
833    """Do something to a client ui."""
834
835    id: Annotated[str, IOAttrs('i')]
836    action: Annotated[ClientUIAction, IOAttrs('a')]
837
838    @override
839    @classmethod
840    def get_response_types(cls) -> list[type[Response] | None]:
841        return [ClientUIActionResponse]

Do something to a client ui.

ClientUIActionMessage( id: Annotated[str, <efro.dataclassio.IOAttrs object>], action: Annotated[ClientUIAction, <efro.dataclassio.IOAttrs object>])
id: Annotated[str, <efro.dataclassio.IOAttrs object at 0x107430aa0>]
action: Annotated[ClientUIAction, <efro.dataclassio.IOAttrs object at 0x1074338c0>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
838    @override
839    @classmethod
840    def get_response_types(cls) -> list[type[Response] | None]:
841        return [ClientUIActionResponse]

Return all Response types this Message can return when sent.

The default implementation specifies a None return type.

@ioprepped
@dataclass
class ClientUIActionResponse(efro.message._message.Response):
844@ioprepped
845@dataclass
846class ClientUIActionResponse(Response):
847    """Did something to that inbox entry, boss."""
848
849    class ErrorType(Enum):
850        """Types of errors that may have occurred."""
851
852        # Probably a future error type we don't recognize.
853        UNKNOWN = 'u'
854
855        # Something went wrong on the server, but specifics are not
856        # relevant.
857        INTERNAL = 'i'
858
859        # The entry expired on the server. In various cases such as 'ok'
860        # buttons this can generally be ignored.
861        EXPIRED = 'e'
862
863    error_type: Annotated[
864        ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN)
865    ]
866
867    # User facing error message in the case of errors.
868    error_message: Annotated[str | None, IOAttrs('em')]
869
870    effects: Annotated[list[ClientEffect], IOAttrs('fx')]

Did something to that inbox entry, boss.

ClientUIActionResponse( error_type: Annotated[ClientUIActionResponse.ErrorType | None, <efro.dataclassio.IOAttrs object>], error_message: Annotated[str | None, <efro.dataclassio.IOAttrs object>], effects: Annotated[list[ClientEffect], <efro.dataclassio.IOAttrs object>])
error_type: Annotated[ClientUIActionResponse.ErrorType | None, <efro.dataclassio.IOAttrs object at 0x1074315b0>]
error_message: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x1074311c0>]
effects: Annotated[list[ClientEffect], <efro.dataclassio.IOAttrs object at 0x107431670>]
class ClientUIActionResponse.ErrorType(enum.Enum):
849    class ErrorType(Enum):
850        """Types of errors that may have occurred."""
851
852        # Probably a future error type we don't recognize.
853        UNKNOWN = 'u'
854
855        # Something went wrong on the server, but specifics are not
856        # relevant.
857        INTERNAL = 'i'
858
859        # The entry expired on the server. In various cases such as 'ok'
860        # buttons this can generally be ignored.
861        EXPIRED = 'e'

Types of errors that may have occurred.

UNKNOWN = <ErrorType.UNKNOWN: 'u'>
INTERNAL = <ErrorType.INTERNAL: 'i'>
EXPIRED = <ErrorType.EXPIRED: 'e'>
@ioprepped
@dataclass
class ScoreSubmitMessage(efro.message._message.Message):
873@ioprepped
874@dataclass
875class ScoreSubmitMessage(Message):
876    """Let the server know we got some score in something."""
877
878    score_token: Annotated[str, IOAttrs('t')]
879
880    @override
881    @classmethod
882    def get_response_types(cls) -> list[type[Response] | None]:
883        return [ScoreSubmitResponse]

Let the server know we got some score in something.

ScoreSubmitMessage(score_token: Annotated[str, <efro.dataclassio.IOAttrs object>])
score_token: Annotated[str, <efro.dataclassio.IOAttrs object at 0x107433590>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
880    @override
881    @classmethod
882    def get_response_types(cls) -> list[type[Response] | None]:
883        return [ScoreSubmitResponse]

Return all Response types this Message can return when sent.

The default implementation specifies a None return type.

@ioprepped
@dataclass
class ScoreSubmitResponse(efro.message._message.Response):
886@ioprepped
887@dataclass
888class ScoreSubmitResponse(Response):
889    """Did something to that inbox entry, boss."""
890
891    # Things we should show on our end.
892    effects: Annotated[list[ClientEffect], IOAttrs('fx')]

Did something to that inbox entry, boss.

ScoreSubmitResponse( effects: Annotated[list[ClientEffect], <efro.dataclassio.IOAttrs object>])
effects: Annotated[list[ClientEffect], <efro.dataclassio.IOAttrs object at 0x10744d430>]
@ioprepped
@dataclass
class ChestActionMessage(efro.message._message.Message):
895@ioprepped
896@dataclass
897class ChestActionMessage(Message):
898    """Request action about a chest."""
899
900    class Action(Enum):
901        """Types of actions we can request."""
902
903        # Unlocking (for free or with tokens).
904        UNLOCK = 'u'
905
906        # Watched an ad to reduce wait.
907        AD = 'ad'
908
909    action: Annotated[Action, IOAttrs('a')]
910
911    # Tokens we are paying (only applies to unlock).
912    token_payment: Annotated[int, IOAttrs('t')]
913
914    chest_id: Annotated[str, IOAttrs('i')]
915
916    @override
917    @classmethod
918    def get_response_types(cls) -> list[type[Response] | None]:
919        return [ChestActionResponse]

Request action about a chest.

ChestActionMessage( action: Annotated[ChestActionMessage.Action, <efro.dataclassio.IOAttrs object>], token_payment: Annotated[int, <efro.dataclassio.IOAttrs object>], chest_id: Annotated[str, <efro.dataclassio.IOAttrs object>])
action: Annotated[ChestActionMessage.Action, <efro.dataclassio.IOAttrs object at 0x10744dd30>]
token_payment: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10744c5c0>]
chest_id: Annotated[str, <efro.dataclassio.IOAttrs object at 0x10744c8c0>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
916    @override
917    @classmethod
918    def get_response_types(cls) -> list[type[Response] | None]:
919        return [ChestActionResponse]

Return all Response types this Message can return when sent.

The default implementation specifies a None return type.

class ChestActionMessage.Action(enum.Enum):
900    class Action(Enum):
901        """Types of actions we can request."""
902
903        # Unlocking (for free or with tokens).
904        UNLOCK = 'u'
905
906        # Watched an ad to reduce wait.
907        AD = 'ad'

Types of actions we can request.

UNLOCK = <Action.UNLOCK: 'u'>
AD = <Action.AD: 'ad'>
@ioprepped
@dataclass
class ChestActionResponse(efro.message._message.Response):
922@ioprepped
923@dataclass
924class ChestActionResponse(Response):
925    """Here's the results of that action you asked for, boss."""
926
927    # Tokens that were actually charged.
928    tokens_charged: Annotated[int, IOAttrs('t')] = 0
929
930    # If present, signifies the chest has been opened and we should show
931    # the user this stuff that was in it.
932    contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None
933
934    # If contents are present, which of the chest's prize-sets they
935    # represent.
936    prizeindex: Annotated[int, IOAttrs('i')] = 0
937
938    # Printable error if something goes wrong.
939    error: Annotated[str | None, IOAttrs('e')] = None
940
941    # Printable warning. Shown in orange with an error sound. Does not
942    # mean the action failed; only that there's something to tell the
943    # users such as 'It looks like you are faking ad views; stop it or
944    # you won't have ad options anymore.'
945    warning: Annotated[str | None, IOAttrs('w', store_default=False)] = None
946
947    # Printable success message. Shown in green with a cash-register
948    # sound. Can be used for things like successful wait reductions via
949    # ad views. Used in builds earlier than 22311; can remove once
950    # 22311+ is ubiquitous.
951    success_msg: Annotated[str | None, IOAttrs('s', store_default=False)] = None
952
953    # Effects to show on the client. Replaces warning and success_msg in
954    # build 22311 or newer.
955    effects: Annotated[
956        list[ClientEffect], IOAttrs('fx', store_default=False)
957    ] = field(default_factory=list)

Here's the results of that action you asked for, boss.

ChestActionResponse( tokens_charged: Annotated[int, <efro.dataclassio.IOAttrs object>] = 0, contents: Annotated[list[DisplayItemWrapper] | None, <efro.dataclassio.IOAttrs object>] = None, prizeindex: Annotated[int, <efro.dataclassio.IOAttrs object>] = 0, error: Annotated[str | None, <efro.dataclassio.IOAttrs object>] = None, warning: Annotated[str | None, <efro.dataclassio.IOAttrs object>] = None, success_msg: Annotated[str | None, <efro.dataclassio.IOAttrs object>] = None, effects: Annotated[list[ClientEffect], <efro.dataclassio.IOAttrs object>] = <factory>)
tokens_charged: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10744e4b0>] = 0
contents: Annotated[list[DisplayItemWrapper] | None, <efro.dataclassio.IOAttrs object at 0x10744e840>] = None
prizeindex: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10744e990>] = 0
error: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x10744ea50>] = None
warning: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x10744ec00>] = None
success_msg: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x10744ed20>] = None
effects: Annotated[list[ClientEffect], <efro.dataclassio.IOAttrs object at 0x10744f290>]