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')]
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'
Appearances bombsquad classic chests can have.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
169 @override 170 @classmethod 171 def get_type_id(cls) -> DisplayItemTypeID: 172 return DisplayItemTypeID.UNKNOWN
Return the type-id for this subclass.
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.
Inherited Members
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.
193 @override 194 @classmethod 195 def get_type_id(cls) -> DisplayItemTypeID: 196 return DisplayItemTypeID.TICKETS
Return the type-id for this subclass.
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.
Inherited Members
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.
210 @override 211 @classmethod 212 def get_type_id(cls) -> DisplayItemTypeID: 213 return DisplayItemTypeID.TOKENS
Return the type-id for this subclass.
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.
Inherited Members
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.
225 @override 226 @classmethod 227 def get_type_id(cls) -> DisplayItemTypeID: 228 return DisplayItemTypeID.TEST
Return the type-id for this subclass.
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.
Inherited Members
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.
242 @override 243 @classmethod 244 def get_type_id(cls) -> DisplayItemTypeID: 245 return DisplayItemTypeID.CHEST
Return the type-id for this subclass.
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.
Inherited Members
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
424 @override 425 @classmethod 426 def get_type_id(cls) -> ClientUITypeID: 427 return ClientUITypeID.UNKNOWN
Return the type-id for this subclass.
Inherited Members
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.
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.
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.
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.
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.
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.
492 @override 493 @classmethod 494 def get_type_id(cls) -> BasicClientUIComponentTypeID: 495 return BasicClientUIComponentTypeID.UNKNOWN
Return the type-id for this subclass.
Inherited Members
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.
514 @override 515 @classmethod 516 def get_type_id(cls) -> BasicClientUIComponentTypeID: 517 return BasicClientUIComponentTypeID.TEXT
Return the type-id for this subclass.
Inherited Members
520@ioprepped 521@dataclass 522class BasicClientUIComponentLink(BasicClientUIComponent): 523 """Show a link in the inbox message.""" 524 525 url: Annotated[str, IOAttrs('u')] 526 label: Annotated[str, IOAttrs('l')] 527 subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field( 528 default_factory=list 529 ) 530 spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 531 spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 532 533 @override 534 @classmethod 535 def get_type_id(cls) -> BasicClientUIComponentTypeID: 536 return BasicClientUIComponentTypeID.LINK
Show a link in the inbox message.
533 @override 534 @classmethod 535 def get_type_id(cls) -> BasicClientUIComponentTypeID: 536 return BasicClientUIComponentTypeID.LINK
Return the type-id for this subclass.
Inherited Members
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.
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.
Inherited Members
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.
567 @override 568 @classmethod 569 def get_type_id(cls) -> BasicClientUIComponentTypeID: 570 return BasicClientUIComponentTypeID.DISPLAY_ITEMS
Return the type-id for this subclass.
Inherited Members
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.
612 @override 613 @classmethod 614 def get_type_id(cls) -> ClientUITypeID: 615 return ClientUITypeID.BASIC
Return the type-id for this subclass.
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.
Inherited Members
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
724 @override 725 @classmethod 726 def get_type_id(cls) -> ClientEffectTypeID: 727 return ClientEffectTypeID.UNKNOWN
Return the type-id for this subclass.
Inherited Members
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.
739 @override 740 @classmethod 741 def get_type_id(cls) -> ClientEffectTypeID: 742 return ClientEffectTypeID.SCREEN_MESSAGE
Return the type-id for this subclass.
Inherited Members
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.
762 @override 763 @classmethod 764 def get_type_id(cls) -> ClientEffectTypeID: 765 return ClientEffectTypeID.SOUND
Return the type-id for this subclass.
Inherited Members
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.
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.
775 @override 776 @classmethod 777 def get_type_id(cls) -> ClientEffectTypeID: 778 return ClientEffectTypeID.DELAY
Return the type-id for this subclass.
Inherited Members
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.
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.
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.