diff --git a/CHANGELOG.md b/CHANGELOG.md index 779927a..ed4f1f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ ## Plugin Manager (dd-mm-yyyy) +### 0.1.7 (03-10-2022) + +- Added New Option in settings for Notifying new plugins. +- Added a Discord Button to join Bombsquad's Official Discord server. + + ### 0.1.6 (15-09-2022) - Distinguish the settings button with a cyan color (previously was green) in plugin manager window. diff --git a/index.json b/index.json index 363be82..e4e02ca 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,12 @@ { "plugin_manager_url": "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { + "0.1.7": { + "api_version": 7, + "commit_sha": "2fe966c", + "released_on": "04-10-2022", + "md5sum": "78d7a6b3a62f8920d0da0acab20b419a" + }, "0.1.6": { "api_version": 7, "commit_sha": "2a7ad8e", diff --git a/plugin_manager.py b/plugin_manager.py index 399482c..e1db5b8 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -20,7 +20,7 @@ _env = _ba.env() _uiscale = ba.app.ui.uiscale -PLUGIN_MANAGER_VERSION = "0.1.6" +PLUGIN_MANAGER_VERSION = "0.1.7" REPOSITORY_URL = "http://github.com/bombsquad-community/plugin-manager" CURRENT_TAG = "main" # XXX: Using https with `ba.open_url` seems to trigger a pop-up dialog box on @@ -36,6 +36,7 @@ REGEXP = { "plugin_entry_points": re.compile(b"(ba_meta export plugin\n+class )(.*)\\("), "minigames": re.compile(b"(ba_meta export game\n+class )(.*)\\("), } +DISCORD_URL = "http://ballistica.net/discord" _CACHE = {} @@ -124,7 +125,6 @@ class StartupTasks: plugin_manager_config = ba.app.config.setdefault("Community Plugin Manager", {}) plugin_manager_config.setdefault("Custom Sources", []) installed_plugins = plugin_manager_config.setdefault("Installed Plugins", {}) - for plugin_name in tuple(installed_plugins.keys()): plugin = PluginLocal(plugin_name) if not plugin.is_installed: @@ -135,6 +135,7 @@ class StartupTasks: "Auto Update Plugin Manager": True, "Auto Update Plugins": True, "Auto Enable Plugins After Installation": True, + "Notify New Plugins": True } settings = plugin_manager_config.setdefault("Settings", {}) @@ -174,12 +175,33 @@ class StartupTasks: plugins_to_update.append(plugin.update()) await asyncio.gather(*plugins_to_update) + async def notify_new_plugins(self): + if not ba.app.config["Community Plugin Manager"]["Settings"]["Notify New Plugins"]: + return + + await self.plugin_manager.setup_index() + try: + num_of_plugins = ba.app.config["Community Plugin Manager"]["Existing Number of Plugins"] + except Exception: + ba.app.config["Community Plugin Manager"]["Existing Number of Plugins"] = len(await self.plugin_manager.categories["All"].get_plugins()) + num_of_plugins = ba.app.config["Community Plugin Manager"]["Existing Number of Plugins"] + ba.app.config.commit() + return + + new_num_of_plugins = len(await self.plugin_manager.categories["All"].get_plugins()) + + if num_of_plugins < new_num_of_plugins: + ba.screenmessage("We got new Plugins for you to try!") + ba.app.config["Community Plugin Manager"]["Existing Number of Plugins"] = new_num_of_plugins + ba.app.config.commit() + async def execute(self): self.setup_config() try: await asyncio.gather( self.update_plugin_manager(), self.update_plugins(), + self.notify_new_plugins(), ) except urllib.error.URLError: pass @@ -898,6 +920,7 @@ class PluginManager: self._index = _CACHE.get("index", {}) self.categories = {} self.module_path = sys.modules[__name__].__file__ + self._index_setup_in_progress = False async def get_index(self): if not self._index: @@ -910,13 +933,20 @@ class PluginManager: headers=self.request_headers, ) response = await async_send_network_request(request) - self._index = json.loads(response.read()) - self.set_index_global_cache(self._index) + index = json.loads(response.read()) + self.set_index_global_cache(index) + self._index = index return self._index async def setup_index(self): + while self._index_setup_in_progress: + # Avoid making multiple network calls to the same resource in parallel. + # Rather wait for the previous network call to complete. + await asyncio.sleep(0.1) + self._index_setup_in_progress = not bool(self._index) index = await self.get_index() await self.setup_plugin_categories(index) + self._index_setup_in_progress = False async def setup_plugin_categories(self, plugin_index): # A hack to have the "All" category show at the top. @@ -1678,7 +1708,7 @@ class PluginManagerSettingsWindow(popup.PopupWindow): scale=text_scale * 0.8) pos -= 34 * text_scale - pos = height - 220 + pos = height - 200 ba.textwidget(parent=self._root_widget, position=(width * 0.49, pos-5), size=(0, 0), @@ -1689,17 +1719,16 @@ class PluginManagerSettingsWindow(popup.PopupWindow): color=color, maxwidth=width * 0.95) - pos -= 45 - ba.textwidget(parent=self._root_widget, - position=(width * 0.22, pos-5), - size=(0, 0), - h_align='center', - v_align='center', - text=f'API Version: {ba.app.api_version}', - scale=text_scale * 0.7, - color=(0.4, 0.8, 1), - maxwidth=width * 0.95) - pos -= 25 + pos -= 75 + ba.buttonwidget(parent=self._root_widget, + position=((width * 0.20) - button_size[0] / 2, pos), + size=button_size, + on_activate_call=lambda: ba.open_url(DISCORD_URL), + textcolor=b_text_color, + button_type='square', + text_scale=1, + label='Discord') + ba.buttonwidget(parent=self._root_widget, position=((width * 0.49) - button_size[0] / 2, pos), size=button_size, @@ -1756,6 +1785,16 @@ class PluginManagerSettingsWindow(popup.PopupWindow): scale=text_scale * 0.8, color=text_color, maxwidth=width * 0.9) + pos -= 25 + ba.textwidget(parent=self._root_widget, + position=(width * 0.49, pos), + size=(0, 0), + h_align='center', + v_align='center', + text=f'API Version: {ba.app.api_version}', + scale=text_scale * 0.7, + color=(0.4, 0.8, 1), + maxwidth=width * 0.95) pos = height * 0.1 diff --git a/plugins/utilities.json b/plugins/utilities.json index b8249c6..7badf8d 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -4,7 +4,7 @@ "plugins_base_url": "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/utilities", "plugins": { "mood_light": { - "description": "Dynamic lighting in co-op games (adjustable using \"cm\" chat command)", + "description": "Dynamic lighting in co-op games (adjustable using \"ml\" chat command)", "external_url": "", "authors": [ { @@ -133,7 +133,7 @@ "authors": [ { "name": "Mr.Smoothy", - "email": "", + "email": "smoothy@bombsquad.ga", "discord": "mr.smoothy#5824" } ], @@ -158,7 +158,7 @@ "authors": [ { "name": "Mr.Smoothy", - "email": "smoothyt@bombsquad.ga", + "email": "smoothy@bombsquad.ga", "discord": "mr.smoothy#5824" } ], @@ -177,7 +177,7 @@ "authors": [ { "name": "Mr.Smoothy", - "email": "smoothyt@bombsquad.ga", + "email": "smoothy@bombsquad.ga", "discord": "mr.smoothy#5824" } ], @@ -190,6 +190,44 @@ } } }, + "character_chooser": { + "description": "Let you choose your character before joining game.", + "external_url": "https://www.youtube.com/watch?v=hNmv2l-NahE", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "smoothy@bombsquad.ga", + "discord": "mr.smoothy#5824" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "ff4de19", + "released_on": "05-10-2022", + "md5sum": "38d47297d4048a2fe1022ea841c76f91" + } + } + }, + "character_maker": { + "description": "Make new characters by manipulating models and textures.", + "external_url": "https://www.youtube.com/watch?v=q0KxY1hfMPQ", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "smoothy@bombsquad.ga", + "discord": "mr.smoothy#5824" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "ff4de19", + "released_on": "05-10-2022", + "md5sum": "f734fa33994c7cfafc637b7e3dca60ce" + } + } + }, "icons_keyboard": { "description": "Enable 'Always Use Internal Keyboard' in Settings>Advanced. Double tap space-bar to change keyboards", "external_url": "", @@ -227,6 +265,25 @@ "md5sum": "bbaee5f133b41d2eb53e3b726403a75e" } } + }, + "ultra_party_window": { + "description": "Ultra your party window with lots of features", + "external_url": "", + "authors": [ + { + "name": "Droopy", + "email": "", + "discord": "Droopy#3730" + } + ], + "versions": { + "4.0.0": { + "api_version": 7, + "commit_sha": "a23e8cd", + "released_on": "04-10-2022", + "md5sum": "7da7fae6ddf2560789bbef56f4ff5bd6" + } + } } } } \ No newline at end of file diff --git a/plugins/utilities/character_chooser.py b/plugins/utilities/character_chooser.py new file mode 100644 index 0000000..006c0a4 --- /dev/null +++ b/plugins/utilities/character_chooser.py @@ -0,0 +1,360 @@ +# ba_meta require api 7 + +''' +Character Chooser by Mr.Smoothy + +This plugin will let you choose your character from lobby. + +Install this plugin on your Phone/PC or on Server + +If installed on server :- this will also let players choose server specific custom characters . so no more sharing of character file with all players, +just install this plugin on server ...and players can pick character from lobby . + +Use:- +> select your profile (focus on color and name) +> press ready (punch) +> now use UP/DOWN buttons to scroll character list +> Press ready again (punch) to join the game +> or press Bomb button to go back to profile choosing menu +> END + +Watch : https://www.youtube.com/watch?v=hNmv2l-NahE +Join : https://discord.gg/ucyaesh +Contact : discord mr.smoothy#5824 + + +Share this plugin with your server owner /admins to use it online + + :) + +''' + + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +from bastd.actor.playerspaz import PlayerSpaz + + +from ba._error import print_exception, print_error, NotFoundError +from ba._gameutils import animate, animate_array +from ba._language import Lstr +from ba._generated.enums import SpecialChar, InputType +from ba._profile import get_player_profile_colors +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional +import weakref +import os +import json +from ba import _lobby +from bastd.actor.spazappearance import * +from ba._lobby import ChangeMessage +from ba._lobby import PlayerReadyMessage + + +def __init__(self, vpos: float, sessionplayer: _ba.SessionPlayer, + lobby: 'Lobby') -> None: + self._deek_sound = _ba.getsound('deek') + self._click_sound = _ba.getsound('click01') + self._punchsound = _ba.getsound('punch01') + self._swish_sound = _ba.getsound('punchSwish') + self._errorsound = _ba.getsound('error') + self._mask_texture = _ba.gettexture('characterIconMask') + self._vpos = vpos + self._lobby = weakref.ref(lobby) + self._sessionplayer = sessionplayer + self._inited = False + self._dead = False + self._text_node: Optional[ba.Node] = None + self._profilename = '' + self._profilenames: List[str] = [] + self._ready: bool = False + self._character_names: List[str] = [] + self._last_change: Sequence[Union[float, int]] = (0, 0) + self._profiles: Dict[str, Dict[str, Any]] = {} + + app = _ba.app + + self.bakwas_chars = ["Lee", "Todd McBurton", "Zola", "Butch", "Witch", "warrior", + "Middle-Man", "Alien", "OldLady", "Gladiator", "Wrestler", "Gretel", "Robot"] + + # Load available player profiles either from the local config or + # from the remote device. + self.reload_profiles() + for name in _ba.app.spaz_appearances: + if name not in self._character_names and name not in self.bakwas_chars: + self._character_names.append(name) + # Note: this is just our local index out of available teams; *not* + # the team-id! + self._selected_team_index: int = self.lobby.next_add_team + + # Store a persistent random character index and colors; we'll use this + # for the '_random' profile. Let's use their input_device id to seed + # it. This will give a persistent character for them between games + # and will distribute characters nicely if everyone is random. + self._random_color, self._random_highlight = ( + get_player_profile_colors(None)) + + # To calc our random character we pick a random one out of our + # unlocked list and then locate that character's index in the full + # list. + char_index_offset = app.lobby_random_char_index_offset + self._random_character_index = ( + (sessionplayer.inputdevice.id + char_index_offset) % + len(self._character_names)) + + # Attempt to set an initial profile based on what was used previously + # for this input-device, etc. + self._profileindex = self._select_initial_profile() + self._profilename = self._profilenames[self._profileindex] + + self._text_node = _ba.newnode('text', + delegate=self, + attrs={ + 'position': (-100, self._vpos), + 'maxwidth': 190, + 'shadow': 0.5, + 'vr_depth': -20, + 'h_align': 'left', + 'v_align': 'center', + 'v_attach': 'top' + }) + animate(self._text_node, 'scale', {0: 0, 0.1: 1.0}) + self.icon = _ba.newnode('image', + owner=self._text_node, + attrs={ + 'position': (-130, self._vpos + 20), + 'mask_texture': self._mask_texture, + 'vr_depth': -10, + 'attach': 'topCenter' + }) + + animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)}) + + # Set our initial name to '' in case anyone asks. + self._sessionplayer.setname( + Lstr(resource='choosingPlayerText').evaluate(), real=False) + + # Init these to our rando but they should get switched to the + # selected profile (if any) right after. + self._character_index = self._random_character_index + self._color = self._random_color + self._highlight = self._random_highlight + self.characterchooser = False + self.update_from_profile() + self.update_position() + self._inited = True + + self._set_ready(False) + + +def _set_ready(self, ready: bool) -> None: + + # pylint: disable=cyclic-import + from bastd.ui.profile import browser as pbrowser + from ba._general import Call + profilename = self._profilenames[self._profileindex] + + # Handle '_edit' as a special case. + if profilename == '_edit' and ready: + with _ba.Context('ui'): + pbrowser.ProfileBrowserWindow(in_main_menu=False) + + # Give their input-device UI ownership too + # (prevent someone else from snatching it in crowded games) + _ba.set_ui_input_device(self._sessionplayer.inputdevice) + return + + if ready == False: + self._sessionplayer.assigninput( + InputType.LEFT_PRESS, + Call(self.handlemessage, ChangeMessage('team', -1))) + self._sessionplayer.assigninput( + InputType.RIGHT_PRESS, + Call(self.handlemessage, ChangeMessage('team', 1))) + self._sessionplayer.assigninput( + InputType.BOMB_PRESS, + Call(self.handlemessage, ChangeMessage('character', 1))) + self._sessionplayer.assigninput( + InputType.UP_PRESS, + Call(self.handlemessage, ChangeMessage('profileindex', -1))) + self._sessionplayer.assigninput( + InputType.DOWN_PRESS, + Call(self.handlemessage, ChangeMessage('profileindex', 1))) + self._sessionplayer.assigninput( + (InputType.JUMP_PRESS, InputType.PICK_UP_PRESS, + InputType.PUNCH_PRESS), + Call(self.handlemessage, ChangeMessage('ready', 1))) + self._ready = False + self._update_text() + self._sessionplayer.setname('untitled', real=False) + elif ready == True: + self.characterchooser = True + self._sessionplayer.assigninput( + (InputType.LEFT_PRESS, InputType.RIGHT_PRESS, + InputType.UP_PRESS, InputType.DOWN_PRESS, + InputType.JUMP_PRESS, InputType.BOMB_PRESS, + InputType.PICK_UP_PRESS), self._do_nothing) + self._sessionplayer.assigninput( + (InputType.UP_PRESS), Call(self.handlemessage, ChangeMessage('characterchooser', -1))) + self._sessionplayer.assigninput( + (InputType.DOWN_PRESS), Call(self.handlemessage, ChangeMessage('characterchooser', 1))) + self._sessionplayer.assigninput( + (InputType.BOMB_PRESS), Call(self.handlemessage, ChangeMessage('ready', 0))) + + self._sessionplayer.assigninput( + (InputType.JUMP_PRESS, InputType.PICK_UP_PRESS, InputType.PUNCH_PRESS), + Call(self.handlemessage, ChangeMessage('ready', 2))) + + # Store the last profile picked by this input for reuse. + input_device = self._sessionplayer.inputdevice + name = input_device.name + unique_id = input_device.unique_identifier + device_profiles = _ba.app.config.setdefault( + 'Default Player Profiles', {}) + + # Make an exception if we have no custom profiles and are set + # to random; in that case we'll want to start picking up custom + # profiles if/when one is made so keep our setting cleared. + special = ('_random', '_edit', '__account__') + have_custom_profiles = any(p not in special + for p in self._profiles) + + profilekey = name + ' ' + unique_id + if profilename == '_random' and not have_custom_profiles: + if profilekey in device_profiles: + del device_profiles[profilekey] + else: + device_profiles[profilekey] = profilename + _ba.app.config.commit() + + # Set this player's short and full name. + self._sessionplayer.setname(self._getname(), + self._getname(full=True), + real=True) + self._ready = True + self._update_text() + else: + + # Inform the session that this player is ready. + _ba.getsession().handlemessage(PlayerReadyMessage(self)) + + +def handlemessage(self, msg: Any) -> Any: + """Standard generic message handler.""" + + if isinstance(msg, ChangeMessage): + self._handle_repeat_message_attack() + + # If we've been removed from the lobby, ignore this stuff. + if self._dead: + print_error('chooser got ChangeMessage after dying') + return + + if not self._text_node: + print_error('got ChangeMessage after nodes died') + return + if msg.what == 'characterchooser': + _ba.playsound(self._click_sound) + # update our index in our local list of characters + self._character_index = ((self._character_index + msg.value) % + len(self._character_names)) + self._update_text() + self._update_icon() + + if msg.what == 'team': + sessionteams = self.lobby.sessionteams + if len(sessionteams) > 1: + _ba.playsound(self._swish_sound) + self._selected_team_index = ( + (self._selected_team_index + msg.value) % + len(sessionteams)) + self._update_text() + self.update_position() + self._update_icon() + + elif msg.what == 'profileindex': + if len(self._profilenames) == 1: + + # This should be pretty hard to hit now with + # automatic local accounts. + _ba.playsound(_ba.getsound('error')) + else: + + # Pick the next player profile and assign our name + # and character based on that. + _ba.playsound(self._deek_sound) + self._profileindex = ((self._profileindex + msg.value) % + len(self._profilenames)) + self.update_from_profile() + + elif msg.what == 'character': + _ba.playsound(self._click_sound) + self.characterchooser = True + # update our index in our local list of characters + self._character_index = ((self._character_index + msg.value) % + len(self._character_names)) + self._update_text() + self._update_icon() + + elif msg.what == 'ready': + self._handle_ready_msg(msg.value) + + +def _update_text(self) -> None: + assert self._text_node is not None + if self._ready: + + # Once we're ready, we've saved the name, so lets ask the system + # for it so we get appended numbers and stuff. + text = Lstr(value=self._sessionplayer.getname(full=True)) + if self.characterchooser: + text = Lstr(value='${A}\n${B}', + subs=[('${A}', text), + ('${B}', Lstr(value=""+self._character_names[self._character_index]))]) + self._text_node.scale = 0.8 + else: + text = Lstr(value='${A} (${B})', + subs=[('${A}', text), + ('${B}', Lstr(resource='readyText'))]) + else: + text = Lstr(value=self._getname(full=True)) + self._text_node.scale = 1.0 + + can_switch_teams = len(self.lobby.sessionteams) > 1 + + # Flash as we're coming in. + fin_color = _ba.safecolor(self.get_color()) + (1, ) + if not self._inited: + animate_array(self._text_node, 'color', 4, { + 0.15: fin_color, + 0.25: (2, 2, 2, 1), + 0.35: fin_color + }) + else: + + # Blend if we're in teams mode; switch instantly otherwise. + if can_switch_teams: + animate_array(self._text_node, 'color', 4, { + 0: self._text_node.color, + 0.1: fin_color + }) + else: + self._text_node.color = fin_color + + self._text_node.text = text + +# ba_meta export plugin + + +class HeySmoothy(ba.Plugin): + + def __init__(self): + _lobby.Chooser.__init__ = __init__ + _lobby.Chooser._set_ready = _set_ready + + _lobby.Chooser._update_text = _update_text + _lobby.Chooser.handlemessage = handlemessage diff --git a/plugins/utilities/character_maker.py b/plugins/utilities/character_maker.py new file mode 100644 index 0000000..218c336 --- /dev/null +++ b/plugins/utilities/character_maker.py @@ -0,0 +1,526 @@ +# Released under the MIT License. See LICENSE for details. + + +''' +Character Builder/Maker by Mr.Smoothy +Plugin helps to mix character models and textures in interactive way. + +Watch tutorial : https://www.youtube.com/c/HeySmoothy +Join discord: https://discord.gg/ucyaesh for help +https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server/ + +> create team playlist and add character maker mini game +> Use export command to save character +> Character will be saved in CustomCharacter folder inside Bombsquad Mods folder + +*Only one player in that mini game supported ... + +Characters can be used offline or online +for online you need to share character file with server owners. + +*For server owners:_ + You might know what to do with that file, + Still , + refer code after line 455 in this file , add it as a plugin to import characters from json file. + +*For modders:- + You can add more models and texture , check line near 400 and add asset names , you can also modify sounds and icon in json file (optional) . + +To share your character with friends , + send them character .json file and tell them to put file in same location i.e Bombsquad/CustomCharacter or for PC appdata/Local/Bombsquad/Mods/CustomCharacter + this plugin should be installed on their device too + +Dont forget to share your creativity with me , +send your character screenshot discord: mr.smoothy#5824 https://discord.gg/ucyaesh + +Register your character in above discord server , so other server owners can add your characters. + + +Released on 28 May 2021 + + +Update 2 june : use import +''' + + +# ba_meta require api 7 + + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + +import os +import json +from bastd.actor.spazappearance import * +spazoutfit = { + "color_mask": "neoSpazColorMask", + "color_texture": "neoSpazColor", + "head": "neoSpazHead", + "hand": "neoSpazHand", + "torso": "neoSpazTorso", + "pelvis": "neoSpazTorso", + "upper_arm": "neoSpazUpperArm", + "forearm": "neoSpazForeArm", + "upper_leg": "neoSpazUpperLeg", + "lower_leg": "neoSpazLowerLeg", + "toes_model": "neoSpazToes", + "jump_sounds": ['spazJump01', 'spazJump02', 'spazJump03', 'spazJump04'], + "attack_sounds": ['spazAttack01', 'spazAttack02', 'spazAttack03', 'spazAttack04'], + "impact_sounds": ['spazImpact01', 'spazImpact02', 'spazImpact03', 'spazImpact04'], + "death_sounds": ['spazDeath01'], + "pickup_sounds": ['spazPickup01'], + "fall_sounds": ['spazFall01'], + "icon_texture": "neoSpazIcon", + "icon_mask_texture": "neoSpazIconColorMask", + "style": "spaz" +} +character = None + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class CharacterBuilder(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Character Maker' + description = 'Create your own custom Characters' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + if issubclass(sessiontype, ba.FreeForAllSession): + settings.append( + ba.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Rampage'] + + def __init__(self, settings: dict): + + super().__init__(settings) + + self.initdic() + _ba.set_party_icon_always_visible(True) + self._score_to_win: Optional[int] = None + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + self.bodyindex = 0 + self.modelindex = 0 + self.youtube = ba.newnode( + 'text', + attrs={ + 'text': "youtube.com/c/HeySmoothy", + 'in_world': True, + 'scale': 0.02, + 'color': (1, 0, 0, 0.4), + 'h_align': 'center', + 'position': (0, 4, -1.9) + }) + self.discordservere = ba.newnode( + 'text', + attrs={ + 'text': "discord.gg/ucyaesh", + 'in_world': True, + 'scale': 0.02, + 'color': (0.12, 0.3, 0.6, 0.4), + 'h_align': 'center', + 'position': (-3, 2.7, -1.9) + }) + # self.discord= ba.newnode( + # 'text', + # attrs={ + # 'text': "mr.smoothy#5824", + # 'in_world': True, + # 'scale': 0.02, + # 'color': (01.2, 0.3, 0.7, 0.4), + # 'h_align': 'center', + # 'position': (4,2.7,-1.9) + # }) + # Base class overrides. + self.bodypart = ba.newnode( + 'text', + attrs={ + 'text': "", + 'in_world': True, + 'scale': 0.02, + 'color': (1, 1, 0, 1), + 'h_align': 'center', + 'position': (-4, 6, -4) + }) + self.newmodel = ba.newnode( + 'text', + attrs={ + 'text': "", + 'in_world': True, + 'scale': 0.02, + 'color': (1, 1, 0, 1), + 'h_align': 'center', + 'position': (6, 6, -4) + }) + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return '' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return '' + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + pass + + def on_begin(self) -> None: + super().on_begin() + + def nextBodyPart(self): + self.bodyindex = (self.bodyindex+1) % len(self.dic.keys()) + self.bodypart.delete() + PART = list(self.dic.keys())[self.bodyindex] + self.bodypart = ba.newnode( + 'text', + attrs={ + 'text': PART, + 'in_world': True, + 'scale': 0.02, + 'color': (1, 1, 1, 1), + 'h_align': 'center', + 'position': (-4, 6, -4) + }) + + def prevBodyPart(self): + self.bodyindex = (self.bodyindex-1) % len(self.dic.keys()) + self.bodypart.delete() + PART = list(self.dic.keys())[self.bodyindex] + self.bodypart = ba.newnode( + 'text', + attrs={ + 'text': PART, + 'in_world': True, + 'scale': 0.02, + 'color': (1, 1, 1, 1), + 'h_align': 'center', + 'position': (-4, 6, -4) + }) + + def nextModel(self): + + self.newmodel.delete() + PART = list(self.dic.keys())[self.bodyindex] + self.modelindex = (self.modelindex+1) % len(self.dic[PART]) + model = self.dic[PART][self.modelindex] + self.newmodel = ba.newnode( + 'text', + attrs={ + 'text': model, + 'in_world': True, + 'scale': 0.02, + 'color': (1, 1, 1, 1), + 'h_align': 'center', + 'position': (6, 6, -4) + }) + + self.setModel(PART, model) + + def prevModel(self): + + self.newmodel.delete() + PART = list(self.dic.keys())[self.bodyindex] + self.modelindex = (self.modelindex-1) % len(self.dic[PART]) + model = self.dic[PART][self.modelindex] + self.newmodel = ba.newnode( + 'text', + attrs={ + 'text': model, + 'in_world': True, + 'scale': 0.02, + 'color': (1, 1, 1, 1), + 'h_align': 'center', + 'position': (6, 6, -4) + }) + self.setModel(PART, model) + + def setModel(self, bodypart, modelname): + global spazoutfit + body = _ba.get_foreground_host_activity().players[0].actor.node + if bodypart == 'head': + body.head_model = ba.getmodel(modelname) + elif bodypart == 'torso': + body.torso_model = ba.getmodel(modelname) + elif bodypart == 'pelvis': + body.pelvis_model = ba.getmodel(modelname) + elif bodypart == 'upper_arm': + body.upper_arm_model = ba.getmodel(modelname) + elif bodypart == 'forearm': + body.forearm_model = ba.getmodel(modelname) + elif bodypart == 'hand': + body.hand_model = ba.getmodel(modelname) + elif bodypart == 'upper_leg': + body.upper_leg_model = ba.getmodel(modelname) + elif bodypart == 'lower_leg': + body.lower_leg_model = ba.getmodel(modelname) + elif bodypart == 'toes_model': + body.toes_model = ba.getmodel(modelname) + elif bodypart == 'style': + body.style = modelname + elif bodypart == 'color_texture': + body.color_texture = ba.gettexture(modelname) + elif bodypart == 'color_mask': + body.color_mask_texture = ba.gettexture(modelname) + spazoutfit[bodypart] = modelname + + def spawn_player(self, player: Player) -> ba.Actor: + global character + if character != None: + player.character = character + + self.setcurrentcharacter(player.character) + + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=False, + enable_jump=False, + enable_bomb=False, + enable_pickup=False) + intp = ba.InputType + player.assigninput(intp.JUMP_PRESS, self.nextBodyPart) + player.assigninput(intp.PICK_UP_PRESS, self.prevBodyPart) + player.assigninput(intp.PUNCH_PRESS, self.nextModel) + player.assigninput(intp.BOMB_PRESS, self.prevModel) + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + else: + return super().handlemessage(msg) + return None + + def setcurrentcharacter(self, charname): + global spazoutfit + char = ba.app.spaz_appearances[charname] + spazoutfit['head'] = char.head_model + spazoutfit['hand'] = char.hand_model + spazoutfit['torso'] = char.torso_model + spazoutfit['pelvis'] = char.pelvis_model + spazoutfit['upper_arm'] = char.upper_arm_model + spazoutfit['forearm'] = char.forearm_model + spazoutfit['upper_leg'] = char.upper_leg_model + spazoutfit['lower_leg'] = char.lower_leg_model + spazoutfit['toes_model'] = char.toes_model + spazoutfit['style'] = char.style + spazoutfit['color_mask'] = char.color_mask_texture + spazoutfit['color_texture'] = char.color_texture + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def initdic(self): + self.dic = {"head": ["bomb", "landMine", "trees", "wing", "eyeLid", "impactBomb"], + "hand": ["hairTuft3", "bomb", "powerup"], + "torso": ["bomb", "landMine", "bomb"], + "pelvis": ["hairTuft4", "bomb"], + "upper_arm": ["wing", "locator", "bomb"], + "forearm": ["flagPole", "bomb"], + "upper_leg": ["bomb"], + "lower_leg": ["bomb"], + "toes_model": ["bomb"], + "style": ["spaz", "female", "ninja", "kronk", "mel", "pirate", "santa", "frosty", "bones", "bear", "penguin", "ali", "cyborg", "agent", "pixie", "bunny"], + "color_texture": ["kronk", "egg1", "egg2", "egg3", "achievementGotTheMoves", "bombColor", "crossOut", "explosion", "rgbStripes", "powerupCurse", "powerupHealth", "impactBombColorLit"], + "color_mask": ["egg1", "egg2", "egg3", "bombColor", "crossOutMask", "fontExtras3"] + + } + chars = ["neoSpaz", "zoe", "ninja", "kronk", "mel", "jack", "santa", "frosty", + "bones", "bear", "penguin", "ali", "cyborg", "agent", "wizard", "pixie", "bunny"] + for char in chars: + self.dic["head"].append(char+"Head") + self.dic["hand"].append(char+"Hand") + self.dic["torso"].append(char+"Torso") + if char not in ['mel', "jack", "santa"]: + self.dic["pelvis"].append(char+"Pelvis") + self.dic["upper_arm"].append(char+"UpperArm") + self.dic["forearm"].append(char+"ForeArm") + self.dic["upper_leg"].append(char+"UpperLeg") + self.dic["lower_leg"].append(char+"LowerLeg") + self.dic["toes_model"].append(char+"Toes") + self.dic["color_mask"].append(char+"ColorMask") + if char != "kronk": + self.dic["color_texture"].append(char+"Color") + + +cm = _ba.chatmessage + + +def _new_chatmessage(msg): + if msg.split(" ")[0] == "export": + if len(msg.split(" ")) > 1: + savecharacter(msg.split(" ")[1]) + else: + _ba.screenmessage("Enter name of character") + elif msg.split(" ")[0] == "import": + importcharacter(msg[7:]) + + else: + cm(msg) + + +_ba.chatmessage = _new_chatmessage + + +def savecharacter(name): + path = os.path.join(_ba.env()["python_directory_user"], "CustomCharacters" + os.sep) + if not os.path.isdir(path): + os.makedirs(path) + if _ba.get_foreground_host_activity() != None: + + with open(path+name+".json", 'w') as f: + json.dump(spazoutfit, f, indent=4) + registercharacter(name, spazoutfit) + ba.playsound(ba.getsound("gunCocking")) + _ba.screenmessage("Character Saved") + else: + _ba.screenmessage("Works offline with Character Maker") + + +def importcharacter(name): + if name in ba.app.spaz_appearances: + global character + character = name + try: + _ba.get_foreground_host_activity().players[0].actor.node.handlemessage(ba.DieMessage()) + _ba.screenmessage("Imported") + except: + _ba.screenmessage("works offline with character maker") + + else: + _ba.screenmessage("invalid name check typo \n name is case sensitive") + + +def registercharacter(name, char): + t = Appearance(name.split(".")[0]) + t.color_texture = char['color_texture'] + t.color_mask_texture = char['color_mask'] + t.default_color = (0.6, 0.6, 0.6) + t.default_highlight = (0, 1, 0) + t.icon_texture = char['icon_texture'] + t.icon_mask_texture = char['icon_mask_texture'] + t.head_model = char['head'] + t.torso_model = char['torso'] + t.pelvis_model = char['pelvis'] + t.upper_arm_model = char['upper_arm'] + t.forearm_model = char['forearm'] + t.hand_model = char['hand'] + t.upper_leg_model = char['upper_leg'] + t.lower_leg_model = char['lower_leg'] + t.toes_model = char['toes_model'] + t.jump_sounds = char['jump_sounds'] + t.attack_sounds = char['attack_sounds'] + t.impact_sounds = char['impact_sounds'] + t.death_sounds = char['death_sounds'] + t.pickup_sounds = char['pickup_sounds'] + t.fall_sounds = char['fall_sounds'] + t.style = char['style'] + + +# ba_meta export plugin +class HeySmoothy(ba.Plugin): + + def __init__(self): + _ba.set_party_icon_always_visible(True) + + path = os.path.join(_ba.env()["python_directory_user"], "CustomCharacters" + os.sep) + if not os.path.isdir(path): + os.makedirs(path) + files = os.listdir(path) + for file in files: + with open(path+file, 'r') as f: + character = json.load(f) + registercharacter(file, character) diff --git a/plugins/utilities/ultra_party_window.py b/plugins/utilities/ultra_party_window.py new file mode 100644 index 0000000..2d39444 --- /dev/null +++ b/plugins/utilities/ultra_party_window.py @@ -0,0 +1,2755 @@ +__author__ = 'Droopy' +__version__ = 4.0 + +# ba_meta require api 7 +import datetime +import json +import math +import os +import pickle +import random +import time +import urllib.request +import weakref +from threading import Thread +from typing import List, Tuple, Sequence, Optional, Dict, Any, cast +from hashlib import md5 + +import _ba +import ba +import bastd.ui.party +from bastd.ui.colorpicker import ColorPickerExact +from bastd.ui.confirm import ConfirmWindow +from bastd.ui.mainmenu import MainMenuWindow +from bastd.ui.popup import PopupMenuWindow, PopupWindow, PopupMenu + +_ip = '127.0.0.1' +_port = 43210 +_ping = '-' +url = 'http://bombsquadprivatechat.ml' +last_msg = None + +my_directory = _ba.env()['python_directory_user'] + '/UltraPartyWindowFiles/' +quick_msg_file = my_directory + 'QuickMessages.txt' +cookies_file = my_directory + 'cookies.txt' +saved_ids_file = my_directory + 'saved_ids.json' +my_location = my_directory + + +def initialize(): + config_defaults = {'Party Chat Muted': False, + 'Chat Muted': False, + 'ping button': True, + 'IP button': True, + 'copy button': True, + 'Direct Send': False, + 'Colorful Chat': True, + 'Custom Commands': [], + 'Message Notification': 'bottom', + 'Self Status': 'online', + 'Translate Source Language': '', + 'Translate Destination Language': 'en', + 'Pronunciation': True + } + config = ba.app.config + for key in config_defaults: + if key not in config: + config[key] = config_defaults[key] + + if not os.path.exists(my_directory): + os.makedirs(my_directory) + if not os.path.exists(cookies_file): + with open(cookies_file, 'wb') as f: + pickle.dump({}, f) + if not os.path.exists(saved_ids_file): + with open(saved_ids_file, 'w') as f: + data = {} + json.dump(data, f) + + +def display_error(msg=None): + if msg: + ba.screenmessage(msg, (1, 0, 0)) + else: + ba.screenmessage('Failed!', (1, 0, 0)) + ba.playsound(ba.getsound('error')) + + +def display_success(msg=None): + if msg: + ba.screenmessage(msg, (0, 1, 0)) + else: + ba.screenmessage('Successful!', (0, 1, 0)) + + +class Translate(Thread): + def __init__(self, data, callback): + super().__init__() + self.data = data + self._callback = callback + + def run(self): + _ba.pushcall(ba.Call(ba.screenmessage, 'Translating...'), from_other_thread=True) + response = messenger._send_request(f'{url}/translate', self.data) + if response: + _ba.pushcall(ba.Call(self._callback, response), from_other_thread=True) + + +class ColorTracker: + def __init__(self): + self.saved = {} + + def _get_safe_color(self, sender): + while True: + color = (random.random(), random.random(), random.random()) + s = 0 + background = ba.app.config.get('PartyWindow Main Color', (0.5, 0.5, 0.5)) + for i, j in zip(color, background): + s += (i - j) ** 2 + if s > 0.1: + self.saved[sender] = color + if len(self.saved) > 20: + self.saved.pop(list(self.saved.keys())[0]) + break + time.sleep(0.1) + + def _get_sender_color(self, sender): + if sender not in self.saved: + self.thread = Thread(target=self._get_safe_color, args=(sender,)) + self.thread.start() + return (1, 1, 1) + else: + return self.saved[sender] + + +class PrivateChatHandler: + def __init__(self): + self.pvt_msgs = {} + self.login_id = None + self.last_msg_id = None + self.logged_in = False + self.cookieProcessor = urllib.request.HTTPCookieProcessor() + self.opener = urllib.request.build_opener(self.cookieProcessor) + self.filter = 'all' + self.pending_messages = [] + self.friends_status = {} + self.error = '' + Thread(target=self._ping).start() + + def _load_ids(self): + with open(saved_ids_file, 'r') as f: + saved = json.load(f) + if self.myid in saved: + self.saved_ids = saved[self.myid] + else: + self.saved_ids = {'all': ''} + + def _dump_ids(self): + with open(saved_ids_file, 'r') as f: + saved = json.load(f) + with open(saved_ids_file, 'w') as f: + saved[self.myid] = self.saved_ids + json.dump(saved, f) + + def _ping(self): + self.server_online = False + response = self._send_request(url=f'{url}') + if not response: + self.error = 'Server offline' + elif response: + try: + self.server_online = True + version = float(response.replace('v', '')) + except: + self.error = 'Server offline' + + def _signup(self, registration_key): + data = dict(pb_id=self.myid, registration_key=registration_key) + response = self._send_request(url=f'{url}/signup', data=data) + if response: + if response == 'successful': + display_success('Account Created Successfully') + self._login(registration_key=registration_key) + return True + display_error(response) + + def _save_cookie(self): + with open(cookies_file, 'rb') as f: + cookies = pickle.load(f) + with open(cookies_file, 'wb') as f: + for c in self.cookieProcessor.cookiejar: + cookie = pickle.dumps(c) + break + cookies[self.myid] = cookie + pickle.dump(cookies, f) + + def _cookie_login(self): + self.myid = ba.internal.get_v1_account_misc_read_val_2('resolvedAccountID', '') + try: + with open(cookies_file, 'rb') as f: + cookies = pickle.load(f) + except: + return False + if self.myid in cookies: + cookie = pickle.loads(cookies[self.myid]) + self.cookieProcessor.cookiejar.set_cookie(cookie) + self.opener = urllib.request.build_opener(self.cookieProcessor) + response = self._send_request(url=f'{url}/login') + if response.startswith('logged in as'): + self.logged_in = True + self._load_ids() + display_success(response) + return True + + def _login(self, registration_key): + self.myid = ba.internal.get_v1_account_misc_read_val_2('resolvedAccountID', '') + data = dict(pb_id=self.myid, registration_key=registration_key) + response = self._send_request(url=f'{url}/login', data=data) + if response == 'successful': + self.logged_in = True + self._load_ids() + self._save_cookie() + display_success('Account Logged in Successfully') + return True + else: + display_error(response) + + def _query(self, pb_id=None): + if not pb_id: + pb_id = self.myid + response = self._send_request(url=f'{url}/query/{pb_id}') + if response == 'exists': + return True + return False + + def _send_request(self, url, data=None): + try: + if not data: + response = self.opener.open(url) + else: + response = self.opener.open(url, data=json.dumps(data).encode()) + if response.getcode() != 200: + display_error(response.read().decode()) + return None + else: + return response.read().decode() + except: + return None + + def _save_id(self, account_id, nickname='', verify=True): + # display_success(f'Saving {account_id}. Please wait...') + if verify: + url = 'http://bombsquadgame.com/accountquery?id=' + account_id + response = json.loads(urllib.request.urlopen(url).read().decode()) + if 'error' in response: + display_error('Enter valid account id') + return False + self.saved_ids[account_id] = {} + name = None + if nickname == '': + name_html = response['name_html'] + name = name_html.split('>')[1] + nick = name if name else nickname + else: + nick = nickname + self.saved_ids[account_id] = nick + self._dump_ids() + display_success(f'Account added: {nick}({account_id})') + return True + + def _remove_id(self, account_id): + removed = self.saved_ids.pop(account_id) + self._dump_ids() + ba.screenmessage(f'Removed successfully: {removed}({account_id})', (0, 1, 0)) + ba.playsound(ba.getsound('shieldDown')) + + def _format_message(self, msg): + filter = msg['filter'] + if filter in self.saved_ids: + if self.filter == 'all': + message = '[' + self.saved_ids[filter] + ']' + msg['message'] + else: + message = msg['message'] + else: + message = '[' + msg['filter'] + ']: ' + \ + 'Message from unsaved id. Save id to view message.' + return message + + def _get_status(self, id, type='status'): + info = self.friends_status.get(id, {}) + if not info: + return '-' + if type == 'status': + return info['status'] + else: + last_seen = info["last_seen"] + last_seen = _get_local_time(last_seen) + ba.screenmessage(f'Last seen on: {last_seen}') + + +def _get_local_time(utctime): + d = datetime.datetime.strptime(utctime, '%d-%m-%Y %H:%M:%S') + d = d.replace(tzinfo=datetime.timezone.utc) + d = d.astimezone() + return d.strftime('%B %d,\t\t%H:%M:%S') + + +def update_status(): + if messenger.logged_in: + if ba.app.config['Self Status'] == 'online': + host = _ba.get_connection_to_host_info().get('name', '') + if host: + my_status = f'Playing in {host}' + else: + my_status = 'in Lobby' + ids_to_check = [i for i in messenger.saved_ids if i != 'all'] + response = messenger._send_request(url=f'{url}/updatestatus', + data=dict(self_status=my_status, ids=ids_to_check)) + if response: + messenger.friends_status = json.loads(response) + else: + messenger.friends_status = {} + + +def messenger_thread(): + counter = 0 + while True: + counter += 1 + time.sleep(0.6) + check_new_message() + if counter > 5: + counter = 0 + update_status() + + +def check_new_message(): + if messenger.logged_in: + if messenger.login_id != messenger.myid: + response = messenger._send_request(f'{url}/first') + if response: + messenger.pvt_msgs = json.loads(response) + if messenger.pvt_msgs['all']: + messenger.last_msg_id = messenger.pvt_msgs['all'][-1]['id'] + messenger.login_id = messenger.myid + else: + response = messenger._send_request(f'{url}/new/{messenger.last_msg_id}') + if response: + new_msgs = json.loads(response) + if new_msgs: + for msg in new_msgs['messages']: + if msg['id'] > messenger.last_msg_id: + messenger.last_msg_id = msg['id'] + messenger.pvt_msgs['all'].append( + dict(id=msg['id'], filter=msg['filter'], message=msg['message'], sent=msg['sent'])) + if len(messenger.pvt_msgs['all']) > 40: + messenger.pvt_msgs['all'].pop(0) + if msg['filter'] not in messenger.pvt_msgs: + messenger.pvt_msgs[msg['filter']] = [ + dict(id=msg['id'], filter=msg['filter'], message=msg['message'], sent=msg['sent'])] + else: + messenger.pvt_msgs[msg['filter']].append( + dict(id=msg['id'], filter=msg['filter'], message=msg['message'], sent=msg['sent'])) + if len(messenger.pvt_msgs[msg['filter']]) > 20: + messenger.pvt_msgs[msg['filter']].pop(0) + messenger.pending_messages.append( + (messenger._format_message(msg), msg['filter'], msg['sent'])) + + +def display_message(msg, msg_type, filter=None, sent=None): + flag = None + notification = ba.app.config['Message Notification'] + if _ba.app.ui.party_window: + if _ba.app.ui.party_window(): + if _ba.app.ui.party_window()._private_chat: + flag = 1 + if msg_type == 'private': + if messenger.filter == filter or messenger.filter == 'all': + _ba.app.ui.party_window().on_chat_message(msg, sent) + else: + if notification == 'top': + ba.screenmessage(msg, (1, 1, 0), True, ba.gettexture('coin')) + else: + ba.screenmessage(msg, (1, 1, 0), False) + else: + ba.screenmessage(msg, (0.2, 1.0, 1.0), True, ba.gettexture('circleShadow')) + else: + flag = 1 + if msg_type == 'private': + if notification == 'top': + ba.screenmessage(msg, (1, 1, 0), True, ba.gettexture('coin')) + else: + ba.screenmessage(msg, (1, 1, 0), False) + if not flag: + if msg_type == 'private': + if notification == 'top': + ba.screenmessage(msg, (1, 1, 0), True, ba.gettexture('coin')) + else: + ba.screenmessage(msg, (1, 1, 0), False) + else: + ba.screenmessage(msg, (0.2, 1.0, 1.0), True, ba.gettexture('circleShadow')) + + +def msg_displayer(): + for msg in messenger.pending_messages: + display_message(msg[0], 'private', msg[1], msg[2]) + messenger.pending_messages.remove(msg) + if ba.app.config['Chat Muted'] and not ba.app.config['Party Chat Muted']: + global last_msg + last = _ba.get_chat_messages() + lm = last[-1] if last else None + if lm != last_msg: + last_msg = lm + display_message(lm, 'public') + + +class SortQuickMessages: + def __init__(self): + uiscale = ba.app.ui.uiscale + bg_color = ba.app.config.get('PartyWindow Main Color', (0.5, 0.5, 0.5)) + self._width = 750 if uiscale is ba.UIScale.SMALL else 600 + self._height = (300 if uiscale is ba.UIScale.SMALL else + 325 if uiscale is ba.UIScale.MEDIUM else 350) + self._root_widget = ba.containerwidget( + size=(self._width, self._height), + transition='in_right', + on_outside_click_call=self._save, + color=bg_color, + parent=_ba.get_special_widget('overlay_stack'), + scale=(2.0 if uiscale is ba.UIScale.SMALL else + 1.3 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -16) if uiscale is ba.UIScale.SMALL else (0, 0)) + ba.textwidget(parent=self._root_widget, + position=(-10, self._height - 50), + size=(self._width, 25), + text='Sort Quick Messages', + color=ba.app.ui.title_color, + scale=1.05, + h_align='center', + v_align='center', + maxwidth=270) + b_textcolor = (0.4, 0.75, 0.5) + up_button = ba.buttonwidget(parent=self._root_widget, + position=(10, 170), + size=(75, 75), + on_activate_call=self._move_up, + label=ba.charstr(ba.SpecialChar.UP_ARROW), + button_type='square', + color=bg_color, + textcolor=b_textcolor, + autoselect=True, + repeat=True) + down_button = ba.buttonwidget(parent=self._root_widget, + position=(10, 75), + size=(75, 75), + on_activate_call=self._move_down, + label=ba.charstr(ba.SpecialChar.DOWN_ARROW), + button_type='square', + color=bg_color, + textcolor=b_textcolor, + autoselect=True, + repeat=True) + self._scroll_width = self._width - 150 + self._scroll_height = self._height - 110 + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + size=(self._scroll_width, self._scroll_height), + color=bg_color, + position=(100, 40)) + self._columnwidget = ba.columnwidget( + parent=self._scrollwidget, + border=2, + margin=0) + with open(quick_msg_file, 'r') as f: + self.msgs = f.read().split('\n') + self._msg_selected = None + self._refresh() + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._save) + + def _refresh(self): + for child in self._columnwidget.get_children(): + child.delete() + for msg in enumerate(self.msgs): + txt = ba.textwidget( + parent=self._columnwidget, + size=(self._scroll_width - 10, 30), + selectable=True, + always_highlight=True, + on_select_call=ba.Call(self._on_msg_select, msg), + text=msg[1], + h_align='left', + v_align='center', + maxwidth=self._scroll_width) + if msg == self._msg_selected: + ba.columnwidget(edit=self._columnwidget, + selected_child=txt, + visible_child=txt) + + def _on_msg_select(self, msg): + self._msg_selected = msg + + def _move_up(self): + index = self._msg_selected[0] + msg = self._msg_selected[1] + if index: + self.msgs.insert((index - 1), self.msgs.pop(index)) + self._msg_selected = (index - 1, msg) + self._refresh() + + def _move_down(self): + index = self._msg_selected[0] + msg = self._msg_selected[1] + if index + 1 < len(self.msgs): + self.msgs.insert((index + 1), self.msgs.pop(index)) + self._msg_selected = (index + 1, msg) + self._refresh() + + def _save(self) -> None: + try: + with open(quick_msg_file, 'w') as f: + f.write('\n'.join(self.msgs)) + except: + ba.print_exception() + ba.screenmessage('Error!', (1, 0, 0)) + ba.containerwidget( + edit=self._root_widget, + transition='out_right') + + +class TranslationSettings: + def __init__(self): + uiscale = ba.app.ui.uiscale + height = (300 if uiscale is ba.UIScale.SMALL else + 350 if uiscale is ba.UIScale.MEDIUM else 400) + width = (500 if uiscale is ba.UIScale.SMALL else + 600 if uiscale is ba.UIScale.MEDIUM else 650) + self._transition_out: Optional[str] + scale_origin: Optional[Tuple[float, float]] + self._transition_out = 'out_scale' + scale_origin = 10 + transition = 'in_scale' + scale_origin = None + cancel_is_selected = False + cfg = ba.app.config + bg_color = cfg.get('PartyWindow Main Color', (0.5, 0.5, 0.5)) + + LANGUAGES = { + '': 'Auto-Detect', + 'af': 'afrikaans', + 'sq': 'albanian', + 'am': 'amharic', + 'ar': 'arabic', + 'hy': 'armenian', + 'az': 'azerbaijani', + 'eu': 'basque', + 'be': 'belarusian', + 'bn': 'bengali', + 'bs': 'bosnian', + 'bg': 'bulgarian', + 'ca': 'catalan', + 'ceb': 'cebuano', + 'ny': 'chichewa', + 'zh-cn': 'chinese (simplified)', + 'zh-tw': 'chinese (traditional)', + 'co': 'corsican', + 'hr': 'croatian', + 'cs': 'czech', + 'da': 'danish', + 'nl': 'dutch', + 'en': 'english', + 'eo': 'esperanto', + 'et': 'estonian', + 'tl': 'filipino', + 'fi': 'finnish', + 'fr': 'french', + 'fy': 'frisian', + 'gl': 'galician', + 'ka': 'georgian', + 'de': 'german', + 'el': 'greek', + 'gu': 'gujarati', + 'ht': 'haitian creole', + 'ha': 'hausa', + 'haw': 'hawaiian', + 'iw': 'hebrew', + 'he': 'hebrew', + 'hi': 'hindi', + 'hmn': 'hmong', + 'hu': 'hungarian', + 'is': 'icelandic', + 'ig': 'igbo', + 'id': 'indonesian', + 'ga': 'irish', + 'it': 'italian', + 'ja': 'japanese', + 'jw': 'javanese', + 'kn': 'kannada', + 'kk': 'kazakh', + 'km': 'khmer', + 'ko': 'korean', + 'ku': 'kurdish (kurmanji)', + 'ky': 'kyrgyz', + 'lo': 'lao', + 'la': 'latin', + 'lv': 'latvian', + 'lt': 'lithuanian', + 'lb': 'luxembourgish', + 'mk': 'macedonian', + 'mg': 'malagasy', + 'ms': 'malay', + 'ml': 'malayalam', + 'mt': 'maltese', + 'mi': 'maori', + 'mr': 'marathi', + 'mn': 'mongolian', + 'my': 'myanmar (burmese)', + 'ne': 'nepali', + 'no': 'norwegian', + 'or': 'odia', + 'ps': 'pashto', + 'fa': 'persian', + 'pl': 'polish', + 'pt': 'portuguese', + 'pa': 'punjabi', + 'ro': 'romanian', + 'ru': 'russian', + 'sm': 'samoan', + 'gd': 'scots gaelic', + 'sr': 'serbian', + 'st': 'sesotho', + 'sn': 'shona', + 'sd': 'sindhi', + 'si': 'sinhala', + 'sk': 'slovak', + 'sl': 'slovenian', + 'so': 'somali', + 'es': 'spanish', + 'su': 'sundanese', + 'sw': 'swahili', + 'sv': 'swedish', + 'tg': 'tajik', + 'ta': 'tamil', + 'te': 'telugu', + 'th': 'thai', + 'tr': 'turkish', + 'uk': 'ukrainian', + 'ur': 'urdu', + 'ug': 'uyghur', + 'uz': 'uzbek', + 'vi': 'vietnamese', + 'cy': 'welsh', + 'xh': 'xhosa', + 'yi': 'yiddish', + 'yo': 'yoruba', + 'zu': 'zulu'} + + self.root_widget = ba.containerwidget( + size=(width, height), + color=bg_color, + transition=transition, + toolbar_visibility='menu_minimal_no_back', + parent=_ba.get_special_widget('overlay_stack'), + on_outside_click_call=self._cancel, + scale=(2.1 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + scale_origin_stack_offset=scale_origin) + ba.textwidget(parent=self.root_widget, + position=(width * 0.5, height - 45), + size=(20, 20), + h_align='center', + v_align='center', + text="Text Translation", + scale=0.9, + color=(5, 5, 5)) + cbtn = btn = ba.buttonwidget(parent=self.root_widget, + autoselect=True, + position=(30, height - 60), + size=(30, 30), + label=ba.charstr(ba.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self._cancel) + + source_lang_text = ba.textwidget(parent=self.root_widget, + position=(40, height - 110), + size=(20, 20), + h_align='left', + v_align='center', + text="Source Language : ", + scale=0.9, + color=(1, 1, 1)) + + source_lang_menu = PopupMenu( + parent=self.root_widget, + position=(330 if uiscale is ba.UIScale.SMALL else 400, height - 115), + width=200, + scale=(2.8 if uiscale is ba.UIScale.SMALL else + 1.8 if uiscale is ba.UIScale.MEDIUM else 1.2), + current_choice=cfg['Translate Source Language'], + choices=LANGUAGES.keys(), + choices_display=(ba.Lstr(value=i) for i in LANGUAGES.values()), + button_size=(130, 35), + on_value_change_call=self._change_source) + + destination_lang_text = ba.textwidget(parent=self.root_widget, + position=(40, height - 165), + size=(20, 20), + h_align='left', + v_align='center', + text="Destination Language : ", + scale=0.9, + color=(1, 1, 1)) + + destination_lang_menu = PopupMenu( + parent=self.root_widget, + position=(330 if uiscale is ba.UIScale.SMALL else 400, height - 170), + width=200, + scale=(2.8 if uiscale is ba.UIScale.SMALL else + 1.8 if uiscale is ba.UIScale.MEDIUM else 1.2), + current_choice=cfg['Translate Destination Language'], + choices=list(LANGUAGES.keys())[1:], + choices_display=list(ba.Lstr(value=i) for i in LANGUAGES.values())[1:], + button_size=(130, 35), + on_value_change_call=self._change_destination) + + try: + + translation_mode_text = ba.textwidget(parent=self.root_widget, + position=(40, height - 215), + size=(20, 20), + h_align='left', + v_align='center', + text="Translate Mode", + scale=0.9, + color=(1, 1, 1)) + decoration = ba.textwidget(parent=self.root_widget, + position=(40, height - 225), + size=(20, 20), + h_align='left', + v_align='center', + text="________________", + scale=0.9, + color=(1, 1, 1)) + + language_char_text = ba.textwidget(parent=self.root_widget, + position=(85, height - 273), + size=(20, 20), + h_align='left', + v_align='center', + text='Normal Translation', + scale=0.6, + color=(1, 1, 1)) + + pronunciation_text = ba.textwidget(parent=self.root_widget, + position=(295, height - 273), + size=(20, 20), + h_align='left', + v_align='center', + text="Show Prononciation", + scale=0.6, + color=(1, 1, 1)) + + from bastd.ui.radiogroup import make_radio_group + cur_val = ba.app.config.get('Pronunciation', True) + cb1 = ba.checkboxwidget( + parent=self.root_widget, + position=(250, height - 275), + size=(20, 20), + maxwidth=300, + scale=1, + autoselect=True, + text="") + cb2 = ba.checkboxwidget( + parent=self.root_widget, + position=(40, height - 275), + size=(20, 20), + maxwidth=300, + scale=1, + autoselect=True, + text="") + make_radio_group((cb1, cb2), (True, False), cur_val, + self._actions_changed) + except Exception as e: + print(e) + pass + + ba.containerwidget(edit=self.root_widget, cancel_button=btn) + + def _change_source(self, choice): + cfg = ba.app.config + cfg['Translate Source Language'] = choice + cfg.apply_and_commit() + + def _change_destination(self, choice): + cfg = ba.app.config + cfg['Translate Destination Language'] = choice + cfg.apply_and_commit() + + def _actions_changed(self, v: str) -> None: + cfg = ba.app.config + cfg['Pronunciation'] = v + cfg.apply_and_commit() + + def _cancel(self) -> None: + ba.containerwidget(edit=self.root_widget, transition='out_scale') + SettingsWindow() + + +class SettingsWindow: + + def __init__(self): + uiscale = ba.app.ui.uiscale + height = (300 if uiscale is ba.UIScale.SMALL else + 350 if uiscale is ba.UIScale.MEDIUM else 400) + width = (500 if uiscale is ba.UIScale.SMALL else + 600 if uiscale is ba.UIScale.MEDIUM else 650) + scroll_h = (200 if uiscale is ba.UIScale.SMALL else + 250 if uiscale is ba.UIScale.MEDIUM else 270) + scroll_w = (450 if uiscale is ba.UIScale.SMALL else + 550 if uiscale is ba.UIScale.MEDIUM else 600) + self._transition_out: Optional[str] + scale_origin: Optional[Tuple[float, float]] + self._transition_out = 'out_scale' + scale_origin = 10 + transition = 'in_scale' + scale_origin = None + cancel_is_selected = False + cfg = ba.app.config + bg_color = cfg.get('PartyWindow Main Color', (0.5, 0.5, 0.5)) + + self.root_widget = ba.containerwidget( + size=(width, height), + color=bg_color, + transition=transition, + toolbar_visibility='menu_minimal_no_back', + parent=_ba.get_special_widget('overlay_stack'), + on_outside_click_call=self._cancel, + scale=(2.1 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + scale_origin_stack_offset=scale_origin) + ba.textwidget(parent=self.root_widget, + position=(width * 0.5, height - 45), + size=(20, 20), + h_align='center', + v_align='center', + text="Custom Settings", + scale=0.9, + color=(5, 5, 5)) + cbtn = btn = ba.buttonwidget(parent=self.root_widget, + autoselect=True, + position=(30, height - 60), + size=(30, 30), + label=ba.charstr(ba.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self._cancel) + scroll_position = (30 if uiscale is ba.UIScale.SMALL else + 40 if uiscale is ba.UIScale.MEDIUM else 50) + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + position=(30, scroll_position), + simple_culling_v=20.0, + highlight=False, + size=(scroll_w, scroll_h), + selection_loops_to_parent=True) + ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) + self._subcontainer = ba.columnwidget(parent=self._scrollwidget, + selection_loops_to_parent=True) + ip_button = ba.checkboxwidget( + parent=self._subcontainer, + size=(300, 30), + maxwidth=300, + textcolor=((0, 1, 0) if cfg['IP button'] else (0.95, 0.65, 0)), + scale=1, + value=cfg['IP button'], + autoselect=True, + text="IP Button", + on_value_change_call=self.ip_button) + ping_button = ba.checkboxwidget( + parent=self._subcontainer, + size=(300, 30), + maxwidth=300, + textcolor=((0, 1, 0) if cfg['ping button'] else (0.95, 0.65, 0)), + scale=1, + value=cfg['ping button'], + autoselect=True, + text="Ping Button", + on_value_change_call=self.ping_button) + copy_button = ba.checkboxwidget( + parent=self._subcontainer, + size=(300, 30), + maxwidth=300, + textcolor=((0, 1, 0) if cfg['copy button'] else (0.95, 0.65, 0)), + scale=1, + value=cfg['copy button'], + autoselect=True, + text="Copy Text Button", + on_value_change_call=self.copy_button) + direct_send = ba.checkboxwidget( + parent=self._subcontainer, + size=(300, 30), + maxwidth=300, + textcolor=((0, 1, 0) if cfg['Direct Send'] else (0.95, 0.65, 0)), + scale=1, + value=cfg['Direct Send'], + autoselect=True, + text="Directly Send Custom Commands", + on_value_change_call=self.direct_send) + colorfulchat = ba.checkboxwidget( + parent=self._subcontainer, + size=(300, 30), + maxwidth=300, + textcolor=((0, 1, 0) if cfg['Colorful Chat'] else (0.95, 0.65, 0)), + scale=1, + value=cfg['Colorful Chat'], + autoselect=True, + text="Colorful Chat", + on_value_change_call=self.colorful_chat) + msg_notification_text = ba.textwidget(parent=self._subcontainer, + scale=0.8, + color=(1, 1, 1), + text='Message Notifcation:', + size=(100, 30), + h_align='left', + v_align='center') + msg_notification_widget = PopupMenu( + parent=self._subcontainer, + position=(100, height - 1200), + width=200, + scale=(2.8 if uiscale is ba.UIScale.SMALL else + 1.8 if uiscale is ba.UIScale.MEDIUM else 1.2), + choices=['top', 'bottom'], + current_choice=ba.app.config['Message Notification'], + button_size=(80, 25), + on_value_change_call=self._change_notification) + self_status_text = ba.textwidget(parent=self._subcontainer, + scale=0.8, + color=(1, 1, 1), + text='Self Status:', + size=(100, 30), + h_align='left', + v_align='center') + self_status_widget = PopupMenu( + parent=self._subcontainer, + position=(50, height - 1000), + width=200, + scale=(2.8 if uiscale is ba.UIScale.SMALL else + 1.8 if uiscale is ba.UIScale.MEDIUM else 1.2), + choices=['online', 'offline'], + current_choice=ba.app.config['Self Status'], + button_size=(80, 25), + on_value_change_call=self._change_status) + ba.containerwidget(edit=self.root_widget, cancel_button=btn) + ba.containerwidget(edit=self.root_widget, + selected_child=(cbtn if cbtn is not None + and cancel_is_selected else None), + start_button=None) + + self._translation_btn = ba.buttonwidget(parent=self._subcontainer, + scale=1.2, + position=(100, 1200), + size=(150, 50), + label='Translate Settings', + on_activate_call=self._translaton_btn, + autoselect=True) + + def ip_button(self, value: bool): + cfg = ba.app.config + cfg['IP button'] = value + cfg.apply_and_commit() + if cfg['IP button']: + ba.screenmessage("IP Button is now enabled", color=(0, 1, 0)) + else: + ba.screenmessage("IP Button is now disabled", color=(1, 0.7, 0)) + + def ping_button(self, value: bool): + cfg = ba.app.config + cfg['ping button'] = value + cfg.apply_and_commit() + if cfg['ping button']: + ba.screenmessage("Ping Button is now enabled", color=(0, 1, 0)) + else: + ba.screenmessage("Ping Button is now disabled", color=(1, 0.7, 0)) + + def copy_button(self, value: bool): + cfg = ba.app.config + cfg['copy button'] = value + cfg.apply_and_commit() + if cfg['copy button']: + ba.screenmessage("Copy Text Button is now enabled", color=(0, 1, 0)) + else: + ba.screenmessage("Copy Text Button is now disabled", color=(1, 0.7, 0)) + + def direct_send(self, value: bool): + cfg = ba.app.config + cfg['Direct Send'] = value + cfg.apply_and_commit() + + def colorful_chat(self, value: bool): + cfg = ba.app.config + cfg['Colorful Chat'] = value + cfg.apply_and_commit() + + def _change_notification(self, choice): + cfg = ba.app.config + cfg['Message Notification'] = choice + cfg.apply_and_commit() + + def _change_status(self, choice): + cfg = ba.app.config + cfg['Self Status'] = choice + cfg.apply_and_commit() + + def _translaton_btn(self): + try: + ba.containerwidget(edit=self.root_widget, transition='out_scale') + TranslationSettings() + except Exception as e: + print(e) + pass + + def _cancel(self) -> None: + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + +class PartyWindow(ba.Window): + """Party list/chat window.""" + + def __del__(self) -> None: + _ba.set_party_window_open(False) + + def __init__(self, origin: Sequence[float] = (0, 0)): + self._private_chat = False + self._firstcall = True + self.ping_server() + _ba.set_party_window_open(True) + self._r = 'partyWindow' + self._popup_type: Optional[str] = None + self._popup_party_member_client_id: Optional[int] = None + self._popup_party_member_is_host: Optional[bool] = None + self._width = 500 + uiscale = ba.app.ui.uiscale + self._height = (365 if uiscale is ba.UIScale.SMALL else + 480 if uiscale is ba.UIScale.MEDIUM else 600) + self.bg_color = ba.app.config.get('PartyWindow Main Color', (0.5, 0.5, 0.5)) + self.ping_timer = ba.Timer(5, ba.WeakCall(self.ping_server), repeat=True) + + ba.Window.__init__(self, root_widget=ba.containerwidget( + size=(self._width, self._height), + transition='in_scale', + color=self.bg_color, + parent=_ba.get_special_widget('overlay_stack'), + on_outside_click_call=self.close_with_sound, + scale_origin_stack_offset=origin, + scale=(2.0 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else ( + 240, 0) if uiscale is ba.UIScale.MEDIUM else (330, 20))) + + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + scale=0.7, + position=(30, self._height - 47), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=self.bg_color, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + self._menu_button = ba.buttonwidget( + parent=self._root_widget, + scale=0.7, + position=(self._width - 80, self._height - 47), + size=(50, 50), + label='...', + autoselect=True, + button_type='square', + on_activate_call=ba.WeakCall(self._on_menu_button_press), + color=self.bg_color, + iconscale=1.2) + + info = _ba.get_connection_to_host_info() + if info.get('name', '') != '': + self.title = ba.Lstr(value=info['name']) + else: + self.title = ba.Lstr(resource=self._r + '.titleText') + + self._title_text = ba.textwidget(parent=self._root_widget, + scale=0.9, + color=(0.5, 0.7, 0.5), + text=self.title, + size=(0, 0), + position=(self._width * 0.47, + self._height - 29), + maxwidth=self._width * 0.6, + h_align='center', + v_align='center') + self._empty_str = ba.textwidget(parent=self._root_widget, + scale=0.75, + size=(0, 0), + position=(self._width * 0.5, + self._height - 65), + maxwidth=self._width * 0.85, + h_align='center', + v_align='center') + + self._scroll_width = self._width - 50 + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + size=(self._scroll_width, + self._height - 200), + position=(30, 80), + color=self.bg_color) + self._columnwidget = ba.columnwidget(parent=self._scrollwidget, + border=2, + margin=0) + ba.widget(edit=self._menu_button, down_widget=self._columnwidget) + + self._muted_text = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + size=(0, 0), + h_align='center', + v_align='center', + text=ba.Lstr(resource='chatMutedText')) + + self._text_field = txt = ba.textwidget( + parent=self._root_widget, + editable=True, + size=(500, 40), + position=(54, 39), + text='', + maxwidth=494, + shadow=0.3, + flatness=1.0, + description=ba.Lstr(resource=self._r + '.chatMessageText'), + autoselect=True, + v_align='center', + corner_scale=0.7) + + ba.widget(edit=self._scrollwidget, + autoselect=True, + left_widget=self._cancel_button, + up_widget=self._cancel_button, + down_widget=self._text_field) + ba.widget(edit=self._columnwidget, + autoselect=True, + up_widget=self._cancel_button, + down_widget=self._text_field) + ba.containerwidget(edit=self._root_widget, selected_child=txt) + self._send_button = btn = ba.buttonwidget(parent=self._root_widget, + size=(50, 35), + label=ba.Lstr(resource=self._r + '.sendText'), + button_type='square', + autoselect=True, + color=self.bg_color, + position=(self._width - 90, 35), + on_activate_call=self._send_chat_message) + ba.textwidget(edit=txt, on_return_press_call=btn.activate) + self._previous_button = ba.buttonwidget(parent=self._root_widget, + size=(30, 30), + label=ba.charstr(ba.SpecialChar.UP_ARROW), + button_type='square', + autoselect=True, + position=(15, 57), + color=self.bg_color, + scale=0.75, + on_activate_call=self._previous_message) + self._next_button = ba.buttonwidget(parent=self._root_widget, + size=(30, 30), + label=ba.charstr(ba.SpecialChar.DOWN_ARROW), + button_type='square', + autoselect=True, + color=self.bg_color, + scale=0.75, + position=(15, 28), + on_activate_call=self._next_message) + self._translate_button = ba.buttonwidget(parent=self._root_widget, + size=(55, 47), + label="Trans", + button_type='square', + autoselect=True, + color=self.bg_color, + scale=0.75, + position=(self._width - 28, 35), + on_activate_call=self._translate) + if ba.app.config['copy button']: + self._copy_button = ba.buttonwidget(parent=self._root_widget, + size=(15, 15), + label='©', + button_type='backSmall', + autoselect=True, + color=self.bg_color, + position=(self._width - 40, 80), + on_activate_call=self._copy_to_clipboard) + self._ping_button = None + if info.get('name', '') != '': + if ba.app.config['ping button']: + self._ping_button = ba.buttonwidget( + parent=self._root_widget, + scale=0.7, + position=(self._width - 538, self._height - 57), + size=(75, 75), + autoselect=True, + button_type='square', + label=f'{_ping}', + on_activate_call=self._send_ping, + color=self.bg_color, + text_scale=2.3, + iconscale=1.2) + if ba.app.config['IP button']: + self._ip_port_button = ba.buttonwidget(parent=self._root_widget, + size=(30, 30), + label='IP', + button_type='square', + autoselect=True, + color=self.bg_color, + position=(self._width - 530, + self._height - 100), + on_activate_call=self._ip_port_msg) + self._settings_button = ba.buttonwidget(parent=self._root_widget, + size=(50, 50), + scale=0.5, + button_type='square', + autoselect=True, + color=self.bg_color, + position=(self._width - 40, self._height - 47), + on_activate_call=self._on_setting_button_press, + icon=ba.gettexture('settingsIcon'), + iconscale=1.2) + self._privatechat_button = ba.buttonwidget(parent=self._root_widget, + size=(50, 50), + scale=0.5, + button_type='square', + autoselect=True, + color=self.bg_color, + position=(self._width - 40, self._height - 80), + on_activate_call=self._on_privatechat_button_press, + icon=ba.gettexture('ouyaOButton'), + iconscale=1.2) + self._name_widgets: List[ba.Widget] = [] + self._roster: Optional[List[Dict[str, Any]]] = None + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + repeat=True, + timetype=ba.TimeType.REAL) + self._update() + + def on_chat_message(self, msg: str, sent=None) -> None: + """Called when a new chat message comes through.""" + if ba.app.config['Party Chat Muted'] and not _ba.app.ui.party_window()._private_chat: + return + if sent: + self._add_msg(msg, sent) + else: + self._add_msg(msg) + + def _add_msg(self, msg: str, sent=None) -> None: + if ba.app.config['Colorful Chat']: + sender = msg.split(': ')[0] + color = color_tracker._get_sender_color(sender) if sender else (1, 1, 1) + else: + color = (1, 1, 1) + maxwidth = self._scroll_width * 0.94 + txt = ba.textwidget(parent=self._columnwidget, + text=msg, + h_align='left', + v_align='center', + size=(0, 13), + scale=0.55, + color=color, + maxwidth=maxwidth, + shadow=0.3, + flatness=1.0) + if sent: + ba.textwidget(edit=txt, size=(100, 15), + selectable=True, + click_activate=True, + on_activate_call=ba.Call(ba.screenmessage, f'Message sent: {_get_local_time(sent)}')) + self._chat_texts.append(txt) + if len(self._chat_texts) > 40: + first = self._chat_texts.pop(0) + first.delete() + ba.containerwidget(edit=self._columnwidget, visible_child=txt) + + def _on_menu_button_press(self) -> None: + is_muted = ba.app.config['Party Chat Muted'] + uiscale = ba.app.ui.uiscale + + choices = ['muteOption', 'modifyColor', 'addQuickReply', 'removeQuickReply', 'credits'] + choices_display = ['Mute Option', 'Modify Main Color', + 'Add as Quick Reply', 'Remove a Quick Reply', 'Credits'] + + if hasattr(_ba.get_foreground_host_activity(), '_map'): + choices.append('manualCamera') + choices_display.append('Manual Camera') + + PopupMenuWindow( + position=self._menu_button.get_screen_space_center(), + color=self.bg_color, + scale=(2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23), + choices=choices, + choices_display=self._create_baLstr_list(choices_display), + current_choice='muteOption', + delegate=self) + self._popup_type = 'menu' + + def _update(self) -> None: + if not self._private_chat: + _ba.set_party_window_open(True) + ba.textwidget(edit=self._title_text, text=self.title) + if self._firstcall: + if hasattr(self, '_status_text'): + self._status_text.delete() + self._roster = [] + self._firstcall = False + self._chat_texts: List[ba.Widget] = [] + if not ba.app.config['Party Chat Muted']: + msgs = _ba.get_chat_messages() + for msg in msgs: + self._add_msg(msg) + # update muted state + if ba.app.config['Party Chat Muted']: + ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.3)) + # clear any chat texts we're showing + if self._chat_texts: + while self._chat_texts: + first = self._chat_texts.pop() + first.delete() + else: + ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0)) + if self._ping_button: + ba.buttonwidget(edit=self._ping_button, + label=f'{_ping}', + textcolor=self._get_ping_color()) + + # update roster section + roster = _ba.get_game_roster() + if roster != self._roster or self._firstcall: + + self._roster = roster + + # clear out old + for widget in self._name_widgets: + widget.delete() + self._name_widgets = [] + if not self._roster: + top_section_height = 60 + ba.textwidget(edit=self._empty_str, + text=ba.Lstr(resource=self._r + '.emptyText')) + ba.scrollwidget(edit=self._scrollwidget, + size=(self._width - 50, + self._height - top_section_height - 110), + position=(30, 80)) + else: + columns = 1 if len( + self._roster) == 1 else 2 if len(self._roster) == 2 else 3 + rows = int(math.ceil(float(len(self._roster)) / columns)) + c_width = (self._width * 0.9) / max(3, columns) + c_width_total = c_width * columns + c_height = 24 + c_height_total = c_height * rows + for y in range(rows): + for x in range(columns): + index = y * columns + x + if index < len(self._roster): + t_scale = 0.65 + pos = (self._width * 0.53 - c_width_total * 0.5 + + c_width * x - 23, + self._height - 65 - c_height * y - 15) + + # if there are players present for this client, use + # their names as a display string instead of the + # client spec-string + try: + if self._roster[index]['players']: + # if there's just one, use the full name; + # otherwise combine short names + if len(self._roster[index] + ['players']) == 1: + p_str = self._roster[index]['players'][ + 0]['name_full'] + else: + p_str = ('/'.join([ + entry['name'] for entry in + self._roster[index]['players'] + ])) + if len(p_str) > 25: + p_str = p_str[:25] + '...' + else: + p_str = self._roster[index][ + 'display_string'] + except Exception: + ba.print_exception( + 'Error calcing client name str.') + p_str = '???' + widget = ba.textwidget(parent=self._root_widget, + position=(pos[0], pos[1]), + scale=t_scale, + size=(c_width * 0.85, 30), + maxwidth=c_width * 0.85, + color=(1, 1, + 1) if index == 0 else + (1, 1, 1), + selectable=True, + autoselect=True, + click_activate=True, + text=ba.Lstr(value=p_str), + h_align='left', + v_align='center') + self._name_widgets.append(widget) + + # in newer versions client_id will be present and + # we can use that to determine who the host is. + # in older versions we assume the first client is + # host + if self._roster[index]['client_id'] is not None: + is_host = self._roster[index][ + 'client_id'] == -1 + else: + is_host = (index == 0) + + # FIXME: Should pass client_id to these sort of + # calls; not spec-string (perhaps should wait till + # client_id is more readily available though). + ba.textwidget(edit=widget, + on_activate_call=ba.Call( + self._on_party_member_press, + self._roster[index]['client_id'], + is_host, widget)) + pos = (self._width * 0.53 - c_width_total * 0.5 + + c_width * x, + self._height - 65 - c_height * y) + + # Make the assumption that the first roster + # entry is the server. + # FIXME: Shouldn't do this. + if is_host: + twd = min( + c_width * 0.85, + _ba.get_string_width( + p_str, suppress_warning=True) * + t_scale) + self._name_widgets.append( + ba.textwidget( + parent=self._root_widget, + position=(pos[0] + twd + 1, + pos[1] - 0.5), + size=(0, 0), + h_align='left', + v_align='center', + maxwidth=c_width * 0.96 - twd, + color=(0.1, 1, 0.1, 0.5), + text=ba.Lstr(resource=self._r + + '.hostText'), + scale=0.4, + shadow=0.1, + flatness=1.0)) + ba.textwidget(edit=self._empty_str, text='') + ba.scrollwidget(edit=self._scrollwidget, + size=(self._width - 50, + max(100, self._height - 139 - + c_height_total)), + position=(30, 80)) + else: + _ba.set_party_window_open(False) + for widget in self._name_widgets: + widget.delete() + self._name_widgets = [] + ba.textwidget(edit=self._title_text, text='Private Chat') + ba.textwidget(edit=self._empty_str, text='') + if self._firstcall: + self._firstcall = False + if hasattr(self, '_status_text'): + self._status_text.delete() + try: + msgs = messenger.pvt_msgs[messenger.filter] + except: + msgs = [] + if self._chat_texts: + while self._chat_texts: + first = self._chat_texts.pop() + first.delete() + uiscale = ba.app.ui.uiscale + scroll_height = (165 if uiscale is ba.UIScale.SMALL else + 280 if uiscale is ba.UIScale.MEDIUM else 400) + ba.scrollwidget(edit=self._scrollwidget, + size=(self._width - 50, scroll_height)) + for msg in msgs: + message = messenger._format_message(msg) + self._add_msg(message, msg['sent']) + self._filter_text = ba.textwidget(parent=self._root_widget, + scale=0.6, + color=(0.9, 1.0, 0.9), + text='Filter: ', + size=(0, 0), + position=(self._width * 0.3, + self._height - 70), + h_align='center', + v_align='center') + choices = [i for i in messenger.saved_ids] + choices_display = [ba.Lstr(value=messenger.saved_ids[i]) + for i in messenger.saved_ids] + choices.append('add') + choices_display.append(ba.Lstr(value='***Add New***')) + filter_widget = PopupMenu( + parent=self._root_widget, + position=(self._width * 0.4, + self._height - 80), + width=200, + scale=(2.8 if uiscale is ba.UIScale.SMALL else + 1.8 if uiscale is ba.UIScale.MEDIUM else 1.2), + choices=choices, + choices_display=choices_display, + current_choice=messenger.filter, + button_size=(120, 30), + on_value_change_call=self._change_filter) + self._popup_button = filter_widget.get_button() + if messenger.filter != 'all': + user_status = messenger._get_status(messenger.filter) + if user_status == 'Offline': + color = (1, 0, 0) + elif user_status.startswith(('Playing in', 'in Lobby')): + color = (0, 1, 0) + else: + color = (0.9, 1.0, 0.9) + self._status_text = ba.textwidget(parent=self._root_widget, + scale=0.5, + color=color, + text=f'Status:\t{user_status}', + size=(200, 30), + position=(self._width * 0.3, + self._height - 110), + h_align='center', + v_align='center', + autoselect=True, + selectable=True, + click_activate=True) + ba.textwidget(edit=self._status_text, + on_activate_call=ba.Call(messenger._get_status, messenger.filter, 'last_seen')) + + def _change_filter(self, choice): + if choice == 'add': + self.close() + AddNewIdWindow() + else: + messenger.filter = choice + self._firstcall = True + self._filter_text.delete() + self._popup_button.delete() + if self._chat_texts: + while self._chat_texts: + first = self._chat_texts.pop() + first.delete() + self._update() + + def popup_menu_selected_choice(self, popup_window: PopupMenuWindow, + choice: str) -> None: + """Called when a choice is selected in the popup.""" + if self._popup_type == 'partyMemberPress': + playerinfo = self._get_player_info(self._popup_party_member_client_id) + if choice == 'kick': + name = playerinfo['ds'] + ConfirmWindow(text=f'Are you sure to kick {name}?', + action=self._vote_kick_player, + cancel_button=True, + cancel_is_selected=True, + color=self.bg_color, + text_scale=1.0, + origin_widget=self.get_root_widget()) + elif choice == 'mention': + players = playerinfo['players'] + choices = [] + namelist = [playerinfo['ds']] + for player in players: + name = player['name_full'] + if name not in namelist: + namelist.append(name) + choices_display = self._create_baLstr_list(namelist) + for i in namelist: + i = i.replace('"', '\"') + i = i.replace("'", "\'") + choices.append(f'self._edit_text_msg_box("{i}")') + PopupMenuWindow(position=popup_window.root_widget.get_screen_space_center(), + color=self.bg_color, + scale=self._get_popup_window_scale(), + choices=choices, + choices_display=choices_display, + current_choice=choices[0], + delegate=self) + self._popup_type = "executeChoice" + elif choice == 'adminkick': + name = playerinfo['ds'] + ConfirmWindow(text=f'Are you sure to use admin\ncommand to kick {name}', + action=self._send_admin_kick_command, + cancel_button=True, + cancel_is_selected=True, + color=self.bg_color, + text_scale=1.0, + origin_widget=self.get_root_widget()) + elif choice == 'customCommands': + choices = [] + choices_display = [] + playerinfo = self._get_player_info(self._popup_party_member_client_id) + account = playerinfo['ds'] + try: + name = playerinfo['players'][0]['name_full'] + except: + name = account + for i in ba.app.config.get('Custom Commands'): + i = i.replace('$c', str(self._popup_party_member_client_id)) + i = i.replace('$a', str(account)) + i = i.replace('$n', str(name)) + if ba.app.config['Direct Send']: + choices.append(f'_ba.chatmessage("{i}")') + else: + choices.append(f'self._edit_text_msg_box("{i}")') + choices_display.append(ba.Lstr(value=i)) + choices.append('AddNewChoiceWindow()') + choices_display.append(ba.Lstr(value='***Add New***')) + PopupMenuWindow(position=popup_window.root_widget.get_screen_space_center(), + color=self.bg_color, + scale=self._get_popup_window_scale(), + choices=choices, + choices_display=choices_display, + current_choice=choices[0], + delegate=self) + self._popup_type = 'executeChoice' + + elif choice == 'addNew': + AddNewChoiceWindow() + + elif self._popup_type == 'menu': + if choice == 'muteOption': + current_choice = self._get_current_mute_type() + PopupMenuWindow( + position=(self._width - 60, self._height - 47), + color=self.bg_color, + scale=self._get_popup_window_scale(), + choices=['muteInGameOnly', 'mutePartyWindowOnly', 'muteAll', 'unmuteAll'], + choices_display=self._create_baLstr_list( + ['Mute In Game Messages Only', 'Mute Party Window Messages Only', 'Mute all', 'Unmute All']), + current_choice=current_choice, + delegate=self + ) + self._popup_type = 'muteType' + elif choice == 'modifyColor': + ColorPickerExact(parent=self.get_root_widget(), + position=self.get_root_widget().get_screen_space_center(), + initial_color=self.bg_color, + delegate=self, tag='') + elif choice == 'addQuickReply': + try: + newReply = ba.textwidget(query=self._text_field) + oldReplies = self._get_quick_responds() + oldReplies.append(newReply) + self._write_quick_responds(oldReplies) + ba.screenmessage(f'"{newReply}" is added.', (0, 1, 0)) + ba.playsound(ba.getsound('dingSmallHigh')) + except: + ba.print_exception() + elif choice == 'removeQuickReply': + quick_reply = self._get_quick_responds() + PopupMenuWindow(position=self._send_button.get_screen_space_center(), + color=self.bg_color, + scale=self._get_popup_window_scale(), + choices=quick_reply, + choices_display=self._create_baLstr_list(quick_reply), + current_choice=quick_reply[0], + delegate=self) + self._popup_type = 'removeQuickReplySelect' + elif choice == 'credits': + ConfirmWindow( + text=u'\ue043Party Window Reloaded V3\ue043\n\nCredits - Droopy#3730\nSpecial Thanks - BoTT-Vishah#4150', + action=self.join_discord, + width=420, + height=230, + color=self.bg_color, + text_scale=1.0, + ok_text="Join Discord", + origin_widget=self.get_root_widget()) + elif choice == 'manualCamera': + ba.containerwidget(edit=self._root_widget, transition='out_scale') + Manual_camera_window() + + elif self._popup_type == 'muteType': + self._change_mute_type(choice) + + elif self._popup_type == 'executeChoice': + exec(choice) + + elif self._popup_type == 'quickMessage': + if choice == '*** EDIT ORDER ***': + SortQuickMessages() + else: + self._edit_text_msg_box(choice) + + elif self._popup_type == 'removeQuickReplySelect': + data = self._get_quick_responds() + data.remove(choice) + self._write_quick_responds(data) + ba.screenmessage(f'"{choice}" is removed.', (1, 0, 0)) + ba.playsound(ba.getsound('shieldDown')) + + else: + print(f'unhandled popup type: {self._popup_type}') + del popup_window # unused + + def _vote_kick_player(self): + if self._popup_party_member_is_host: + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource='internal.cantKickHostError'), + color=(1, 0, 0)) + else: + assert self._popup_party_member_client_id is not None + + # Ban for 5 minutes. + result = _ba.disconnect_client( + self._popup_party_member_client_id, ban_time=5 * 60) + if not result: + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource='getTicketsWindow.unavailableText'), + color=(1, 0, 0)) + + def _send_admin_kick_command(self): + _ba.chatmessage('/kick ' + str(self._popup_party_member_client_id)) + + def _translate(self): + def _apply_translation(translated): + if self._text_field.exists(): + ba.textwidget(edit=self._text_field, text=translated) + msg = ba.textwidget(query=self._text_field) + cfg = ba.app.config + if msg == '': + ba.screenmessage('Nothing to translate.', (1, 0, 0)) + ba.playsound(ba.getsound('error')) + else: + data = dict(message=msg) + if cfg['Translate Source Language']: + data['src'] = cfg['Translate Source Language'] + if cfg['Translate Destination Language']: + data['dest'] = cfg['Translate Destination Language'] + if cfg['Pronunciation']: + data['type'] = 'pronunciation' + Translate(data, _apply_translation).start() + + def _copy_to_clipboard(self): + msg = ba.textwidget(query=self._text_field) + if msg == '': + ba.screenmessage('Nothing to copy.', (1, 0, 0)) + ba.playsound(ba.getsound('error')) + else: + ba.clipboard_set_text(msg) + ba.screenmessage(f'"{msg}" is copied to clipboard.', (0, 1, 0)) + ba.playsound(ba.getsound('dingSmallHigh')) + + def _get_current_mute_type(self): + cfg = ba.app.config + if cfg['Chat Muted'] == True: + if cfg['Party Chat Muted'] == True: + return 'muteAll' + else: + return 'muteInGameOnly' + else: + if cfg['Party Chat Muted'] == True: + return 'mutePartyWindowOnly' + else: + return 'unmuteAll' + + def _change_mute_type(self, choice): + cfg = ba.app.config + if choice == 'muteInGameOnly': + cfg['Chat Muted'] = True + cfg['Party Chat Muted'] = False + elif choice == 'mutePartyWindowOnly': + cfg['Chat Muted'] = False + cfg['Party Chat Muted'] = True + elif choice == 'muteAll': + cfg['Chat Muted'] = True + cfg['Party Chat Muted'] = True + else: + cfg['Chat Muted'] = False + cfg['Party Chat Muted'] = False + cfg.apply_and_commit() + self._update() + + def popup_menu_closing(self, popup_window: PopupWindow) -> None: + """Called when the popup is closing.""" + + def _on_party_member_press(self, client_id: int, is_host: bool, + widget: ba.Widget) -> None: + # if we're the host, pop up 'kick' options for all non-host members + if _ba.get_foreground_host_session() is not None: + kick_str = ba.Lstr(resource='kickText') + else: + # kick-votes appeared in build 14248 + if (_ba.get_connection_to_host_info().get('build_number', 0) < + 14248): + return + kick_str = ba.Lstr(resource='kickVoteText') + uiscale = ba.app.ui.uiscale + choices = ['kick', 'mention', 'adminkick'] + choices_display = [kick_str] + \ + list(self._create_baLstr_list(['Mention this guy', f'Kick ID: {client_id}'])) + choices.append('customCommands') + choices_display.append(ba.Lstr(value='Custom Commands')) + PopupMenuWindow( + position=widget.get_screen_space_center(), + color=self.bg_color, + scale=(2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23), + choices=choices, + choices_display=choices_display, + current_choice='mention', + delegate=self) + self._popup_type = 'partyMemberPress' + self._popup_party_member_client_id = client_id + self._popup_party_member_is_host = is_host + + def _send_chat_message(self) -> None: + msg = ba.textwidget(query=self._text_field) + ba.textwidget(edit=self._text_field, text='') + if '\\' in msg: + msg = msg.replace('\\d', ('\ue048')) + msg = msg.replace('\\c', ('\ue043')) + msg = msg.replace('\\h', ('\ue049')) + msg = msg.replace('\\s', ('\ue046')) + msg = msg.replace('\\n', ('\ue04b')) + msg = msg.replace('\\f', ('\ue04f')) + msg = msg.replace('\\g', ('\ue027')) + msg = msg.replace('\\i', ('\ue03a')) + msg = msg.replace('\\m', ('\ue04d')) + msg = msg.replace('\\t', ('\ue01f')) + msg = msg.replace('\\bs', ('\ue01e')) + msg = msg.replace('\\j', ('\ue010')) + msg = msg.replace('\\e', ('\ue045')) + msg = msg.replace('\\l', ('\ue047')) + msg = msg.replace('\\a', ('\ue020')) + msg = msg.replace('\\b', ('\ue00c')) + if not msg: + choices = self._get_quick_responds() + choices.append('*** EDIT ORDER ***') + PopupMenuWindow(position=self._send_button.get_screen_space_center(), + scale=self._get_popup_window_scale(), + color=self.bg_color, + choices=choices, + current_choice=choices[0], + delegate=self) + self._popup_type = 'quickMessage' + return + elif msg.startswith('/info '): + account = msg.replace('/info ', '') + if account: + from bastd.ui.account import viewer + viewer.AccountViewerWindow( + account_id=account) + ba.textwidget(edit=self._text_field, text='') + return + if not self._private_chat: + if msg == '/id': + myid = ba.internal.get_v1_account_misc_read_val_2('resolvedAccountID', '') + _ba.chatmessage(f"My Unique ID : {myid}") + elif msg == '/save': + info = _ba.get_connection_to_host_info() + config = ba.app.config + if info.get('name', '') != '': + title = info['name'] + if not isinstance(config.get('Saved Servers'), dict): + config['Saved Servers'] = {} + config['Saved Servers'][f'{_ip}@{_port}'] = { + 'addr': _ip, + 'port': _port, + 'name': title + } + config.commit() + ba.screenmessage("Server Added To Manual", color=(0, 1, 0), transient=True) + ba.playsound(ba.getsound('gunCocking')) + elif msg != '': + _ba.chatmessage(cast(str, msg)) + else: + receiver = messenger.filter + name = ba.internal.get_v1_account_display_string() + if not receiver: + display_error('Choose a valid receiver id') + return + data = {'receiver': receiver, 'message': f'{name}: {msg}'} + if msg.startswith('/rename '): + if messenger.filter != 'all': + nickname = msg.replace('/rename ', '') + messenger._save_id(messenger.filter, nickname, verify=False) + self._change_filter(messenger.filter) + elif msg == '/remove': + if messenger.filter != 'all': + messenger._remove_id(messenger.filter) + self._change_filter('all') + else: + display_error('Cant delete this') + ba.textwidget(edit=self._text_field, text='') + return + ba.Call(messenger._send_request, url, data) + ba.Call(check_new_message) + Thread(target=messenger._send_request, args=(url, data)).start() + Thread(target=check_new_message).start() + ba.textwidget(edit=self._text_field, text='') + + def _write_quick_responds(self, data): + try: + with open(quick_msg_file, 'w') as f: + f.write('\n'.join(data)) + except: + ba.print_exception() + ba.screenmessage('Error!', (1, 0, 0)) + ba.playsound(ba.getsound('error')) + + def _get_quick_responds(self): + if os.path.exists(quick_msg_file): + with open(quick_msg_file, 'r') as f: + return f.read().split('\n') + else: + default_replies = ['What the hell?', 'Dude that\'s amazing!'] + self._write_quick_responds(default_replies) + return default_replies + + def color_picker_selected_color(self, picker, color) -> None: + ba.containerwidget(edit=self._root_widget, color=color) + color = tuple(round(i, 2) for i in color) + self.bg_color = color + ba.app.config['PartyWindow Main Color'] = color + + def color_picker_closing(self, picker) -> None: + ba.app.config.apply_and_commit() + + def _remove_sender_from_message(self, msg=''): + msg_start = msg.find(": ") + 2 + return msg[msg_start:] + + def _previous_message(self): + msgs = self._chat_texts + if not hasattr(self, 'msg_index'): + self.msg_index = len(msgs) - 1 + else: + if self.msg_index > 0: + self.msg_index -= 1 + else: + del self.msg_index + try: + msg_widget = msgs[self.msg_index] + msg = ba.textwidget(query=msg_widget) + msg = self._remove_sender_from_message(msg) + if msg in ('', ' '): + self._previous_message() + return + except: + msg = '' + self._edit_text_msg_box(msg, 'replace') + + def _next_message(self): + msgs = self._chat_texts + if not hasattr(self, 'msg_index'): + self.msg_index = 0 + else: + if self.msg_index < len(msgs) - 1: + self.msg_index += 1 + else: + del self.msg_index + try: + msg_widget = msgs[self.msg_index] + msg = ba.textwidget(query=msg_widget) + msg = self._remove_sender_from_message(msg) + if msg in ('', ' '): + self._next_message() + return + except: + msg = '' + self._edit_text_msg_box(msg, 'replace') + + def _ip_port_msg(self): + try: + msg = f'IP : {_ip} PORT : {_port}' + except: + msg = '' + self._edit_text_msg_box(msg, 'replace') + + def ping_server(self): + info = _ba.get_connection_to_host_info() + if info.get('name', '') != '': + self.pingThread = PingThread(_ip, _port) + self.pingThread.start() + + def _get_ping_color(self): + try: + if _ping < 100: + return (0, 1, 0) + elif _ping < 500: + return (1, 1, 0) + else: + return (1, 0, 0) + except: + return (0.1, 0.1, 0.1) + + def _send_ping(self): + if isinstance(_ping, int): + _ba.chatmessage(f'My ping = {_ping}ms') + + def close(self) -> None: + """Close the window.""" + ba.containerwidget(edit=self._root_widget, transition='out_scale') + + def close_with_sound(self) -> None: + """Close the window and make a lovely sound.""" + ba.playsound(ba.getsound('swish')) + self.close() + + def _get_popup_window_scale(self) -> float: + uiscale = ba.app.ui.uiscale + return (2.4 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0) + + def _create_baLstr_list(self, list1): + return (ba.Lstr(value=i) for i in list1) + + def _get_player_info(self, clientID): + info = {} + for i in _ba.get_game_roster(): + if i['client_id'] == clientID: + info['ds'] = i['display_string'] + info['players'] = i['players'] + info['aid'] = i['account_id'] + break + return info + + def _edit_text_msg_box(self, text, action='add'): + if isinstance(text, str): + if action == 'add': + ba.textwidget(edit=self._text_field, text=ba.textwidget( + query=self._text_field) + text) + elif action == 'replace': + ba.textwidget(edit=self._text_field, text=text) + + def _on_setting_button_press(self): + try: + SettingsWindow() + except Exception as e: + ba.print_exception() + pass + + def _on_privatechat_button_press(self): + try: + if messenger.logged_in: + self._firstcall = True + if self._chat_texts: + while self._chat_texts: + first = self._chat_texts.pop() + first.delete() + if not self._private_chat: + self._private_chat = True + else: + self._filter_text.delete() + self._popup_button.delete() + self._private_chat = False + self._update() + else: + if messenger.server_online: + if not messenger._cookie_login(): + if messenger._query(): + LoginWindow(wtype='login') + else: + LoginWindow(wtype='signup') + else: + display_error(messenger.error) + except Exception as e: + ba.print_exception() + pass + + def join_discord(self): + ba.open_url("https://discord.gg/KvYgpEg2JR") + + +class LoginWindow: + def __init__(self, wtype): + self.wtype = wtype + if self.wtype == 'signup': + title = 'Sign Up Window' + label = 'Sign Up' + else: + title = 'Login Window' + label = 'Log In' + uiscale = ba.app.ui.uiscale + bg_color = ba.app.config.get('PartyWindow Main Color', (0.5, 0.5, 0.5)) + self._root_widget = ba.containerwidget(size=(500, 250), + transition='in_scale', + color=bg_color, + toolbar_visibility='menu_minimal_no_back', + parent=_ba.get_special_widget('overlay_stack'), + on_outside_click_call=self._close, + scale=(2.1 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else ( + 240, 0) if uiscale is ba.UIScale.MEDIUM else (330, 20)) + self._title_text = ba.textwidget(parent=self._root_widget, + scale=0.8, + color=(1, 1, 1), + text=title, + size=(0, 0), + position=(250, 200), + h_align='center', + v_align='center') + self._id = ba.textwidget(parent=self._root_widget, + scale=0.5, + color=(1, 1, 1), + text=f'Account: ' + + ba.internal.get_v1_account_misc_read_val_2( + 'resolvedAccountID', ''), + size=(0, 0), + position=(220, 170), + h_align='center', + v_align='center') + self._registrationkey_text = ba.textwidget(parent=self._root_widget, + scale=0.5, + color=(1, 1, 1), + text=f'Registration Key:', + size=(0, 0), + position=(100, 140), + h_align='center', + v_align='center') + self._text_field = ba.textwidget( + parent=self._root_widget, + editable=True, + size=(200, 40), + position=(175, 130), + text='', + maxwidth=410, + flatness=1.0, + autoselect=True, + v_align='center', + corner_scale=0.7) + self._connect_button = ba.buttonwidget(parent=self._root_widget, + size=(150, 30), + color=(0, 1, 0), + label='Get Registration Key', + button_type='square', + autoselect=True, + position=(150, 80), + on_activate_call=self._connect) + self._confirm_button = ba.buttonwidget(parent=self._root_widget, + size=(50, 30), + label=label, + button_type='square', + autoselect=True, + position=(200, 40), + on_activate_call=self._confirmcall) + ba.textwidget(edit=self._text_field, on_return_press_call=self._confirm_button.activate) + + def _close(self): + ba.containerwidget(edit=self._root_widget, + transition=('out_scale')) + + def _connect(self): + try: + host = url.split('http://')[1].split(':')[0] + import socket + address = socket.gethostbyname(host) + _ba.disconnect_from_host() + _ba.connect_to_party(address, port=11111) + except Exception: + display_error('Cant get ip from hostname') + + def _confirmcall(self): + if self.wtype == 'signup': + key = ba.textwidget(query=self._text_field) + answer = messenger._signup(registration_key=key) if key else None + if answer: + self._close() + else: + if messenger._login(registration_key=ba.textwidget(query=self._text_field)): + self._close() + + +class AddNewIdWindow: + def __init__(self): + uiscale = ba.app.ui.uiscale + bg_color = ba.app.config.get('PartyWindow Main Color', (0.5, 0.5, 0.5)) + self._root_widget = ba.containerwidget(size=(500, 250), + transition='in_scale', + color=bg_color, + toolbar_visibility='menu_minimal_no_back', + parent=_ba.get_special_widget('overlay_stack'), + on_outside_click_call=self._close, + scale=(2.1 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0)) + self._title_text = ba.textwidget(parent=self._root_widget, + scale=0.8, + color=(1, 1, 1), + text='Add New ID', + size=(0, 0), + position=(250, 200), + h_align='center', + v_align='center') + self._accountid_text = ba.textwidget(parent=self._root_widget, + scale=0.6, + color=(1, 1, 1), + text='pb-id: ', + size=(0, 0), + position=(50, 155), + h_align='center', + v_align='center') + self._accountid_field = ba.textwidget( + parent=self._root_widget, + editable=True, + size=(250, 40), + position=(100, 140), + text='', + maxwidth=410, + flatness=1.0, + autoselect=True, + v_align='center', + corner_scale=0.7) + self._nickname_text = ba.textwidget(parent=self._root_widget, + scale=0.5, + color=(1, 1, 1), + text='Nickname: ', + size=(0, 0), + position=(50, 115), + h_align='center', + v_align='center') + self._nickname_field = ba.textwidget( + parent=self._root_widget, + editable=True, + size=(250, 40), + position=(100, 100), + text='', + maxwidth=410, + flatness=1.0, + autoselect=True, + v_align='center', + corner_scale=0.7) + self._help_text = ba.textwidget(parent=self._root_widget, + scale=0.4, + color=(0.1, 0.9, 0.9), + text='Help:\nEnter pb-id of account you\n want to chat to\nEnter nickname of id to\n recognize id easily\nLeave nickname \n to use their default name', + size=(0, 0), + position=(325, 120), + h_align='left', + v_align='center') + self._add = ba.buttonwidget(parent=self._root_widget, + size=(50, 30), + label='Add', + button_type='square', + autoselect=True, + position=(100, 50), + on_activate_call=ba.Call(self._relay_function)) + ba.textwidget(edit=self._accountid_field, on_return_press_call=self._add.activate) + self._remove = ba.buttonwidget(parent=self._root_widget, + size=(75, 30), + label='Remove', + button_type='square', + autoselect=True, + position=(170, 50), + on_activate_call=self._remove_id) + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._close) + + def _relay_function(self): + account_id = ba.textwidget(query=self._accountid_field) + nickname = ba.textwidget(query=self._nickname_field) + try: + if messenger._save_id(account_id, nickname): + self._close() + except: + display_error('Enter valid pb-id') + + def _remove_id(self): + uiscale = ba.app.ui.uiscale + if len(messenger.saved_ids) > 1: + choices = [i for i in messenger.saved_ids] + choices.remove('all') + choices_display = [ba.Lstr(value=messenger.saved_ids[i]) for i in choices] + PopupMenuWindow(position=self._remove.get_screen_space_center(), + color=ba.app.config.get('PartyWindow Main Color', (0.5, 0.5, 0.5)), + scale=(2.4 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + choices=choices, + choices_display=choices_display, + current_choice=choices[0], + delegate=self) + self._popup_type = 'removeSelectedID' + + def popup_menu_selected_choice(self, popup_window: PopupMenuWindow, + choice: str) -> None: + """Called when a choice is selected in the popup.""" + if self._popup_type == 'removeSelectedID': + messenger._remove_id(choice) + self._close() + + def popup_menu_closing(self, popup_window: PopupWindow) -> None: + """Called when the popup is closing.""" + + def _close(self): + ba.containerwidget(edit=self._root_widget, + transition=('out_scale')) + + +class AddNewChoiceWindow: + def __init__(self): + uiscale = ba.app.ui.uiscale + bg_color = ba.app.config.get('PartyWindow Main Color', (0.5, 0.5, 0.5)) + self._root_widget = ba.containerwidget(size=(500, 250), + transition='in_scale', + color=bg_color, + toolbar_visibility='menu_minimal_no_back', + parent=_ba.get_special_widget('overlay_stack'), + on_outside_click_call=self._close, + scale=(2.1 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else ( + 240, 0) if uiscale is ba.UIScale.MEDIUM else (330, 20)) + self._title_text = ba.textwidget(parent=self._root_widget, + scale=0.8, + color=(1, 1, 1), + text='Add Custom Command', + size=(0, 0), + position=(250, 200), + h_align='center', + v_align='center') + self._text_field = ba.textwidget( + parent=self._root_widget, + editable=True, + size=(500, 40), + position=(75, 140), + text='', + maxwidth=410, + flatness=1.0, + autoselect=True, + v_align='center', + corner_scale=0.7) + self._help_text = ba.textwidget(parent=self._root_widget, + scale=0.4, + color=(0.2, 0.2, 0.2), + text='Use\n$c = client id\n$a = account id\n$n = name', + size=(0, 0), + position=(70, 75), + h_align='left', + v_align='center') + self._add = ba.buttonwidget(parent=self._root_widget, + size=(50, 30), + label='Add', + button_type='square', + autoselect=True, + position=(150, 50), + on_activate_call=self._add_choice) + ba.textwidget(edit=self._text_field, on_return_press_call=self._add.activate) + self._remove = ba.buttonwidget(parent=self._root_widget, + size=(50, 30), + label='Remove', + button_type='square', + autoselect=True, + position=(350, 50), + on_activate_call=self._remove_custom_command) + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._close) + + def _add_choice(self): + newCommand = ba.textwidget(query=self._text_field) + cfg = ba.app.config + if any(i in newCommand for i in ('$c', '$a', '$n')): + cfg['Custom Commands'].append(newCommand) + cfg.apply_and_commit() + ba.screenmessage('Added successfully', (0, 1, 0)) + ba.playsound(ba.getsound('dingSmallHigh')) + self._close() + else: + ba.screenmessage('Use at least of these ($c, $a, $n)', (1, 0, 0)) + ba.playsound(ba.getsound('error')) + + def _remove_custom_command(self): + uiscale = ba.app.ui.uiscale + commands = ba.app.config['Custom Commands'] + PopupMenuWindow(position=self._remove.get_screen_space_center(), + color=ba.app.config.get('PartyWindow Main Color', (0.5, 0.5, 0.5)), + scale=(2.4 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + choices=commands, + current_choice=commands[0], + delegate=self) + self._popup_type = 'removeCustomCommandSelect' + + def popup_menu_selected_choice(self, popup_window: PopupMenuWindow, + choice: str) -> None: + """Called when a choice is selected in the popup.""" + if self._popup_type == 'removeCustomCommandSelect': + config = ba.app.config + config['Custom Commands'].remove(choice) + config.apply_and_commit() + ba.screenmessage('Removed successfully', (0, 1, 0)) + ba.playsound(ba.getsound('shieldDown')) + + def popup_menu_closing(self, popup_window: PopupWindow) -> None: + """Called when the popup is closing.""" + + def _close(self): + ba.containerwidget(edit=self._root_widget, + transition=('out_scale')) + + +class Manual_camera_window: + def __init__(self): + self._root_widget = ba.containerwidget( + on_outside_click_call=None, + size=(0, 0)) + button_size = (30, 30) + self._title_text = ba.textwidget(parent=self._root_widget, + scale=0.9, + color=(1, 1, 1), + text='Manual Camera Setup', + size=(0, 0), + position=(130, 153), + h_align='center', + v_align='center') + self._xminus = ba.buttonwidget(parent=self._root_widget, + size=button_size, + label=ba.charstr(ba.SpecialChar.LEFT_ARROW), + button_type='square', + autoselect=True, + position=(1, 60), + on_activate_call=ba.Call(self._change_camera_position, 'x-')) + self._xplus = ba.buttonwidget(parent=self._root_widget, + size=button_size, + label=ba.charstr(ba.SpecialChar.RIGHT_ARROW), + button_type='square', + autoselect=True, + position=(60, 60), + on_activate_call=ba.Call(self._change_camera_position, 'x')) + self._yplus = ba.buttonwidget(parent=self._root_widget, + size=button_size, + label=ba.charstr(ba.SpecialChar.UP_ARROW), + button_type='square', + autoselect=True, + position=(30, 100), + on_activate_call=ba.Call(self._change_camera_position, 'y')) + self._yminus = ba.buttonwidget(parent=self._root_widget, + size=button_size, + label=ba.charstr(ba.SpecialChar.DOWN_ARROW), + button_type='square', + autoselect=True, + position=(30, 20), + on_activate_call=ba.Call(self._change_camera_position, 'y-')) + self.inwards = ba.buttonwidget(parent=self._root_widget, + size=(100, 30), + label='INWARDS', + button_type='square', + autoselect=True, + position=(120, 90), + on_activate_call=ba.Call(self._change_camera_position, 'z-')) + self._outwards = ba.buttonwidget(parent=self._root_widget, + size=(100, 30), + label='OUTWARDS', + button_type='square', + autoselect=True, + position=(120, 50), + on_activate_call=ba.Call(self._change_camera_position, 'z')) + self._step_text = ba.textwidget(parent=self._root_widget, + scale=0.5, + color=(1, 1, 1), + text='Step:', + size=(0, 0), + position=(1, -20), + h_align='center', + v_align='center') + self._text_field = ba.textwidget( + parent=self._root_widget, + editable=True, + size=(100, 40), + position=(26, -35), + text='', + maxwidth=120, + flatness=1.0, + autoselect=True, + v_align='center', + corner_scale=0.7) + self._reset = ba.buttonwidget(parent=self._root_widget, + size=(50, 30), + label='Reset', + button_type='square', + autoselect=True, + position=(120, -35), + on_activate_call=ba.Call(self._change_camera_position, 'reset')) + self._done = ba.buttonwidget(parent=self._root_widget, + size=(50, 30), + label='Done', + button_type='square', + autoselect=True, + position=(180, -35), + on_activate_call=self._close) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._done) + + def _close(self): + ba.containerwidget(edit=self._root_widget, + transition=('out_scale')) + + def _change_camera_position(self, direction): + activity = _ba.get_foreground_host_activity() + node = activity.globalsnode + aoi = list(node.area_of_interest_bounds) + center = [(aoi[0] + aoi[3]) / 2, + (aoi[1] + aoi[4]) / 2, + (aoi[2] + aoi[5]) / 2] + size = (aoi[3] - aoi[0], + aoi[4] - aoi[1], + aoi[5] - aoi[2]) + + try: + increment = float(ba.textwidget(query=self._text_field)) + except: + # ba.print_exception() + increment = 1 + + if direction == 'x': + center[0] += increment + elif direction == 'x-': + center[0] -= increment + elif direction == 'y': + center[1] += increment + elif direction == 'y-': + center[1] -= increment + elif direction == 'z': + center[2] += increment + elif direction == 'z-': + center[2] -= increment + elif direction == 'reset': + node.area_of_interest_bounds = activity._map.get_def_bound_box( + 'area_of_interest_bounds') + return + + aoi = (center[0] - size[0] / 2, + center[1] - size[1] / 2, + center[2] - size[2] / 2, + center[0] + size[0] / 2, + center[1] + size[1] / 2, + center[2] + size[2] / 2) + node.area_of_interest_bounds = tuple(aoi) + + +def __popup_menu_window_init__(self, + position: Tuple[float, float], + choices: Sequence[str], + current_choice: str, + delegate: Any = None, + width: float = 230.0, + maxwidth: float = None, + scale: float = 1.0, + color: Tuple[float, float, float] = (0.35, 0.55, 0.15), + choices_disabled: Sequence[str] = None, + choices_display: Sequence[ba.Lstr] = None): + # FIXME: Clean up a bit. + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + if choices_disabled is None: + choices_disabled = [] + if choices_display is None: + choices_display = [] + + # FIXME: For the moment we base our width on these strings so + # we need to flatten them. + choices_display_fin: List[str] = [] + for choice_display in choices_display: + choices_display_fin.append(choice_display.evaluate()) + + if maxwidth is None: + maxwidth = width * 1.5 + + self._transitioning_out = False + self._choices = list(choices) + self._choices_display = list(choices_display_fin) + self._current_choice = current_choice + self._color = color + self._choices_disabled = list(choices_disabled) + self._done_building = False + if not choices: + raise TypeError('Must pass at least one choice') + self._width = width + self._scale = scale + if len(choices) > 8: + self._height = 280 + self._use_scroll = True + else: + self._height = 20 + len(choices) * 33 + self._use_scroll = False + self._delegate = None # don't want this stuff called just yet.. + + # extend width to fit our longest string (or our max-width) + for index, choice in enumerate(choices): + if len(choices_display_fin) == len(choices): + choice_display_name = choices_display_fin[index] + else: + choice_display_name = choice + if self._use_scroll: + self._width = max( + self._width, + min( + maxwidth, + _ba.get_string_width(choice_display_name, + suppress_warning=True)) + 75) + else: + self._width = max( + self._width, + min( + maxwidth, + _ba.get_string_width(choice_display_name, + suppress_warning=True)) + 60) + + # init parent class - this will rescale and reposition things as + # needed and create our root widget + PopupWindow.__init__(self, + position, + size=(self._width, self._height), + bg_color=self._color, + scale=self._scale) + + if self._use_scroll: + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + position=(20, 20), + highlight=False, + color=(0.35, 0.55, 0.15), + size=(self._width - 40, + self._height - 40)) + self._columnwidget = ba.columnwidget(parent=self._scrollwidget, + border=2, + margin=0) + else: + self._offset_widget = ba.containerwidget(parent=self.root_widget, + position=(30, 15), + size=(self._width - 40, + self._height), + background=False) + self._columnwidget = ba.columnwidget(parent=self._offset_widget, + border=2, + margin=0) + for index, choice in enumerate(choices): + if len(choices_display_fin) == len(choices): + choice_display_name = choices_display_fin[index] + else: + choice_display_name = choice + inactive = (choice in self._choices_disabled) + wdg = ba.textwidget(parent=self._columnwidget, + size=(self._width - 40, 28), + on_select_call=ba.Call(self._select, index), + click_activate=True, + color=(0.5, 0.5, 0.5, 0.5) if inactive else + ((0.5, 1, 0.5, + 1) if choice == self._current_choice else + (0.8, 0.8, 0.8, 1.0)), + padding=0, + maxwidth=maxwidth, + text=choice_display_name, + on_activate_call=self._activate, + v_align='center', + selectable=(not inactive)) + if choice == self._current_choice: + ba.containerwidget(edit=self._columnwidget, + selected_child=wdg, + visible_child=wdg) + + # ok from now on our delegate can be called + self._delegate = weakref.ref(delegate) + self._done_building = True + + +original_connect_to_party = _ba.connect_to_party +original_sign_in = ba.internal.sign_in_v1 + + +def modify_connect_to_party(address: str, port: int = 43210, print_progress: bool = True) -> None: + global _ip, _port + _ip = address + _port = port + original_connect_to_party(_ip, _port, print_progress) + + +temptimer = None + + +def modify_sign_in(account_type: str) -> None: + original_sign_in(account_type) + if messenger.server_online: + messenger.logged_in = False + global temptimer + temptimer = ba.Timer(2, messenger._cookie_login) + + +class PingThread(Thread): + """Thread for sending out game pings.""" + + def __init__(self, address: str, port: int): + super().__init__() + self._address = address + self._port = port + + def run(self) -> None: + sock: Optional[socket.socket] = None + try: + import socket + from ba.internal import get_ip_address_type + socket_type = get_ip_address_type(self._address) + sock = socket.socket(socket_type, socket.SOCK_DGRAM) + sock.connect((self._address, self._port)) + + starttime = time.time() + + # Send a few pings and wait a second for + # a response. + sock.settimeout(1) + for _i in range(3): + sock.send(b'\x0b') + result: Optional[bytes] + try: + # 11: BA_PACKET_SIMPLE_PING + result = sock.recv(10) + except Exception: + result = None + if result == b'\x0c': + # 12: BA_PACKET_SIMPLE_PONG + accessible = True + break + time.sleep(1) + global _ping + _ping = int((time.time() - starttime) * 1000.0) + except Exception: + ba.print_exception('Error on gather ping', once=True) + finally: + try: + if sock is not None: + sock.close() + except Exception: + ba.print_exception('Error on gather ping cleanup', once=True) + + +def _get_store_char_tex(self) -> str: + _ba.set_party_icon_always_visible(True) + return ('storeCharacterXmas' if ba.internal.get_v1_account_misc_read_val( + 'xmas', False) else + 'storeCharacterEaster' if ba.internal.get_v1_account_misc_read_val( + 'easter', False) else 'storeCharacter') + + +# ba_meta export plugin +class InitalRun(ba.Plugin): + def __init__(self): + if _ba.env().get("build_number", 0) >= 20124: + global messenger, listener, displayer, color_tracker + initialize() + messenger = PrivateChatHandler() + listener = Thread(target=messenger_thread) + listener.start() + displayer = ba.Timer(0.4, msg_displayer, True) + color_tracker = ColorTracker() + bastd.ui.party.PartyWindow = PartyWindow + PopupMenuWindow.__init__ = __popup_menu_window_init__ + _ba.connect_to_party = modify_connect_to_party + ba.internal.sign_in_v1 = modify_sign_in + MainMenuWindow._get_store_char_tex = _get_store_char_tex + else: + display_error("This Party Window only runs with BombSquad version higer than 1.6.0.")