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')]
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
196 @override 197 @classmethod 198 def get_type_id(cls) -> DisplayItemTypeID: 199 return DisplayItemTypeID.UNKNOWN
Return the type-id for this subclass.
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.
Inherited Members
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.
220 @override 221 @classmethod 222 def get_type_id(cls) -> DisplayItemTypeID: 223 return DisplayItemTypeID.TICKETS
Return the type-id for this subclass.
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.
Inherited Members
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.
237 @override 238 @classmethod 239 def get_type_id(cls) -> DisplayItemTypeID: 240 return DisplayItemTypeID.TOKENS
Return the type-id for this subclass.
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.
Inherited Members
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.
252 @override 253 @classmethod 254 def get_type_id(cls) -> DisplayItemTypeID: 255 return DisplayItemTypeID.TEST
Return the type-id for this subclass.
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.
Inherited Members
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.
269 @override 270 @classmethod 271 def get_type_id(cls) -> DisplayItemTypeID: 272 return DisplayItemTypeID.CHEST
Return the type-id for this subclass.
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.
Inherited Members
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
451 @override 452 @classmethod 453 def get_type_id(cls) -> ClientUITypeID: 454 return ClientUITypeID.UNKNOWN
Return the type-id for this subclass.
Inherited Members
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.
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.
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.
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.
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.
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.
522 @override 523 @classmethod 524 def get_type_id(cls) -> BasicClientUIComponentTypeID: 525 return BasicClientUIComponentTypeID.UNKNOWN
Return the type-id for this subclass.
Inherited Members
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.
544 @override 545 @classmethod 546 def get_type_id(cls) -> BasicClientUIComponentTypeID: 547 return BasicClientUIComponentTypeID.TEXT
Return the type-id for this subclass.
Inherited Members
550@ioprepped 551@dataclass 552class BasicClientUIComponentLink(BasicClientUIComponent): 553 """Show a link in the inbox message.""" 554 555 url: Annotated[str, IOAttrs('u')] 556 label: Annotated[str, IOAttrs('l')] 557 subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field( 558 default_factory=list 559 ) 560 spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 561 spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 562 563 @override 564 @classmethod 565 def get_type_id(cls) -> BasicClientUIComponentTypeID: 566 return BasicClientUIComponentTypeID.LINK
Show a link in the inbox message.
563 @override 564 @classmethod 565 def get_type_id(cls) -> BasicClientUIComponentTypeID: 566 return BasicClientUIComponentTypeID.LINK
Return the type-id for this subclass.
Inherited Members
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.
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.
Inherited Members
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.
597 @override 598 @classmethod 599 def get_type_id(cls) -> BasicClientUIComponentTypeID: 600 return BasicClientUIComponentTypeID.DISPLAY_ITEMS
Return the type-id for this subclass.
Inherited Members
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.
612 @override 613 @classmethod 614 def get_type_id(cls) -> BasicClientUIComponentTypeID: 615 return BasicClientUIComponentTypeID.EXPIRE_TIME
Return the type-id for this subclass.
Inherited Members
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.
657 @override 658 @classmethod 659 def get_type_id(cls) -> ClientUITypeID: 660 return ClientUITypeID.BASIC
Return the type-id for this subclass.
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.
Inherited Members
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
769 @override 770 @classmethod 771 def get_type_id(cls) -> ClientEffectTypeID: 772 return ClientEffectTypeID.UNKNOWN
Return the type-id for this subclass.
Inherited Members
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.
784 @override 785 @classmethod 786 def get_type_id(cls) -> ClientEffectTypeID: 787 return ClientEffectTypeID.SCREEN_MESSAGE
Return the type-id for this subclass.
Inherited Members
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.
807 @override 808 @classmethod 809 def get_type_id(cls) -> ClientEffectTypeID: 810 return ClientEffectTypeID.SOUND
Return the type-id for this subclass.
Inherited Members
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.
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.
820 @override 821 @classmethod 822 def get_type_id(cls) -> ClientEffectTypeID: 823 return ClientEffectTypeID.DELAY
Return the type-id for this subclass.
Inherited Members
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.
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.
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.
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.
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.