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

Appearances bombsquad classic chests can have.

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

Pretty name for the chest in English.

@ioprepped
@dataclass
class ClassicAccountLiveData:
 80@ioprepped
 81@dataclass
 82class ClassicAccountLiveData:
 83    """Live account data fed to the client in the bs classic app mode."""
 84
 85    @dataclass
 86    class Chest:
 87        """A lovely chest."""
 88
 89        appearance: Annotated[
 90            ClassicChestAppearance,
 91            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
 92        ]
 93        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
 94        ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]
 95
 96    class LeagueType(Enum):
 97        """Type of league we are in."""
 98
 99        BRONZE = 'b'
100        SILVER = 's'
101        GOLD = 'g'
102        DIAMOND = 'd'
103
104    tickets: Annotated[int, IOAttrs('ti')]
105
106    tokens: Annotated[int, IOAttrs('to')]
107    gold_pass: Annotated[bool, IOAttrs('g')]
108    remove_ads: Annotated[bool, IOAttrs('r')]
109
110    achievements: Annotated[int, IOAttrs('a')]
111    achievements_total: Annotated[int, IOAttrs('at')]
112
113    league_type: Annotated[LeagueType | None, IOAttrs('lt')]
114    league_num: Annotated[int | None, IOAttrs('ln')]
115    league_rank: Annotated[int | None, IOAttrs('lr')]
116
117    level: Annotated[int, IOAttrs('lv')]
118    xp: Annotated[int, IOAttrs('xp')]
119    xpmax: Annotated[int, IOAttrs('xpm')]
120
121    inbox_count: Annotated[int, IOAttrs('ibc')]
122    inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
123
124    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 0x104d9e420>]
tokens: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104d9e930>]
gold_pass: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x104d9ea80>]
remove_ads: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x104d9eb70>]
achievements: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104d9ecc0>]
achievements_total: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104d9ee10>]
league_type: Annotated[ClassicAccountLiveData.LeagueType | None, <efro.dataclassio.IOAttrs object at 0x104d9eed0>]
league_num: Annotated[int | None, <efro.dataclassio.IOAttrs object at 0x104d9f020>]
league_rank: Annotated[int | None, <efro.dataclassio.IOAttrs object at 0x104d9f1a0>]
level: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104d9f350>]
xp: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104d9f4a0>]
xpmax: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104d9f5c0>]
inbox_count: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104d9f710>]
inbox_count_is_max: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x104d9f8f0>]
chests: Annotated[dict[str, ClassicAccountLiveData.Chest], <efro.dataclassio.IOAttrs object at 0x104d9f7d0>]
@dataclass
class ClassicAccountLiveData.Chest:
85    @dataclass
86    class Chest:
87        """A lovely chest."""
88
89        appearance: Annotated[
90            ClassicChestAppearance,
91            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
92        ]
93        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
94        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 0x104d97110>]
unlock_time: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x104d959d0>]
ad_allow_time: Annotated[datetime.datetime | None, <efro.dataclassio.IOAttrs object at 0x104d95a30>]
class ClassicAccountLiveData.LeagueType(enum.Enum):
 96    class LeagueType(Enum):
 97        """Type of league we are in."""
 98
 99        BRONZE = 'b'
100        SILVER = 's'
101        GOLD = 'g'
102        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):
127class DisplayItemTypeID(Enum):
128    """Type ID for each of our subclasses."""
129
130    UNKNOWN = 'u'
131    TICKETS = 't'
132    TOKENS = 'k'
133    TEST = 's'
134    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]):
137class DisplayItem(IOMultiType[DisplayItemTypeID]):
138    """Some amount of something that can be shown or described.
139
140    Used to depict chest contents or other rewards or prices.
141    """
142
143    @override
144    @classmethod
145    def get_type_id(cls) -> DisplayItemTypeID:
146        # Require child classes to supply this themselves. If we did a
147        # full type registry/lookup here it would require us to import
148        # everything and would prevent lazy loading.
149        raise NotImplementedError()
150
151    @override
152    @classmethod
153    def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
154        """Return the subclass for each of our type-ids."""
155        # pylint: disable=cyclic-import
156
157        t = DisplayItemTypeID
158        if type_id is t.UNKNOWN:
159            return UnknownDisplayItem
160        if type_id is t.TICKETS:
161            return TicketsDisplayItem
162        if type_id is t.TOKENS:
163            return TokensDisplayItem
164        if type_id is t.TEST:
165            return TestDisplayItem
166        if type_id is t.CHEST:
167            return ChestDisplayItem
168
169        # Important to make sure we provide all types.
170        assert_never(type_id)
171
172    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
173        """Return a string description and subs for the item.
174
175        These decriptions are baked into the DisplayItemWrapper and
176        should be accessed from there when available. This allows
177        clients to give descriptions even for newer display items they
178        don't recognize.
179        """
180        raise NotImplementedError()
181
182    # Implement fallbacks so client can digest item lists even if they
183    # contain unrecognized stuff. DisplayItemWrapper contains basic
184    # baked down info that they can still use in such cases.
185    @override
186    @classmethod
187    def get_unknown_type_fallback(cls) -> DisplayItem:
188        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:
143    @override
144    @classmethod
145    def get_type_id(cls) -> DisplayItemTypeID:
146        # Require child classes to supply this themselves. If we did a
147        # full type registry/lookup here it would require us to import
148        # everything and would prevent lazy loading.
149        raise NotImplementedError()

Return the type-id for this subclass.

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

Return the subclass for each of our type-ids.

def get_description(self) -> tuple[str, list[tuple[str, str]]]:
172    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
173        """Return a string description and subs for the item.
174
175        These decriptions are baked into the DisplayItemWrapper and
176        should be accessed from there when available. This allows
177        clients to give descriptions even for newer display items they
178        don't recognize.
179        """
180        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:
185    @override
186    @classmethod
187    def get_unknown_type_fallback(cls) -> DisplayItem:
188        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]):
191@ioprepped
192@dataclass
193class UnknownDisplayItem(DisplayItem):
194    """Something we don't know how to display."""
195
196    @override
197    @classmethod
198    def get_type_id(cls) -> DisplayItemTypeID:
199        return DisplayItemTypeID.UNKNOWN
200
201    @override
202    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
203        import logging
204
205        # Make noise but don't break.
206        logging.exception(
207            'UnknownDisplayItem.get_description() should never be called.'
208            ' Always access descriptions on the DisplayItemWrapper.'
209        )
210        return 'Unknown', []

Something we don't know how to display.

@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
196    @override
197    @classmethod
198    def get_type_id(cls) -> DisplayItemTypeID:
199        return DisplayItemTypeID.UNKNOWN

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
201    @override
202    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
203        import logging
204
205        # Make noise but don't break.
206        logging.exception(
207            'UnknownDisplayItem.get_description() should never be called.'
208            ' Always access descriptions on the DisplayItemWrapper.'
209        )
210        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]):
213@ioprepped
214@dataclass
215class TicketsDisplayItem(DisplayItem):
216    """Some amount of tickets."""
217
218    count: Annotated[int, IOAttrs('c')]
219
220    @override
221    @classmethod
222    def get_type_id(cls) -> DisplayItemTypeID:
223        return DisplayItemTypeID.TICKETS
224
225    @override
226    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
227        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 0x104d56960>]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
220    @override
221    @classmethod
222    def get_type_id(cls) -> DisplayItemTypeID:
223        return DisplayItemTypeID.TICKETS

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
225    @override
226    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
227        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]):
230@ioprepped
231@dataclass
232class TokensDisplayItem(DisplayItem):
233    """Some amount of tokens."""
234
235    count: Annotated[int, IOAttrs('c')]
236
237    @override
238    @classmethod
239    def get_type_id(cls) -> DisplayItemTypeID:
240        return DisplayItemTypeID.TOKENS
241
242    @override
243    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
244        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 0x104d578c0>]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
237    @override
238    @classmethod
239    def get_type_id(cls) -> DisplayItemTypeID:
240        return DisplayItemTypeID.TOKENS

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
242    @override
243    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
244        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]):
247@ioprepped
248@dataclass
249class TestDisplayItem(DisplayItem):
250    """Fills usable space for a display-item - good for calibration."""
251
252    @override
253    @classmethod
254    def get_type_id(cls) -> DisplayItemTypeID:
255        return DisplayItemTypeID.TEST
256
257    @override
258    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
259        return 'Test Display Item Here', []

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

@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
252    @override
253    @classmethod
254    def get_type_id(cls) -> DisplayItemTypeID:
255        return DisplayItemTypeID.TEST

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
257    @override
258    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
259        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]):
262@ioprepped
263@dataclass
264class ChestDisplayItem(DisplayItem):
265    """Display a chest."""
266
267    appearance: Annotated[ClassicChestAppearance, IOAttrs('a')]
268
269    @override
270    @classmethod
271    def get_type_id(cls) -> DisplayItemTypeID:
272        return DisplayItemTypeID.CHEST
273
274    @override
275    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
276        return self.appearance.pretty_name, []

Display a chest.

ChestDisplayItem( appearance: Annotated[ClassicChestAppearance, <efro.dataclassio.IOAttrs object>])
appearance: Annotated[ClassicChestAppearance, <efro.dataclassio.IOAttrs object at 0x104ef9490>]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
269    @override
270    @classmethod
271    def get_type_id(cls) -> DisplayItemTypeID:
272        return DisplayItemTypeID.CHEST

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
274    @override
275    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
276        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:
279@ioprepped
280@dataclass
281class DisplayItemWrapper:
282    """Wraps a DisplayItem and common info."""
283
284    item: Annotated[DisplayItem, IOAttrs('i')]
285    description: Annotated[str, IOAttrs('d')]
286    description_subs: Annotated[list[str] | None, IOAttrs('s')]
287
288    @classmethod
289    def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
290        """Convenience method to wrap a DisplayItem."""
291        desc, subs = item.get_description()
292        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 0x104ef9f40>]
description: Annotated[str, <efro.dataclassio.IOAttrs object at 0x104efbe60>]
description_subs: Annotated[list[str] | None, <efro.dataclassio.IOAttrs object at 0x104ef89e0>]
@classmethod
def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
288    @classmethod
289    def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
290        """Convenience method to wrap a DisplayItem."""
291        desc, subs = item.get_description()
292        return DisplayItemWrapper(item, desc, pairs_to_flat(subs))

Convenience method to wrap a DisplayItem.

@ioprepped
@dataclass
class ChestInfoMessage(efro.message._message.Message):
295@ioprepped
296@dataclass
297class ChestInfoMessage(Message):
298    """Request info about a chest."""
299
300    chest_id: Annotated[str, IOAttrs('i')]
301
302    @override
303    @classmethod
304    def get_response_types(cls) -> list[type[Response] | None]:
305        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 0x104efafc0>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
302    @override
303    @classmethod
304    def get_response_types(cls) -> list[type[Response] | None]:
305        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):
308@ioprepped
309@dataclass
310class ChestInfoResponse(Response):
311    """Here's that chest info you asked for, boss."""
312
313    @dataclass
314    class Chest:
315        """A lovely chest."""
316
317        @dataclass
318        class PrizeSet:
319            """A possible set of prizes for this chest."""
320
321            weight: Annotated[float, IOAttrs('w')]
322            contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
323
324        appearance: Annotated[
325            ClassicChestAppearance,
326            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
327        ]
328
329        # How much it costs to unlock *now*.
330        unlock_tokens: Annotated[int, IOAttrs('tk')]
331
332        # When it unlocks on its own.
333        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
334
335        # Possible prizes we contain.
336        prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
337
338        # Are ads allowed now?
339        ad_allow: Annotated[bool, IOAttrs('aa')]
340
341    chest: Annotated[Chest | None, IOAttrs('c')]
342    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 0x104efa6f0>]
user_tokens: Annotated[int | None, <efro.dataclassio.IOAttrs object at 0x104efa6c0>]
@dataclass
class ChestInfoResponse.Chest:
313    @dataclass
314    class Chest:
315        """A lovely chest."""
316
317        @dataclass
318        class PrizeSet:
319            """A possible set of prizes for this chest."""
320
321            weight: Annotated[float, IOAttrs('w')]
322            contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
323
324        appearance: Annotated[
325            ClassicChestAppearance,
326            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
327        ]
328
329        # How much it costs to unlock *now*.
330        unlock_tokens: Annotated[int, IOAttrs('tk')]
331
332        # When it unlocks on its own.
333        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
334
335        # Possible prizes we contain.
336        prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
337
338        # Are ads allowed now?
339        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 0x104efb9b0>]
unlock_tokens: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104efbce0>]
unlock_time: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x104efbda0>]
prizesets: Annotated[list[ChestInfoResponse.Chest.PrizeSet], <efro.dataclassio.IOAttrs object at 0x104efbf20>]
ad_allow: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x104ecc170>]
@dataclass
class ChestInfoResponse.Chest.PrizeSet:
317        @dataclass
318        class PrizeSet:
319            """A possible set of prizes for this chest."""
320
321            weight: Annotated[float, IOAttrs('w')]
322            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 0x104ece3f0>]
contents: Annotated[list[DisplayItemWrapper], <efro.dataclassio.IOAttrs object at 0x104ecd160>]
@ioprepped
@dataclass
class ChestActionMessage(efro.message._message.Message):
345@ioprepped
346@dataclass
347class ChestActionMessage(Message):
348    """Request action about a chest."""
349
350    class Action(Enum):
351        """Types of actions we can request."""
352
353        # Unlocking (for free or with tokens).
354        UNLOCK = 'u'
355
356        # Watched an ad to reduce wait.
357        AD = 'ad'
358
359    action: Annotated[Action, IOAttrs('a')]
360
361    # Tokens we are paying (only applies to unlock).
362    token_payment: Annotated[int, IOAttrs('t')]
363
364    chest_id: Annotated[str, IOAttrs('i')]
365
366    @override
367    @classmethod
368    def get_response_types(cls) -> list[type[Response] | None]:
369        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 0x104eceff0>]
token_payment: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104eccb60>]
chest_id: Annotated[str, <efro.dataclassio.IOAttrs object at 0x104eccb90>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
366    @override
367    @classmethod
368    def get_response_types(cls) -> list[type[Response] | None]:
369        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):
350    class Action(Enum):
351        """Types of actions we can request."""
352
353        # Unlocking (for free or with tokens).
354        UNLOCK = 'u'
355
356        # Watched an ad to reduce wait.
357        AD = 'ad'

Types of actions we can request.

UNLOCK = <Action.UNLOCK: 'u'>
AD = <Action.AD: 'ad'>
@ioprepped
@dataclass
class ChestActionResponse(efro.message._message.Response):
372@ioprepped
373@dataclass
374class ChestActionResponse(Response):
375    """Here's the results of that action you asked for, boss."""
376
377    # Tokens that were actually charged.
378    tokens_charged: Annotated[int, IOAttrs('t')] = 0
379
380    # If present, signifies the chest has been opened and we should show
381    # the user this stuff that was in it.
382    contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None
383
384    # If contents are present, which of the chest's prize-sets they
385    # represent.
386    prizeindex: Annotated[int, IOAttrs('i')] = 0
387
388    # Printable error if something goes wrong.
389    error: Annotated[str | None, IOAttrs('e')] = None
390
391    # Printable warning. Shown in orange with an error sound. Does not
392    # mean the action failed; only that there's something to tell the
393    # users such as 'It looks like you are faking ad views; stop it or
394    # you won't have ad options anymore.'
395    warning: Annotated[str | None, IOAttrs('w')] = None
396
397    # Printable success message. Shown in green with a cash-register
398    # sound. Can be used for things like successful wait reductions via
399    # ad views.
400    success_msg: Annotated[str | None, IOAttrs('s')] = None

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)
tokens_charged: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104ecfdd0>] = 0
contents: Annotated[list[DisplayItemWrapper] | None, <efro.dataclassio.IOAttrs object at 0x104ecff80>] = None
prizeindex: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104e0c110>] = 0
error: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x104e0c1a0>] = None
warning: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x104e0c170>] = None
success_msg: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x104e0c320>] = None
class ClientUITypeID(enum.Enum):
403class ClientUITypeID(Enum):
404    """Type ID for each of our subclasses."""
405
406    UNKNOWN = 'u'
407    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]):
410class ClientUI(IOMultiType[ClientUITypeID]):
411    """Defines some user interface on the client."""
412
413    @override
414    @classmethod
415    def get_type_id(cls) -> ClientUITypeID:
416        # Require child classes to supply this themselves. If we did a
417        # full type registry/lookup here it would require us to import
418        # everything and would prevent lazy loading.
419        raise NotImplementedError()
420
421    @override
422    @classmethod
423    def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
424        """Return the subclass for each of our type-ids."""
425        # pylint: disable=cyclic-import
426        out: type[ClientUI]
427
428        t = ClientUITypeID
429        if type_id is t.UNKNOWN:
430            out = UnknownClientUI
431        elif type_id is t.BASIC:
432            out = BasicClientUI
433        else:
434            # Important to make sure we provide all types.
435            assert_never(type_id)
436        return out
437
438    @override
439    @classmethod
440    def get_unknown_type_fallback(cls) -> ClientUI:
441        # If we encounter some future message type we don't know
442        # anything about, drop in a placeholder.
443        return UnknownClientUI()

Defines some user interface on the client.

@override
@classmethod
def get_type_id(cls) -> ClientUITypeID:
413    @override
414    @classmethod
415    def get_type_id(cls) -> ClientUITypeID:
416        # Require child classes to supply this themselves. If we did a
417        # full type registry/lookup here it would require us to import
418        # everything and would prevent lazy loading.
419        raise NotImplementedError()

Return the type-id for this subclass.

@override
@classmethod
def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
421    @override
422    @classmethod
423    def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
424        """Return the subclass for each of our type-ids."""
425        # pylint: disable=cyclic-import
426        out: type[ClientUI]
427
428        t = ClientUITypeID
429        if type_id is t.UNKNOWN:
430            out = UnknownClientUI
431        elif type_id is t.BASIC:
432            out = BasicClientUI
433        else:
434            # Important to make sure we provide all types.
435            assert_never(type_id)
436        return out

Return the subclass for each of our type-ids.

@override
@classmethod
def get_unknown_type_fallback(cls) -> ClientUI:
438    @override
439    @classmethod
440    def get_unknown_type_fallback(cls) -> ClientUI:
441        # If we encounter some future message type we don't know
442        # anything about, drop in a placeholder.
443        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]):
446@ioprepped
447@dataclass
448class UnknownClientUI(ClientUI):
449    """Fallback type for unrecognized entries."""
450
451    @override
452    @classmethod
453    def get_type_id(cls) -> ClientUITypeID:
454        return ClientUITypeID.UNKNOWN

Fallback type for unrecognized entries.

@override
@classmethod
def get_type_id(cls) -> ClientUITypeID:
451    @override
452    @classmethod
453    def get_type_id(cls) -> ClientUITypeID:
454        return ClientUITypeID.UNKNOWN

Return the type-id for this subclass.

class BasicClientUIComponentTypeID(enum.Enum):
457class BasicClientUIComponentTypeID(Enum):
458    """Type ID for each of our subclasses."""
459
460    UNKNOWN = 'u'
461    TEXT = 't'
462    LINK = 'l'
463    BS_CLASSIC_TOURNEY_RESULT = 'ct'
464    DISPLAY_ITEMS = 'di'
465    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]):
468class BasicClientUIComponent(IOMultiType[BasicClientUIComponentTypeID]):
469    """Top level class for our multitype."""
470
471    @override
472    @classmethod
473    def get_type_id(cls) -> BasicClientUIComponentTypeID:
474        # Require child classes to supply this themselves. If we did a
475        # full type registry/lookup here it would require us to import
476        # everything and would prevent lazy loading.
477        raise NotImplementedError()
478
479    @override
480    @classmethod
481    def get_type(
482        cls, type_id: BasicClientUIComponentTypeID
483    ) -> type[BasicClientUIComponent]:
484        """Return the subclass for each of our type-ids."""
485        # pylint: disable=cyclic-import
486
487        t = BasicClientUIComponentTypeID
488        if type_id is t.UNKNOWN:
489            return BasicClientUIComponentUnknown
490        if type_id is t.TEXT:
491            return BasicClientUIComponentText
492        if type_id is t.LINK:
493            return BasicClientUIComponentLink
494        if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
495            return BasicClientUIBsClassicTourneyResult
496        if type_id is t.DISPLAY_ITEMS:
497            return BasicClientUIDisplayItems
498        if type_id is t.EXPIRE_TIME:
499            return BasicClientUIExpireTime
500
501        # Important to make sure we provide all types.
502        assert_never(type_id)
503
504    @override
505    @classmethod
506    def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
507        # If we encounter some future message type we don't know
508        # anything about, drop in a placeholder.
509        return BasicClientUIComponentUnknown()

Top level class for our multitype.

@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
471    @override
472    @classmethod
473    def get_type_id(cls) -> BasicClientUIComponentTypeID:
474        # Require child classes to supply this themselves. If we did a
475        # full type registry/lookup here it would require us to import
476        # everything and would prevent lazy loading.
477        raise NotImplementedError()

Return the type-id for this subclass.

@override
@classmethod
def get_type( cls, type_id: BasicClientUIComponentTypeID) -> type[BasicClientUIComponent]:
479    @override
480    @classmethod
481    def get_type(
482        cls, type_id: BasicClientUIComponentTypeID
483    ) -> type[BasicClientUIComponent]:
484        """Return the subclass for each of our type-ids."""
485        # pylint: disable=cyclic-import
486
487        t = BasicClientUIComponentTypeID
488        if type_id is t.UNKNOWN:
489            return BasicClientUIComponentUnknown
490        if type_id is t.TEXT:
491            return BasicClientUIComponentText
492        if type_id is t.LINK:
493            return BasicClientUIComponentLink
494        if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
495            return BasicClientUIBsClassicTourneyResult
496        if type_id is t.DISPLAY_ITEMS:
497            return BasicClientUIDisplayItems
498        if type_id is t.EXPIRE_TIME:
499            return BasicClientUIExpireTime
500
501        # Important to make sure we provide all types.
502        assert_never(type_id)

Return the subclass for each of our type-ids.

@override
@classmethod
def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
504    @override
505    @classmethod
506    def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
507        # If we encounter some future message type we don't know
508        # anything about, drop in a placeholder.
509        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]):
512@ioprepped
513@dataclass
514class BasicClientUIComponentUnknown(BasicClientUIComponent):
515    """An unknown basic client component type.
516
517    In practice these should never show up since the master-server
518    generates these on the fly for the client and so should not send
519    clients one they can't digest.
520    """
521
522    @override
523    @classmethod
524    def get_type_id(cls) -> BasicClientUIComponentTypeID:
525        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:
522    @override
523    @classmethod
524    def get_type_id(cls) -> BasicClientUIComponentTypeID:
525        return BasicClientUIComponentTypeID.UNKNOWN

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIComponentText(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
528@ioprepped
529@dataclass
530class BasicClientUIComponentText(BasicClientUIComponent):
531    """Show some text in the inbox message."""
532
533    text: Annotated[str, IOAttrs('t')]
534    subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
535        default_factory=list
536    )
537    scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0
538    color: Annotated[
539        tuple[float, float, float, float], IOAttrs('c', store_default=False)
540    ] = (1.0, 1.0, 1.0, 1.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.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 0x104e22ed0>]
subs: Annotated[list[str], <efro.dataclassio.IOAttrs object at 0x104e215b0>]
scale: Annotated[float, <efro.dataclassio.IOAttrs object at 0x104e217c0>] = 1.0
color: Annotated[tuple[float, float, float, float], <efro.dataclassio.IOAttrs object at 0x104e21940>] = (1.0, 1.0, 1.0, 1.0)
spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object at 0x104e218e0>] = 0.0
spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object at 0x104e21a90>] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
544    @override
545    @classmethod
546    def get_type_id(cls) -> BasicClientUIComponentTypeID:
547        return BasicClientUIComponentTypeID.TEXT

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIBsClassicTourneyResult(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
569@ioprepped
570@dataclass
571class BasicClientUIBsClassicTourneyResult(BasicClientUIComponent):
572    """Show info about a classic tourney."""
573
574    tournament_id: Annotated[str, IOAttrs('t')]
575    game: Annotated[str, IOAttrs('g')]
576    players: Annotated[int, IOAttrs('p')]
577    rank: Annotated[int, IOAttrs('r')]
578    trophy: Annotated[str | None, IOAttrs('tr')]
579    prizes: Annotated[list[DisplayItemWrapper], IOAttrs('pr')]
580
581    @override
582    @classmethod
583    def get_type_id(cls) -> BasicClientUIComponentTypeID:
584        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 0x104e49e20>]
game: Annotated[str, <efro.dataclassio.IOAttrs object at 0x104e4bf20>]
players: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104e48c50>]
rank: Annotated[int, <efro.dataclassio.IOAttrs object at 0x104e48d40>]
trophy: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x104e48e00>]
prizes: Annotated[list[DisplayItemWrapper], <efro.dataclassio.IOAttrs object at 0x104e48fb0>]
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
581    @override
582    @classmethod
583    def get_type_id(cls) -> BasicClientUIComponentTypeID:
584        return BasicClientUIComponentTypeID.BS_CLASSIC_TOURNEY_RESULT

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIDisplayItems(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
587@ioprepped
588@dataclass
589class BasicClientUIDisplayItems(BasicClientUIComponent):
590    """Show some display-items."""
591
592    items: Annotated[list[DisplayItemWrapper], IOAttrs('d')]
593    width: Annotated[float, IOAttrs('w')] = 100.0
594    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
595    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
596
597    @override
598    @classmethod
599    def get_type_id(cls) -> BasicClientUIComponentTypeID:
600        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 0x104e4b680>]
width: Annotated[float, <efro.dataclassio.IOAttrs object at 0x104e4a5d0>] = 100.0
spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object at 0x104e4a720>] = 0.0
spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object at 0x104e4a7e0>] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
597    @override
598    @classmethod
599    def get_type_id(cls) -> BasicClientUIComponentTypeID:
600        return BasicClientUIComponentTypeID.DISPLAY_ITEMS

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIExpireTime(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
603@ioprepped
604@dataclass
605class BasicClientUIExpireTime(BasicClientUIComponent):
606    """Show expire-time."""
607
608    time: Annotated[datetime.datetime, IOAttrs('d')]
609    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
610    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
611
612    @override
613    @classmethod
614    def get_type_id(cls) -> BasicClientUIComponentTypeID:
615        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 0x104e4b350>]
spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object at 0x104e4bd10>] = 0.0
spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object at 0x104e4be60>] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
612    @override
613    @classmethod
614    def get_type_id(cls) -> BasicClientUIComponentTypeID:
615        return BasicClientUIComponentTypeID.EXPIRE_TIME

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUI(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientUITypeID]):
618@ioprepped
619@dataclass
620class BasicClientUI(ClientUI):
621    """A basic UI for the client."""
622
623    class ButtonLabel(Enum):
624        """Distinct button labels we support."""
625
626        UNKNOWN = 'u'
627        OK = 'o'
628        APPLY = 'a'
629        CANCEL = 'c'
630        ACCEPT = 'ac'
631        DECLINE = 'dn'
632        IGNORE = 'ig'
633        CLAIM = 'cl'
634        DISCARD = 'd'
635
636    class InteractionStyle(Enum):
637        """Overall interaction styles we support."""
638
639        UNKNOWN = 'u'
640        BUTTON_POSITIVE = 'p'
641        BUTTON_POSITIVE_NEGATIVE = 'pn'
642
643    components: Annotated[list[BasicClientUIComponent], IOAttrs('s')]
644
645    interaction_style: Annotated[
646        InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN)
647    ] = InteractionStyle.BUTTON_POSITIVE
648
649    button_label_positive: Annotated[
650        ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN)
651    ] = ButtonLabel.OK
652
653    button_label_negative: Annotated[
654        ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN)
655    ] = ButtonLabel.CANCEL
656
657    @override
658    @classmethod
659    def get_type_id(cls) -> ClientUITypeID:
660        return ClientUITypeID.BASIC
661
662    def contains_unknown_elements(self) -> bool:
663        """Whether something within us is an unknown type or enum."""
664        return (
665            self.interaction_style is self.InteractionStyle.UNKNOWN
666            or self.button_label_positive is self.ButtonLabel.UNKNOWN
667            or self.button_label_negative is self.ButtonLabel.UNKNOWN
668            or any(
669                c.get_type_id() is BasicClientUIComponentTypeID.UNKNOWN
670                for c in self.components
671            )
672        )

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 0x103e17fb0>]
interaction_style: Annotated[BasicClientUI.InteractionStyle, <efro.dataclassio.IOAttrs object at 0x103f39790>] = <InteractionStyle.BUTTON_POSITIVE: 'p'>
button_label_positive: Annotated[BasicClientUI.ButtonLabel, <efro.dataclassio.IOAttrs object at 0x103f3b980>] = <ButtonLabel.OK: 'o'>
button_label_negative: Annotated[BasicClientUI.ButtonLabel, <efro.dataclassio.IOAttrs object at 0x103f3a2a0>] = <ButtonLabel.CANCEL: 'c'>
@override
@classmethod
def get_type_id(cls) -> ClientUITypeID:
657    @override
658    @classmethod
659    def get_type_id(cls) -> ClientUITypeID:
660        return ClientUITypeID.BASIC

Return the type-id for this subclass.

def contains_unknown_elements(self) -> bool:
662    def contains_unknown_elements(self) -> bool:
663        """Whether something within us is an unknown type or enum."""
664        return (
665            self.interaction_style is self.InteractionStyle.UNKNOWN
666            or self.button_label_positive is self.ButtonLabel.UNKNOWN
667            or self.button_label_negative is self.ButtonLabel.UNKNOWN
668            or any(
669                c.get_type_id() is BasicClientUIComponentTypeID.UNKNOWN
670                for c in self.components
671            )
672        )

Whether something within us is an unknown type or enum.

class BasicClientUI.ButtonLabel(enum.Enum):
623    class ButtonLabel(Enum):
624        """Distinct button labels we support."""
625
626        UNKNOWN = 'u'
627        OK = 'o'
628        APPLY = 'a'
629        CANCEL = 'c'
630        ACCEPT = 'ac'
631        DECLINE = 'dn'
632        IGNORE = 'ig'
633        CLAIM = 'cl'
634        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):
636    class InteractionStyle(Enum):
637        """Overall interaction styles we support."""
638
639        UNKNOWN = 'u'
640        BUTTON_POSITIVE = 'p'
641        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:
675@ioprepped
676@dataclass
677class ClientUIWrapper:
678    """Wrapper for a ClientUI and its common data."""
679
680    id: Annotated[str, IOAttrs('i')]
681    createtime: Annotated[datetime.datetime, IOAttrs('c')]
682    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 0x104e42cf0>]
createtime: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x104e42ed0>]
ui: Annotated[ClientUI, <efro.dataclassio.IOAttrs object at 0x104e42ff0>]
@ioprepped
@dataclass
class InboxRequestMessage(efro.message._message.Message):
685@ioprepped
686@dataclass
687class InboxRequestMessage(Message):
688    """Message requesting our inbox."""
689
690    @override
691    @classmethod
692    def get_response_types(cls) -> list[type[Response] | None]:
693        return [InboxRequestResponse]

Message requesting our inbox.

@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
690    @override
691    @classmethod
692    def get_response_types(cls) -> list[type[Response] | None]:
693        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):
696@ioprepped
697@dataclass
698class InboxRequestResponse(Response):
699    """Here's that inbox contents you asked for, boss."""
700
701    wrappers: Annotated[list[ClientUIWrapper], IOAttrs('w')]
702
703    # Printable error if something goes wrong.
704    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 0x104f71a90>]
error: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x104f739e0>] = None
class ClientUIAction(enum.Enum):
707class ClientUIAction(Enum):
708    """Types of actions we can run."""
709
710    BUTTON_PRESS_POSITIVE = 'p'
711    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):
714class ClientEffectTypeID(Enum):
715    """Type ID for each of our subclasses."""
716
717    UNKNOWN = 'u'
718    SCREEN_MESSAGE = 'm'
719    SOUND = 's'
720    DELAY = 'd'

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'>
class ClientEffect(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
723class ClientEffect(IOMultiType[ClientEffectTypeID]):
724    """Something that can happen on the client.
725
726    This can include screen messages, sounds, visual effects, etc.
727    """
728
729    @override
730    @classmethod
731    def get_type_id(cls) -> ClientEffectTypeID:
732        # Require child classes to supply this themselves. If we did a
733        # full type registry/lookup here it would require us to import
734        # everything and would prevent lazy loading.
735        raise NotImplementedError()
736
737    @override
738    @classmethod
739    def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
740        """Return the subclass for each of our type-ids."""
741        # pylint: disable=cyclic-import
742
743        t = ClientEffectTypeID
744        if type_id is t.UNKNOWN:
745            return ClientEffectUnknown
746        if type_id is t.SCREEN_MESSAGE:
747            return ClientEffectScreenMessage
748        if type_id is t.SOUND:
749            return ClientEffectSound
750        if type_id is t.DELAY:
751            return ClientEffectDelay
752
753        # Important to make sure we provide all types.
754        assert_never(type_id)
755
756    @override
757    @classmethod
758    def get_unknown_type_fallback(cls) -> ClientEffect:
759        # If we encounter some future message type we don't know
760        # anything about, drop in a placeholder.
761        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:
729    @override
730    @classmethod
731    def get_type_id(cls) -> ClientEffectTypeID:
732        # Require child classes to supply this themselves. If we did a
733        # full type registry/lookup here it would require us to import
734        # everything and would prevent lazy loading.
735        raise NotImplementedError()

Return the type-id for this subclass.

@override
@classmethod
def get_type( cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
737    @override
738    @classmethod
739    def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
740        """Return the subclass for each of our type-ids."""
741        # pylint: disable=cyclic-import
742
743        t = ClientEffectTypeID
744        if type_id is t.UNKNOWN:
745            return ClientEffectUnknown
746        if type_id is t.SCREEN_MESSAGE:
747            return ClientEffectScreenMessage
748        if type_id is t.SOUND:
749            return ClientEffectSound
750        if type_id is t.DELAY:
751            return ClientEffectDelay
752
753        # Important to make sure we provide all types.
754        assert_never(type_id)

Return the subclass for each of our type-ids.

@override
@classmethod
def get_unknown_type_fallback(cls) -> ClientEffect:
756    @override
757    @classmethod
758    def get_unknown_type_fallback(cls) -> ClientEffect:
759        # If we encounter some future message type we don't know
760        # anything about, drop in a placeholder.
761        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]):
764@ioprepped
765@dataclass
766class ClientEffectUnknown(ClientEffect):
767    """Fallback substitute for types we don't recognize."""
768
769    @override
770    @classmethod
771    def get_type_id(cls) -> ClientEffectTypeID:
772        return ClientEffectTypeID.UNKNOWN

Fallback substitute for types we don't recognize.

@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
769    @override
770    @classmethod
771    def get_type_id(cls) -> ClientEffectTypeID:
772        return ClientEffectTypeID.UNKNOWN

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientEffectScreenMessage(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
775@ioprepped
776@dataclass
777class ClientEffectScreenMessage(ClientEffect):
778    """Display a screen-message."""
779
780    message: Annotated[str, IOAttrs('m')]
781    subs: Annotated[list[str], IOAttrs('s')]
782    color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0)
783
784    @override
785    @classmethod
786    def get_type_id(cls) -> ClientEffectTypeID:
787        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 0x104f41970>]
subs: Annotated[list[str], <efro.dataclassio.IOAttrs object at 0x104f439e0>]
color: Annotated[tuple[float, float, float], <efro.dataclassio.IOAttrs object at 0x104f40500>] = (1.0, 1.0, 1.0)
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
784    @override
785    @classmethod
786    def get_type_id(cls) -> ClientEffectTypeID:
787        return ClientEffectTypeID.SCREEN_MESSAGE

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientEffectSound(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
790@ioprepped
791@dataclass
792class ClientEffectSound(ClientEffect):
793    """Play a sound."""
794
795    class Sound(Enum):
796        """Sounds that can be made alongside the message."""
797
798        UNKNOWN = 'u'
799        CASH_REGISTER = 'c'
800        ERROR = 'e'
801        POWER_DOWN = 'p'
802        GUN_COCKING = 'g'
803
804    sound: Annotated[Sound, IOAttrs('s', enum_fallback=Sound.UNKNOWN)]
805    volume: Annotated[float, IOAttrs('v')] = 1.0
806
807    @override
808    @classmethod
809    def get_type_id(cls) -> ClientEffectTypeID:
810        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 0x104f42900>]
volume: Annotated[float, <efro.dataclassio.IOAttrs object at 0x104f41190>] = 1.0
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
807    @override
808    @classmethod
809    def get_type_id(cls) -> ClientEffectTypeID:
810        return ClientEffectTypeID.SOUND

Return the type-id for this subclass.

class ClientEffectSound.Sound(enum.Enum):
795    class Sound(Enum):
796        """Sounds that can be made alongside the message."""
797
798        UNKNOWN = 'u'
799        CASH_REGISTER = 'c'
800        ERROR = 'e'
801        POWER_DOWN = 'p'
802        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 ClientEffectDelay(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
813@ioprepped
814@dataclass
815class ClientEffectDelay(ClientEffect):
816    """Delay effect processing."""
817
818    seconds: Annotated[float, IOAttrs('s')]
819
820    @override
821    @classmethod
822    def get_type_id(cls) -> ClientEffectTypeID:
823        return ClientEffectTypeID.DELAY

Delay effect processing.

ClientEffectDelay(seconds: Annotated[float, <efro.dataclassio.IOAttrs object>])
seconds: Annotated[float, <efro.dataclassio.IOAttrs object at 0x104f433e0>]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
820    @override
821    @classmethod
822    def get_type_id(cls) -> ClientEffectTypeID:
823        return ClientEffectTypeID.DELAY

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientUIActionMessage(efro.message._message.Message):
826@ioprepped
827@dataclass
828class ClientUIActionMessage(Message):
829    """Do something to a client ui."""
830
831    id: Annotated[str, IOAttrs('i')]
832    action: Annotated[ClientUIAction, IOAttrs('a')]
833
834    @override
835    @classmethod
836    def get_response_types(cls) -> list[type[Response] | None]:
837        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 0x104ffd610>]
action: Annotated[ClientUIAction, <efro.dataclassio.IOAttrs object at 0x104fff410>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
834    @override
835    @classmethod
836    def get_response_types(cls) -> list[type[Response] | None]:
837        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):
840@ioprepped
841@dataclass
842class ClientUIActionResponse(Response):
843    """Did something to that inbox entry, boss."""
844
845    class ErrorType(Enum):
846        """Types of errors that may have occurred."""
847
848        # Probably a future error type we don't recognize.
849        UNKNOWN = 'u'
850
851        # Something went wrong on the server, but specifics are not
852        # relevant.
853        INTERNAL = 'i'
854
855        # The entry expired on the server. In various cases such as 'ok'
856        # buttons this can generally be ignored.
857        EXPIRED = 'e'
858
859    error_type: Annotated[
860        ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN)
861    ]
862
863    # User facing error message in the case of errors.
864    error_message: Annotated[str | None, IOAttrs('em')]
865
866    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 0x104fffb00>]
error_message: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x104ffcf80>]
effects: Annotated[list[ClientEffect], <efro.dataclassio.IOAttrs object at 0x104ffcc20>]
class ClientUIActionResponse.ErrorType(enum.Enum):
845    class ErrorType(Enum):
846        """Types of errors that may have occurred."""
847
848        # Probably a future error type we don't recognize.
849        UNKNOWN = 'u'
850
851        # Something went wrong on the server, but specifics are not
852        # relevant.
853        INTERNAL = 'i'
854
855        # The entry expired on the server. In various cases such as 'ok'
856        # buttons this can generally be ignored.
857        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):
869@ioprepped
870@dataclass
871class ScoreSubmitMessage(Message):
872    """Let the server know we got some score in something."""
873
874    score_token: Annotated[str, IOAttrs('t')]
875
876    @override
877    @classmethod
878    def get_response_types(cls) -> list[type[Response] | None]:
879        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 0x104ffed50>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
876    @override
877    @classmethod
878    def get_response_types(cls) -> list[type[Response] | None]:
879        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):
882@ioprepped
883@dataclass
884class ScoreSubmitResponse(Response):
885    """Did something to that inbox entry, boss."""
886
887    # Things we should show on our end.
888    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 0x104fffce0>]