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.")