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

Appearances bombsquad classic chests can have.

@ioprepped
@dataclass
class ClassicAccountLiveData:
55@ioprepped
56@dataclass
57class ClassicAccountLiveData:
58    """Live account data fed to the client in the bs classic app mode."""
59
60    @dataclass
61    class Chest:
62        """A lovely chest."""
63
64        appearance: Annotated[
65            ClassicChestAppearance,
66            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
67        ]
68        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
69        ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]
70
71    class LeagueType(Enum):
72        """Type of league we are in."""
73
74        BRONZE = 'b'
75        SILVER = 's'
76        GOLD = 'g'
77        DIAMOND = 'd'
78
79    tickets: Annotated[int, IOAttrs('ti')]
80
81    tokens: Annotated[int, IOAttrs('to')]
82    gold_pass: Annotated[bool, IOAttrs('g')]
83
84    achievements: Annotated[int, IOAttrs('a')]
85    achievements_total: Annotated[int, IOAttrs('at')]
86
87    league_type: Annotated[LeagueType | None, IOAttrs('lt')]
88    league_num: Annotated[int | None, IOAttrs('ln')]
89    league_rank: Annotated[int | None, IOAttrs('lr')]
90
91    level: Annotated[int, IOAttrs('lv')]
92    xp: Annotated[int, IOAttrs('xp')]
93    xpmax: Annotated[int, IOAttrs('xpm')]
94
95    inbox_count: Annotated[int, IOAttrs('ibc')]
96    inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
97
98    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>], 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 0x10636fbc0>]
tokens: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10636deb0>]
gold_pass: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x10636dfa0>]
achievements: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10636e090>]
achievements_total: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10636e240>]
league_type: Annotated[ClassicAccountLiveData.LeagueType | None, <efro.dataclassio.IOAttrs object at 0x10636e360>]
league_num: Annotated[int | None, <efro.dataclassio.IOAttrs object at 0x10636e4e0>]
league_rank: Annotated[int | None, <efro.dataclassio.IOAttrs object at 0x10636e600>]
level: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10636e7b0>]
xp: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10636e8d0>]
xpmax: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10636e9f0>]
inbox_count: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10636eb10>]
inbox_count_is_max: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x10636ec60>]
chests: Annotated[dict[str, ClassicAccountLiveData.Chest], <efro.dataclassio.IOAttrs object at 0x10636ebd0>]
@dataclass
class ClassicAccountLiveData.Chest:
60    @dataclass
61    class Chest:
62        """A lovely chest."""
63
64        appearance: Annotated[
65            ClassicChestAppearance,
66            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
67        ]
68        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
69        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 0x1063e6990>]
unlock_time: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x1063e6ed0>]
ad_allow_time: Annotated[datetime.datetime | None, <efro.dataclassio.IOAttrs object at 0x1063e7170>]
class ClassicAccountLiveData.LeagueType(enum.Enum):
71    class LeagueType(Enum):
72        """Type of league we are in."""
73
74        BRONZE = 'b'
75        SILVER = 's'
76        GOLD = 'g'
77        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):
101class DisplayItemTypeID(Enum):
102    """Type ID for each of our subclasses."""
103
104    UNKNOWN = 'u'
105    TICKETS = 't'
106    TOKENS = 'k'
107    TEST = 's'
108    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]):
111class DisplayItem(IOMultiType[DisplayItemTypeID]):
112    """Some amount of something that can be shown or described.
113
114    Used to depict chest contents or other rewards or prices.
115    """
116
117    @override
118    @classmethod
119    def get_type_id(cls) -> DisplayItemTypeID:
120        # Require child classes to supply this themselves. If we did a
121        # full type registry/lookup here it would require us to import
122        # everything and would prevent lazy loading.
123        raise NotImplementedError()
124
125    @override
126    @classmethod
127    def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
128        """Return the subclass for each of our type-ids."""
129        # pylint: disable=cyclic-import
130
131        t = DisplayItemTypeID
132        if type_id is t.UNKNOWN:
133            return UnknownDisplayItem
134        if type_id is t.TICKETS:
135            return TicketsDisplayItem
136        if type_id is t.TOKENS:
137            return TokensDisplayItem
138        if type_id is t.TEST:
139            return TestDisplayItem
140        if type_id is t.CHEST:
141            return ChestDisplayItem
142
143        # Important to make sure we provide all types.
144        assert_never(type_id)
145
146    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
147        """Return a string description and subs for the item.
148
149        These decriptions are baked into the DisplayItemWrapper and
150        should be accessed from there by the client. This should only be
151        called on the server side when doing said baking.
152        """
153        raise NotImplementedError()
154
155    # Implement fallbacks so client can digest item lists even if they
156    # contain unrecognized stuff. DisplayItemWrapper contains basic
157    # baked down info that they can still use in such cases.
158    @override
159    @classmethod
160    def get_unknown_type_fallback(cls) -> DisplayItem:
161        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:
117    @override
118    @classmethod
119    def get_type_id(cls) -> DisplayItemTypeID:
120        # Require child classes to supply this themselves. If we did a
121        # full type registry/lookup here it would require us to import
122        # everything and would prevent lazy loading.
123        raise NotImplementedError()

Return the type-id for this subclass.

@override
@classmethod
def get_type( cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
125    @override
126    @classmethod
127    def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
128        """Return the subclass for each of our type-ids."""
129        # pylint: disable=cyclic-import
130
131        t = DisplayItemTypeID
132        if type_id is t.UNKNOWN:
133            return UnknownDisplayItem
134        if type_id is t.TICKETS:
135            return TicketsDisplayItem
136        if type_id is t.TOKENS:
137            return TokensDisplayItem
138        if type_id is t.TEST:
139            return TestDisplayItem
140        if type_id is t.CHEST:
141            return ChestDisplayItem
142
143        # Important to make sure we provide all types.
144        assert_never(type_id)

Return the subclass for each of our type-ids.

def get_description(self) -> tuple[str, list[tuple[str, str]]]:
146    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
147        """Return a string description and subs for the item.
148
149        These decriptions are baked into the DisplayItemWrapper and
150        should be accessed from there by the client. This should only be
151        called on the server side when doing said baking.
152        """
153        raise NotImplementedError()

Return a string description and subs for the item.

These decriptions are baked into the DisplayItemWrapper and should be accessed from there by the client. This should only be called on the server side when doing said baking.

@override
@classmethod
def get_unknown_type_fallback(cls) -> DisplayItem:
158    @override
159    @classmethod
160    def get_unknown_type_fallback(cls) -> DisplayItem:
161        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]):
164@ioprepped
165@dataclass
166class UnknownDisplayItem(DisplayItem):
167    """Something we don't know how to display."""
168
169    @override
170    @classmethod
171    def get_type_id(cls) -> DisplayItemTypeID:
172        return DisplayItemTypeID.UNKNOWN
173
174    @override
175    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
176        import logging
177
178        # Make noise but don't break.
179        logging.exception(
180            'UnknownDisplayItem.get_description() should never be called.'
181            ' Always access descriptions on the DisplayItemWrapper.'
182        )
183        return 'Unknown', []

Something we don't know how to display.

@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
169    @override
170    @classmethod
171    def get_type_id(cls) -> DisplayItemTypeID:
172        return DisplayItemTypeID.UNKNOWN

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
174    @override
175    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
176        import logging
177
178        # Make noise but don't break.
179        logging.exception(
180            'UnknownDisplayItem.get_description() should never be called.'
181            ' Always access descriptions on the DisplayItemWrapper.'
182        )
183        return 'Unknown', []

Return a string description and subs for the item.

These decriptions are baked into the DisplayItemWrapper and should be accessed from there by the client. This should only be called on the server side when doing said baking.

@ioprepped
@dataclass
class TicketsDisplayItem(efro.dataclassio._base.IOMultiType[bacommon.bs.DisplayItemTypeID]):
186@ioprepped
187@dataclass
188class TicketsDisplayItem(DisplayItem):
189    """Some amount of tickets."""
190
191    count: Annotated[int, IOAttrs('c')]
192
193    @override
194    @classmethod
195    def get_type_id(cls) -> DisplayItemTypeID:
196        return DisplayItemTypeID.TICKETS
197
198    @override
199    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
200        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 0x106356090>]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
193    @override
194    @classmethod
195    def get_type_id(cls) -> DisplayItemTypeID:
196        return DisplayItemTypeID.TICKETS

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
198    @override
199    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
200        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 by the client. This should only be called on the server side when doing said baking.

@ioprepped
@dataclass
class TokensDisplayItem(efro.dataclassio._base.IOMultiType[bacommon.bs.DisplayItemTypeID]):
203@ioprepped
204@dataclass
205class TokensDisplayItem(DisplayItem):
206    """Some amount of tokens."""
207
208    count: Annotated[int, IOAttrs('c')]
209
210    @override
211    @classmethod
212    def get_type_id(cls) -> DisplayItemTypeID:
213        return DisplayItemTypeID.TOKENS
214
215    @override
216    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
217        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 0x106356e70>]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
210    @override
211    @classmethod
212    def get_type_id(cls) -> DisplayItemTypeID:
213        return DisplayItemTypeID.TOKENS

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
215    @override
216    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
217        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 by the client. This should only be called on the server side when doing said baking.

@ioprepped
@dataclass
class TestDisplayItem(efro.dataclassio._base.IOMultiType[bacommon.bs.DisplayItemTypeID]):
220@ioprepped
221@dataclass
222class TestDisplayItem(DisplayItem):
223    """Fills usable space for a display-item - good for calibration."""
224
225    @override
226    @classmethod
227    def get_type_id(cls) -> DisplayItemTypeID:
228        return DisplayItemTypeID.TEST
229
230    @override
231    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
232        return 'Test Display Item Here', []

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

@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
225    @override
226    @classmethod
227    def get_type_id(cls) -> DisplayItemTypeID:
228        return DisplayItemTypeID.TEST

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
230    @override
231    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
232        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 by the client. This should only be called on the server side when doing said baking.

@ioprepped
@dataclass
class ChestDisplayItem(efro.dataclassio._base.IOMultiType[bacommon.bs.DisplayItemTypeID]):
235@ioprepped
236@dataclass
237class ChestDisplayItem(DisplayItem):
238    """Display a chest."""
239
240    appearance: Annotated[ClassicChestAppearance, IOAttrs('a')]
241
242    @override
243    @classmethod
244    def get_type_id(cls) -> DisplayItemTypeID:
245        return DisplayItemTypeID.CHEST
246
247    @override
248    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
249        return '${TYPE} Chest', [('${TYPE}', self.appearance.name.capitalize())]

Display a chest.

ChestDisplayItem( appearance: Annotated[ClassicChestAppearance, <efro.dataclassio.IOAttrs object>])
appearance: Annotated[ClassicChestAppearance, <efro.dataclassio.IOAttrs object at 0x1063573e0>]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
242    @override
243    @classmethod
244    def get_type_id(cls) -> DisplayItemTypeID:
245        return DisplayItemTypeID.CHEST

Return the type-id for this subclass.

@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
247    @override
248    def get_description(self) -> tuple[str, list[tuple[str, str]]]:
249        return '${TYPE} Chest', [('${TYPE}', self.appearance.name.capitalize())]

Return a string description and subs for the item.

These decriptions are baked into the DisplayItemWrapper and should be accessed from there by the client. This should only be called on the server side when doing said baking.

@ioprepped
@dataclass
class DisplayItemWrapper:
252@ioprepped
253@dataclass
254class DisplayItemWrapper:
255    """Wraps a DisplayItem and common info."""
256
257    item: Annotated[DisplayItem, IOAttrs('i')]
258    description: Annotated[str, IOAttrs('d')]
259    description_subs: Annotated[list[str] | None, IOAttrs('s')]
260
261    @classmethod
262    def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
263        """Convenience method to wrap a DisplayItem."""
264        desc, subs = item.get_description()
265        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 0x1063d54c0>]
description: Annotated[str, <efro.dataclassio.IOAttrs object at 0x1063d6780>]
description_subs: Annotated[list[str] | None, <efro.dataclassio.IOAttrs object at 0x1063d6ae0>]
@classmethod
def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
261    @classmethod
262    def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
263        """Convenience method to wrap a DisplayItem."""
264        desc, subs = item.get_description()
265        return DisplayItemWrapper(item, desc, pairs_to_flat(subs))

Convenience method to wrap a DisplayItem.

@ioprepped
@dataclass
class ChestInfoMessage(efro.message._message.Message):
268@ioprepped
269@dataclass
270class ChestInfoMessage(Message):
271    """Request info about a chest."""
272
273    chest_id: Annotated[str, IOAttrs('i')]
274
275    @override
276    @classmethod
277    def get_response_types(cls) -> list[type[Response] | None]:
278        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 0x1063d65a0>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
275    @override
276    @classmethod
277    def get_response_types(cls) -> list[type[Response] | None]:
278        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):
281@ioprepped
282@dataclass
283class ChestInfoResponse(Response):
284    """Here's that chest info you asked for, boss."""
285
286    @dataclass
287    class Chest:
288        """A lovely chest."""
289
290        @dataclass
291        class PrizeSet:
292            """A possible set of prizes for this chest."""
293
294            weight: Annotated[float, IOAttrs('w')]
295            contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
296
297        appearance: Annotated[
298            ClassicChestAppearance,
299            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
300        ]
301
302        # How much it costs to unlock *now*.
303        unlock_tokens: Annotated[int, IOAttrs('tk')]
304
305        # When it unlocks on its own.
306        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
307
308        # Possible prizes we contain.
309        prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
310
311        # Are ads allowed now?
312        ad_allow: Annotated[bool, IOAttrs('aa')]
313
314    chest: Annotated[Chest | None, IOAttrs('c')]
315    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 0x1063d5b50>]
user_tokens: Annotated[int | None, <efro.dataclassio.IOAttrs object at 0x1063d5940>]
@dataclass
class ChestInfoResponse.Chest:
286    @dataclass
287    class Chest:
288        """A lovely chest."""
289
290        @dataclass
291        class PrizeSet:
292            """A possible set of prizes for this chest."""
293
294            weight: Annotated[float, IOAttrs('w')]
295            contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
296
297        appearance: Annotated[
298            ClassicChestAppearance,
299            IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
300        ]
301
302        # How much it costs to unlock *now*.
303        unlock_tokens: Annotated[int, IOAttrs('tk')]
304
305        # When it unlocks on its own.
306        unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
307
308        # Possible prizes we contain.
309        prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
310
311        # Are ads allowed now?
312        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 0x1063d6c30>]
unlock_tokens: Annotated[int, <efro.dataclassio.IOAttrs object at 0x1063d6f30>]
unlock_time: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x1063d6fc0>]
prizesets: Annotated[list[ChestInfoResponse.Chest.PrizeSet], <efro.dataclassio.IOAttrs object at 0x1063d70e0>]
ad_allow: Annotated[bool, <efro.dataclassio.IOAttrs object at 0x1063d73e0>]
@dataclass
class ChestInfoResponse.Chest.PrizeSet:
290        @dataclass
291        class PrizeSet:
292            """A possible set of prizes for this chest."""
293
294            weight: Annotated[float, IOAttrs('w')]
295            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 0x1064454c0>]
contents: Annotated[list[DisplayItemWrapper], <efro.dataclassio.IOAttrs object at 0x1064466c0>]
@ioprepped
@dataclass
class ChestActionMessage(efro.message._message.Message):
318@ioprepped
319@dataclass
320class ChestActionMessage(Message):
321    """Request action about a chest."""
322
323    class Action(Enum):
324        """Types of actions we can request."""
325
326        # Unlocking (for free or with tokens).
327        UNLOCK = 'u'
328
329        # Watched an ad to reduce wait.
330        AD = 'ad'
331
332    action: Annotated[Action, IOAttrs('a')]
333
334    # Tokens we are paying (only applies to unlock).
335    token_payment: Annotated[int, IOAttrs('t')]
336
337    chest_id: Annotated[str, IOAttrs('i')]
338
339    @override
340    @classmethod
341    def get_response_types(cls) -> list[type[Response] | None]:
342        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 0x106446120>]
token_payment: Annotated[int, <efro.dataclassio.IOAttrs object at 0x1064468d0>]
chest_id: Annotated[str, <efro.dataclassio.IOAttrs object at 0x106447830>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
339    @override
340    @classmethod
341    def get_response_types(cls) -> list[type[Response] | None]:
342        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):
323    class Action(Enum):
324        """Types of actions we can request."""
325
326        # Unlocking (for free or with tokens).
327        UNLOCK = 'u'
328
329        # Watched an ad to reduce wait.
330        AD = 'ad'

Types of actions we can request.

UNLOCK = <Action.UNLOCK: 'u'>
AD = <Action.AD: 'ad'>
@ioprepped
@dataclass
class ChestActionResponse(efro.message._message.Response):
345@ioprepped
346@dataclass
347class ChestActionResponse(Response):
348    """Here's the results of that action you asked for, boss."""
349
350    # Tokens that were actually charged.
351    tokens_charged: Annotated[int, IOAttrs('t')] = 0
352
353    # If present, signifies the chest has been opened and we should show
354    # the user this stuff that was in it.
355    contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None
356
357    # If contents are present, which of the chest's prize-sets they
358    # represent.
359    prizeindex: Annotated[int, IOAttrs('i')] = 0
360
361    # Printable error if something goes wrong.
362    error: Annotated[str | None, IOAttrs('e')] = None
363
364    # Printable warning. Shown in orange with an error sound. Does not
365    # mean the action failed; only that there's something to tell the
366    # users such as 'It looks like you are faking ad views; stop it or
367    # you won't have ad options anymore.'
368    warning: Annotated[str | None, IOAttrs('w')] = None
369
370    # Printable success message. Shown in green with a cash-register
371    # sound. Can be used for things like successful wait reductions via
372    # ad views.
373    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 0x106446ba0>] = 0
contents: Annotated[list[DisplayItemWrapper] | None, <efro.dataclassio.IOAttrs object at 0x106446db0>] = None
prizeindex: Annotated[int, <efro.dataclassio.IOAttrs object at 0x106446f30>] = 0
error: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x106446fc0>] = None
warning: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x106446f90>] = None
success_msg: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x1064471d0>] = None
class ClientUITypeID(enum.Enum):
376class ClientUITypeID(Enum):
377    """Type ID for each of our subclasses."""
378
379    UNKNOWN = 'u'
380    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]):
383class ClientUI(IOMultiType[ClientUITypeID]):
384    """Defines some user interface on the client."""
385
386    @override
387    @classmethod
388    def get_type_id(cls) -> ClientUITypeID:
389        # Require child classes to supply this themselves. If we did a
390        # full type registry/lookup here it would require us to import
391        # everything and would prevent lazy loading.
392        raise NotImplementedError()
393
394    @override
395    @classmethod
396    def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
397        """Return the subclass for each of our type-ids."""
398        # pylint: disable=cyclic-import
399        out: type[ClientUI]
400
401        t = ClientUITypeID
402        if type_id is t.UNKNOWN:
403            out = UnknownClientUI
404        elif type_id is t.BASIC:
405            out = BasicClientUI
406        else:
407            # Important to make sure we provide all types.
408            assert_never(type_id)
409        return out
410
411    @override
412    @classmethod
413    def get_unknown_type_fallback(cls) -> ClientUI:
414        # If we encounter some future message type we don't know
415        # anything about, drop in a placeholder.
416        return UnknownClientUI()

Defines some user interface on the client.

@override
@classmethod
def get_type_id(cls) -> ClientUITypeID:
386    @override
387    @classmethod
388    def get_type_id(cls) -> ClientUITypeID:
389        # Require child classes to supply this themselves. If we did a
390        # full type registry/lookup here it would require us to import
391        # everything and would prevent lazy loading.
392        raise NotImplementedError()

Return the type-id for this subclass.

@override
@classmethod
def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
394    @override
395    @classmethod
396    def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
397        """Return the subclass for each of our type-ids."""
398        # pylint: disable=cyclic-import
399        out: type[ClientUI]
400
401        t = ClientUITypeID
402        if type_id is t.UNKNOWN:
403            out = UnknownClientUI
404        elif type_id is t.BASIC:
405            out = BasicClientUI
406        else:
407            # Important to make sure we provide all types.
408            assert_never(type_id)
409        return out

Return the subclass for each of our type-ids.

@override
@classmethod
def get_unknown_type_fallback(cls) -> ClientUI:
411    @override
412    @classmethod
413    def get_unknown_type_fallback(cls) -> ClientUI:
414        # If we encounter some future message type we don't know
415        # anything about, drop in a placeholder.
416        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]):
419@ioprepped
420@dataclass
421class UnknownClientUI(ClientUI):
422    """Fallback type for unrecognized entries."""
423
424    @override
425    @classmethod
426    def get_type_id(cls) -> ClientUITypeID:
427        return ClientUITypeID.UNKNOWN

Fallback type for unrecognized entries.

@override
@classmethod
def get_type_id(cls) -> ClientUITypeID:
424    @override
425    @classmethod
426    def get_type_id(cls) -> ClientUITypeID:
427        return ClientUITypeID.UNKNOWN

Return the type-id for this subclass.

class BasicClientUIComponentTypeID(enum.Enum):
430class BasicClientUIComponentTypeID(Enum):
431    """Type ID for each of our subclasses."""
432
433    UNKNOWN = 'u'
434    TEXT = 't'
435    LINK = 'l'
436    BS_CLASSIC_TOURNEY_RESULT = 'ct'
437    DISPLAY_ITEMS = 'di'

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]):
440class BasicClientUIComponent(IOMultiType[BasicClientUIComponentTypeID]):
441    """Top level class for our multitype."""
442
443    @override
444    @classmethod
445    def get_type_id(cls) -> BasicClientUIComponentTypeID:
446        # Require child classes to supply this themselves. If we did a
447        # full type registry/lookup here it would require us to import
448        # everything and would prevent lazy loading.
449        raise NotImplementedError()
450
451    @override
452    @classmethod
453    def get_type(
454        cls, type_id: BasicClientUIComponentTypeID
455    ) -> type[BasicClientUIComponent]:
456        """Return the subclass for each of our type-ids."""
457        # pylint: disable=cyclic-import
458
459        t = BasicClientUIComponentTypeID
460        if type_id is t.UNKNOWN:
461            return BasicClientUIComponentUnknown
462        if type_id is t.TEXT:
463            return BasicClientUIComponentText
464        if type_id is t.LINK:
465            return BasicClientUIComponentLink
466        if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
467            return BasicClientUIBsClassicTourneyResult
468        if type_id is t.DISPLAY_ITEMS:
469            return BasicClientUIDisplayItems
470
471        # Important to make sure we provide all types.
472        assert_never(type_id)
473
474    @override
475    @classmethod
476    def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
477        # If we encounter some future message type we don't know
478        # anything about, drop in a placeholder.
479        return BasicClientUIComponentUnknown()

Top level class for our multitype.

@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
443    @override
444    @classmethod
445    def get_type_id(cls) -> BasicClientUIComponentTypeID:
446        # Require child classes to supply this themselves. If we did a
447        # full type registry/lookup here it would require us to import
448        # everything and would prevent lazy loading.
449        raise NotImplementedError()

Return the type-id for this subclass.

@override
@classmethod
def get_type( cls, type_id: BasicClientUIComponentTypeID) -> type[BasicClientUIComponent]:
451    @override
452    @classmethod
453    def get_type(
454        cls, type_id: BasicClientUIComponentTypeID
455    ) -> type[BasicClientUIComponent]:
456        """Return the subclass for each of our type-ids."""
457        # pylint: disable=cyclic-import
458
459        t = BasicClientUIComponentTypeID
460        if type_id is t.UNKNOWN:
461            return BasicClientUIComponentUnknown
462        if type_id is t.TEXT:
463            return BasicClientUIComponentText
464        if type_id is t.LINK:
465            return BasicClientUIComponentLink
466        if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
467            return BasicClientUIBsClassicTourneyResult
468        if type_id is t.DISPLAY_ITEMS:
469            return BasicClientUIDisplayItems
470
471        # Important to make sure we provide all types.
472        assert_never(type_id)

Return the subclass for each of our type-ids.

@override
@classmethod
def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
474    @override
475    @classmethod
476    def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
477        # If we encounter some future message type we don't know
478        # anything about, drop in a placeholder.
479        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]):
482@ioprepped
483@dataclass
484class BasicClientUIComponentUnknown(BasicClientUIComponent):
485    """An unknown basic client component type.
486
487    In practice these should never show up since the master-server
488    generates these on the fly for the client and so should not send
489    clients one they can't digest.
490    """
491
492    @override
493    @classmethod
494    def get_type_id(cls) -> BasicClientUIComponentTypeID:
495        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:
492    @override
493    @classmethod
494    def get_type_id(cls) -> BasicClientUIComponentTypeID:
495        return BasicClientUIComponentTypeID.UNKNOWN

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIComponentText(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
498@ioprepped
499@dataclass
500class BasicClientUIComponentText(BasicClientUIComponent):
501    """Show some text in the inbox message."""
502
503    text: Annotated[str, IOAttrs('t')]
504    subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
505        default_factory=list
506    )
507    scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0
508    color: Annotated[
509        tuple[float, float, float, float], IOAttrs('c', store_default=False)
510    ] = (1.0, 1.0, 1.0, 1.0)
511    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
512    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
513
514    @override
515    @classmethod
516    def get_type_id(cls) -> BasicClientUIComponentTypeID:
517        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 0x106656690>]
subs: Annotated[list[str], <efro.dataclassio.IOAttrs object at 0x106654b30>]
scale: Annotated[float, <efro.dataclassio.IOAttrs object at 0x106654d10>] = 1.0
color: Annotated[tuple[float, float, float, float], <efro.dataclassio.IOAttrs object at 0x106654e90>] = (1.0, 1.0, 1.0, 1.0)
spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object at 0x106654f20>] = 0.0
spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object at 0x106655010>] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
514    @override
515    @classmethod
516    def get_type_id(cls) -> BasicClientUIComponentTypeID:
517        return BasicClientUIComponentTypeID.TEXT

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIBsClassicTourneyResult(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
539@ioprepped
540@dataclass
541class BasicClientUIBsClassicTourneyResult(BasicClientUIComponent):
542    """Show info about a classic tourney."""
543
544    tournament_id: Annotated[str, IOAttrs('t')]
545    game: Annotated[str, IOAttrs('g')]
546    players: Annotated[int, IOAttrs('p')]
547    rank: Annotated[int, IOAttrs('r')]
548    trophy: Annotated[str | None, IOAttrs('tr')]
549    prizes: Annotated[list[DisplayItemWrapper], IOAttrs('pr')]
550
551    @override
552    @classmethod
553    def get_type_id(cls) -> BasicClientUIComponentTypeID:
554        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 0x106629010>]
game: Annotated[str, <efro.dataclassio.IOAttrs object at 0x10662a5a0>]
players: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10662aed0>]
rank: Annotated[int, <efro.dataclassio.IOAttrs object at 0x10662b0e0>]
trophy: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x10662ae70>]
prizes: Annotated[list[DisplayItemWrapper], <efro.dataclassio.IOAttrs object at 0x1066289e0>]
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
551    @override
552    @classmethod
553    def get_type_id(cls) -> BasicClientUIComponentTypeID:
554        return BasicClientUIComponentTypeID.BS_CLASSIC_TOURNEY_RESULT

Return the type-id for this subclass.

@ioprepped
@dataclass
class BasicClientUIDisplayItems(efro.dataclassio._base.IOMultiType[bacommon.bs.BasicClientUIComponentTypeID]):
557@ioprepped
558@dataclass
559class BasicClientUIDisplayItems(BasicClientUIComponent):
560    """Show some display-items."""
561
562    items: Annotated[list[DisplayItemWrapper], IOAttrs('d')]
563    width: Annotated[float, IOAttrs('w')] = 100.0
564    spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
565    spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
566
567    @override
568    @classmethod
569    def get_type_id(cls) -> BasicClientUIComponentTypeID:
570        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 0x10662aba0>]
width: Annotated[float, <efro.dataclassio.IOAttrs object at 0x1066295e0>] = 100.0
spacing_top: Annotated[float, <efro.dataclassio.IOAttrs object at 0x106629760>] = 0.0
spacing_bottom: Annotated[float, <efro.dataclassio.IOAttrs object at 0x106629790>] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicClientUIComponentTypeID:
567    @override
568    @classmethod
569    def get_type_id(cls) -> BasicClientUIComponentTypeID:
570        return BasicClientUIComponentTypeID.DISPLAY_ITEMS

Return the type-id for this subclass.

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

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 0x10558f950>]
interaction_style: Annotated[BasicClientUI.InteractionStyle, <efro.dataclassio.IOAttrs object at 0x10558fcb0>] = <InteractionStyle.BUTTON_POSITIVE: 'p'>
button_label_positive: Annotated[BasicClientUI.ButtonLabel, <efro.dataclassio.IOAttrs object at 0x10558d520>] = <ButtonLabel.OK: 'o'>
button_label_negative: Annotated[BasicClientUI.ButtonLabel, <efro.dataclassio.IOAttrs object at 0x10558fa70>] = <ButtonLabel.CANCEL: 'c'>
@override
@classmethod
def get_type_id(cls) -> ClientUITypeID:
612    @override
613    @classmethod
614    def get_type_id(cls) -> ClientUITypeID:
615        return ClientUITypeID.BASIC

Return the type-id for this subclass.

def contains_unknown_elements(self) -> bool:
617    def contains_unknown_elements(self) -> bool:
618        """Whether something within us is an unknown type or enum."""
619        return (
620            self.interaction_style is self.InteractionStyle.UNKNOWN
621            or self.button_label_positive is self.ButtonLabel.UNKNOWN
622            or self.button_label_negative is self.ButtonLabel.UNKNOWN
623            or any(
624                c.get_type_id() is BasicClientUIComponentTypeID.UNKNOWN
625                for c in self.components
626            )
627        )

Whether something within us is an unknown type or enum.

class BasicClientUI.ButtonLabel(enum.Enum):
578    class ButtonLabel(Enum):
579        """Distinct button labels we support."""
580
581        UNKNOWN = 'u'
582        OK = 'o'
583        APPLY = 'a'
584        CANCEL = 'c'
585        ACCEPT = 'ac'
586        DECLINE = 'dn'
587        IGNORE = 'ig'
588        CLAIM = 'cl'
589        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):
591    class InteractionStyle(Enum):
592        """Overall interaction styles we support."""
593
594        UNKNOWN = 'u'
595        BUTTON_POSITIVE = 'p'
596        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:
630@ioprepped
631@dataclass
632class ClientUIWrapper:
633    """Wrapper for a ClientUI and its common data."""
634
635    id: Annotated[str, IOAttrs('i')]
636    createtime: Annotated[datetime.datetime, IOAttrs('c')]
637    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 0x1066f9fd0>]
createtime: Annotated[datetime.datetime, <efro.dataclassio.IOAttrs object at 0x1066fb440>]
ui: Annotated[ClientUI, <efro.dataclassio.IOAttrs object at 0x1066f8a40>]
@ioprepped
@dataclass
class InboxRequestMessage(efro.message._message.Message):
640@ioprepped
641@dataclass
642class InboxRequestMessage(Message):
643    """Message requesting our inbox."""
644
645    @override
646    @classmethod
647    def get_response_types(cls) -> list[type[Response] | None]:
648        return [InboxRequestResponse]

Message requesting our inbox.

@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
645    @override
646    @classmethod
647    def get_response_types(cls) -> list[type[Response] | None]:
648        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):
651@ioprepped
652@dataclass
653class InboxRequestResponse(Response):
654    """Here's that inbox contents you asked for, boss."""
655
656    wrappers: Annotated[list[ClientUIWrapper], IOAttrs('w')]
657
658    # Printable error if something goes wrong.
659    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 0x1066fb950>]
error: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x1066fa420>] = None
class ClientUIAction(enum.Enum):
662class ClientUIAction(Enum):
663    """Types of actions we can run."""
664
665    BUTTON_PRESS_POSITIVE = 'p'
666    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):
669class ClientEffectTypeID(Enum):
670    """Type ID for each of our subclasses."""
671
672    UNKNOWN = 'u'
673    SCREEN_MESSAGE = 'm'
674    SOUND = 's'
675    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]):
678class ClientEffect(IOMultiType[ClientEffectTypeID]):
679    """Something that can happen on the client.
680
681    This can include screen messages, sounds, visual effects, etc.
682    """
683
684    @override
685    @classmethod
686    def get_type_id(cls) -> ClientEffectTypeID:
687        # Require child classes to supply this themselves. If we did a
688        # full type registry/lookup here it would require us to import
689        # everything and would prevent lazy loading.
690        raise NotImplementedError()
691
692    @override
693    @classmethod
694    def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
695        """Return the subclass for each of our type-ids."""
696        # pylint: disable=cyclic-import
697
698        t = ClientEffectTypeID
699        if type_id is t.UNKNOWN:
700            return ClientEffectUnknown
701        if type_id is t.SCREEN_MESSAGE:
702            return ClientEffectScreenMessage
703        if type_id is t.SOUND:
704            return ClientEffectSound
705        if type_id is t.DELAY:
706            return ClientEffectDelay
707
708        # Important to make sure we provide all types.
709        assert_never(type_id)
710
711    @override
712    @classmethod
713    def get_unknown_type_fallback(cls) -> ClientEffect:
714        # If we encounter some future message type we don't know
715        # anything about, drop in a placeholder.
716        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:
684    @override
685    @classmethod
686    def get_type_id(cls) -> ClientEffectTypeID:
687        # Require child classes to supply this themselves. If we did a
688        # full type registry/lookup here it would require us to import
689        # everything and would prevent lazy loading.
690        raise NotImplementedError()

Return the type-id for this subclass.

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

Return the subclass for each of our type-ids.

@override
@classmethod
def get_unknown_type_fallback(cls) -> ClientEffect:
711    @override
712    @classmethod
713    def get_unknown_type_fallback(cls) -> ClientEffect:
714        # If we encounter some future message type we don't know
715        # anything about, drop in a placeholder.
716        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]):
719@ioprepped
720@dataclass
721class ClientEffectUnknown(ClientEffect):
722    """Fallback substitute for types we don't recognize."""
723
724    @override
725    @classmethod
726    def get_type_id(cls) -> ClientEffectTypeID:
727        return ClientEffectTypeID.UNKNOWN

Fallback substitute for types we don't recognize.

@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
724    @override
725    @classmethod
726    def get_type_id(cls) -> ClientEffectTypeID:
727        return ClientEffectTypeID.UNKNOWN

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientEffectScreenMessage(efro.dataclassio._base.IOMultiType[bacommon.bs.ClientEffectTypeID]):
730@ioprepped
731@dataclass
732class ClientEffectScreenMessage(ClientEffect):
733    """Display a screen-message."""
734
735    message: Annotated[str, IOAttrs('m')]
736    subs: Annotated[list[str], IOAttrs('s')]
737    color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0)
738
739    @override
740    @classmethod
741    def get_type_id(cls) -> ClientEffectTypeID:
742        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 0x10660fb00>]
subs: Annotated[list[str], <efro.dataclassio.IOAttrs object at 0x10660e360>]
color: Annotated[tuple[float, float, float], <efro.dataclassio.IOAttrs object at 0x10660e540>] = (1.0, 1.0, 1.0)
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
739    @override
740    @classmethod
741    def get_type_id(cls) -> ClientEffectTypeID:
742        return ClientEffectTypeID.SCREEN_MESSAGE

Return the type-id for this subclass.

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

Return the type-id for this subclass.

class ClientEffectSound.Sound(enum.Enum):
750    class Sound(Enum):
751        """Sounds that can be made alongside the message."""
752
753        UNKNOWN = 'u'
754        CASH_REGISTER = 'c'
755        ERROR = 'e'
756        POWER_DOWN = 'p'
757        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]):
768@ioprepped
769@dataclass
770class ClientEffectDelay(ClientEffect):
771    """Delay effect processing."""
772
773    seconds: Annotated[float, IOAttrs('s')]
774
775    @override
776    @classmethod
777    def get_type_id(cls) -> ClientEffectTypeID:
778        return ClientEffectTypeID.DELAY

Delay effect processing.

ClientEffectDelay(seconds: Annotated[float, <efro.dataclassio.IOAttrs object>])
seconds: Annotated[float, <efro.dataclassio.IOAttrs object at 0x106746a20>]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
775    @override
776    @classmethod
777    def get_type_id(cls) -> ClientEffectTypeID:
778        return ClientEffectTypeID.DELAY

Return the type-id for this subclass.

@ioprepped
@dataclass
class ClientUIActionMessage(efro.message._message.Message):
781@ioprepped
782@dataclass
783class ClientUIActionMessage(Message):
784    """Do something to a client ui."""
785
786    id: Annotated[str, IOAttrs('i')]
787    action: Annotated[ClientUIAction, IOAttrs('a')]
788
789    @override
790    @classmethod
791    def get_response_types(cls) -> list[type[Response] | None]:
792        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 0x1067477a0>]
action: Annotated[ClientUIAction, <efro.dataclassio.IOAttrs object at 0x106746300>]
@override
@classmethod
def get_response_types(cls) -> list[type[efro.message.Response] | None]:
789    @override
790    @classmethod
791    def get_response_types(cls) -> list[type[Response] | None]:
792        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):
795@ioprepped
796@dataclass
797class ClientUIActionResponse(Response):
798    """Did something to that inbox entry, boss."""
799
800    class ErrorType(Enum):
801        """Types of errors that may have occurred."""
802
803        # Probably a future error type we don't recognize.
804        UNKNOWN = 'u'
805
806        # Something went wrong on the server, but specifics are not
807        # relevant.
808        INTERNAL = 'i'
809
810        # The entry expired on the server. In various cases such as 'ok'
811        # buttons this can generally be ignored.
812        EXPIRED = 'e'
813
814    error_type: Annotated[
815        ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN)
816    ]
817
818    # User facing error message in the case of errors.
819    error_message: Annotated[str | None, IOAttrs('em')]
820
821    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 0x1067472f0>]
error_message: Annotated[str | None, <efro.dataclassio.IOAttrs object at 0x106747410>]
effects: Annotated[list[ClientEffect], <efro.dataclassio.IOAttrs object at 0x106747470>]
class ClientUIActionResponse.ErrorType(enum.Enum):
800    class ErrorType(Enum):
801        """Types of errors that may have occurred."""
802
803        # Probably a future error type we don't recognize.
804        UNKNOWN = 'u'
805
806        # Something went wrong on the server, but specifics are not
807        # relevant.
808        INTERNAL = 'i'
809
810        # The entry expired on the server. In various cases such as 'ok'
811        # buttons this can generally be ignored.
812        EXPIRED = 'e'

Types of errors that may have occurred.

UNKNOWN = <ErrorType.UNKNOWN: 'u'>
INTERNAL = <ErrorType.INTERNAL: 'i'>
EXPIRED = <ErrorType.EXPIRED: 'e'>