diff --git a/plugins/utilities/easy_connect.py b/plugins/utilities/easy_connect.py new file mode 100644 index 0000000..0c9d6e0 --- /dev/null +++ b/plugins/utilities/easy_connect.py @@ -0,0 +1,592 @@ +# -*- coding: utf-8 -*- +# ba_meta require api 7 + +# =============================================== +# EasyConnect by Mr.Smoothy | +# verion 1.2 | +# https://discord.gg/ucyaesh | +# Serverconnector X IPPORTRevealer | +# for bombsquad v1.7 + | +# =============================================== + + +# .................___________________________________________ +# WATCH IN ACTION https://www.youtube.com/watch?v=jwi2wKwZblQ +# .................___________________________________________ + +# Have any idea/suggestion/bug report > send message on discord mr.smoothy#5824 + +# Discord:- +# mr.smoothy#5824 + + +# DONT EDIT ANYTHING WITHOUT PERMISSION + +# join Bombspot - bombsquad biggest modding community .... open for everyone https://discord.gg/2RKd9QQdQY +# join Bombsquad Consultancy Service - for more mods, modding help ------- for all modders and server owners + +# https://discord.gg/2RKd9QQdQY +# https://discord.gg/ucyaesh + +# REQUIREMENTS +# built for bs 1.7 and above + +# by Mr.Smoothy for Bombsquad version 1.7 + +import _ba +import ba +import bastd +import threading +from bastd.ui.gather import manualtab, publictab +from bastd.ui import popup +from dataclasses import dataclass +import random +from enum import Enum +from bastd.ui.popup import PopupMenuWindow, PopupWindow +from typing import Any, Optional, Dict, List, Tuple, Type, Union, Callable +from bastd.ui.gather.publictab import PublicGatherTab + + +class _HostLookupThread(threading.Thread): + """Thread to fetch an addr.""" + + def __init__(self, name: str, port: int, + call: Callable[[Optional[str], int], Any]): + super().__init__() + self._name = name + self._port = port + self._call = call + + def run(self) -> None: + result: Optional[str] + try: + import socket + result = socket.gethostbyname(self._name) + except Exception: + result = None + ba.pushcall(lambda: self._call(result, self._port), + from_other_thread=True) + + +def new_build_favorites_tab(self, region_height: float) -> None: + c_height = region_height - 20 + v = c_height - 35 - 25 - 30 + self.retry_inter = 0.0 + uiscale = ba.app.ui.uiscale + self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (578 if uiscale is ba.UIScale.SMALL else + 670 if uiscale is ba.UIScale.MEDIUM else 800) + + self._scroll_width = self._width - 130 + 2 * x_inset + self._scroll_height = self._height - 180 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + + c_height = self._scroll_height - 20 + sub_scroll_height = c_height - 63 + self._favorites_scroll_width = sub_scroll_width = ( + 680 if uiscale is ba.UIScale.SMALL else 640) + + v = c_height - 30 + + b_width = 140 if uiscale is ba.UIScale.SMALL else 178 + b_height = (90 if uiscale is ba.UIScale.SMALL else + 142 if uiscale is ba.UIScale.MEDIUM else 130) + b_space_extra = (0 if uiscale is ba.UIScale.SMALL else + -2 if uiscale is ba.UIScale.MEDIUM else -5) + + btnv = (c_height - (48 if uiscale is ba.UIScale.SMALL else + 45 if uiscale is ba.UIScale.MEDIUM else 40) - + b_height) + # ================= smoothy ============= + + ba.textwidget(parent=self._container, + position=(90 if uiscale is ba.UIScale.SMALL else 120, btnv + + 120 if uiscale is ba.UIScale.SMALL else btnv+90), + size=(0, 0), + h_align='center', + color=(0.8, 0.8, 0.8), + v_align='top', + text="Auto") + btnv += 50 if uiscale is ba.UIScale.SMALL else 0 + + ba.buttonwidget(parent=self._container, + size=(30, 30), + position=(25 if uiscale is ba.UIScale.SMALL else 40, + btnv+10), + + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self.auto_retry_dec, + text_scale=1.3 if uiscale is ba.UIScale.SMALL else 1.2, + label="-", + autoselect=True) + self.retry_inter_text = ba.textwidget(parent=self._container, + position=( + 90 if uiscale is ba.UIScale.SMALL else 120, btnv+25), + size=(0, 0), + h_align='center', + color=(0.8, 0.8, 0.8), + v_align='center', + text=str(self.retry_inter) if self.retry_inter > 0.0 else 'off') + ba.buttonwidget(parent=self._container, + size=(30, 30), + position=(125 if uiscale is ba.UIScale.SMALL else 155, + btnv+10), + + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self.auto_retry_inc, + text_scale=1.3 if uiscale is ba.UIScale.SMALL else 1.2, + label="+", + autoselect=True) + + btnv -= b_height + b_space_extra + + self._favorites_connect_button = btn1 = ba.buttonwidget( + parent=self._container, + size=(b_width, b_height), + position=(25 if uiscale is ba.UIScale.SMALL else 40, btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_favorites_connect_press, + text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, + label=ba.Lstr(resource='gatherWindow.manualConnectText'), + autoselect=True) + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + ba.widget(edit=btn1, + left_widget=_ba.get_special_widget('back_button')) + btnv -= b_height + b_space_extra + ba.buttonwidget(parent=self._container, + size=(b_width, b_height), + position=(25 if uiscale is ba.UIScale.SMALL else 40, + btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_favorites_edit_press, + text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, + label=ba.Lstr(resource='editText'), + autoselect=True) + btnv -= b_height + b_space_extra + ba.buttonwidget(parent=self._container, + size=(b_width, b_height), + position=(25 if uiscale is ba.UIScale.SMALL else 40, + btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_favorite_delete_press, + text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, + label=ba.Lstr(resource='deleteText'), + autoselect=True) + + v -= sub_scroll_height + 23 + self._scrollwidget = scrlw = ba.scrollwidget( + parent=self._container, + position=(190 if uiscale is ba.UIScale.SMALL else 225, v), + size=(sub_scroll_width, sub_scroll_height), + claims_left_right=True) + ba.widget(edit=self._favorites_connect_button, + right_widget=self._scrollwidget) + self._columnwidget = ba.columnwidget(parent=scrlw, + left_border=10, + border=2, + margin=0, + claims_left_right=True) + + self._favorite_selected = None + self._refresh_favorites() + + +def new_on_favorites_connect_press(self) -> None: + if self._favorite_selected is None: + self._no_favorite_selected_error() + + else: + config = ba.app.config['Saved Servers'][self._favorite_selected] + _HostLookupThread(name=config['addr'], + port=config['port'], + call=ba.WeakCall( + self._host_lookup_result)).start() + + if self.retry_inter > 0 and (_ba.get_connection_to_host_info() == {} or _ba.get_connection_to_host_info()['build_number'] == 0): + ba.screenmessage("Server full or unreachable, Retrying....") + self._retry_timer = ba.Timer(self.retry_inter, ba.Call( + self._on_favorites_connect_press), timetype=ba.TimeType.REAL) + + +def auto_retry_inc(self): + + self.retry_inter += 0.5 + ba.textwidget(edit=self.retry_inter_text, text='%.1f' % self.retry_inter) + + +def auto_retry_dec(self): + if self.retry_inter > 0.0: + self.retry_inter -= 0.5 + + if self.retry_inter == 0.0: + ba.textwidget(edit=self.retry_inter_text, text='off') + else: + ba.textwidget(edit=self.retry_inter_text, text='%.1f' % self.retry_inter) + + +@dataclass +class PartyEntry: + """Info about a public party.""" + address: str + index: int + queue: Optional[str] = None + port: int = -1 + name: str = '' + size: int = -1 + size_max: int = -1 + claimed: bool = False + ping: Optional[float] = None + ping_interval: float = -1.0 + next_ping_time: float = -1.0 + ping_attempts: int = 0 + ping_responses: int = 0 + stats_addr: Optional[str] = None + clean_display_index: Optional[int] = None + + def get_key(self) -> str: + """Return the key used to store this party.""" + return f'{self.address}_{self.port}' + + +class SelectionComponent(Enum): + """Describes what part of an entry is selected.""" + NAME = 'name' + STATS_BUTTON = 'stats_button' + + +@dataclass +class Selection: + """Describes the currently selected list element.""" + entry_key: str + component: SelectionComponent + + +def _clear(self) -> None: + for widget in [ + self._name_widget, self._size_widget, self._ping_widget, + self._stats_button + ]: + if widget: + try: + widget.delete() + except: + pass + + +def update(self, index: int, party: PartyEntry, sub_scroll_width: float, + sub_scroll_height: float, lineheight: float, + columnwidget: ba.Widget, join_text: ba.Widget, + filter_text: ba.Widget, existing_selection: Optional[Selection], + tab: PublicGatherTab) -> None: + """Update for the given data.""" + # pylint: disable=too-many-locals + + # Quick-out: if we've been marked clean for a certain index and + # we're still at that index, we're done. + if party.clean_display_index == index: + return + + ping_good = _ba.get_v1_account_misc_read_val('pingGood', 100) + ping_med = _ba.get_v1_account_misc_read_val('pingMed', 500) + + self._clear() + hpos = 20 + vpos = sub_scroll_height - lineheight * index - 50 + self._name_widget = ba.textwidget( + text=ba.Lstr(value=party.name), + parent=columnwidget, + size=(sub_scroll_width * 0.63, 20), + position=(0 + hpos, 4 + vpos), + selectable=True, + on_select_call=ba.WeakCall( + tab.set_public_party_selection, + Selection(party.get_key(), SelectionComponent.NAME)), + on_activate_call=ba.WeakCall(tab.on_public_party_activate, party), + click_activate=True, + maxwidth=sub_scroll_width * 0.45, + corner_scale=1.4, + autoselect=True, + color=(1, 1, 1, 0.3 if party.ping is None else 1.0), + h_align='left', + v_align='center') + ba.widget(edit=self._name_widget, + left_widget=join_text, + show_buffer_top=64.0, + show_buffer_bottom=64.0) + if existing_selection == Selection(party.get_key(), + SelectionComponent.NAME): + ba.containerwidget(edit=columnwidget, + selected_child=self._name_widget) + if party.stats_addr or True: + url = party.stats_addr.replace( + '${ACCOUNT}', + _ba.get_v1_account_misc_read_val_2('resolvedAccountID', + 'UNKNOWN')) + self._stats_button = ba.buttonwidget( + color=(0.5, 0.8, 0.8), + textcolor=(1.0, 1.0, 1.0), + label='....', + parent=columnwidget, + autoselect=True, + + on_select_call=ba.WeakCall( + tab.set_public_party_selection, + Selection(party.get_key(), + SelectionComponent.STATS_BUTTON)), + size=(100, 40), + position=(sub_scroll_width * 0.66 + hpos, 1 + vpos), + scale=0.9) + ba.buttonwidget(edit=self._stats_button, on_activate_call=ba.Call( + self.on_stats_click, self._stats_button, party)) + if existing_selection == Selection( + party.get_key(), SelectionComponent.STATS_BUTTON): + ba.containerwidget(edit=columnwidget, + selected_child=self._stats_button) + + self._size_widget = ba.textwidget( + text=str(party.size) + '/' + str(party.size_max), + parent=columnwidget, + size=(0, 0), + position=(sub_scroll_width * 0.86 + hpos, 20 + vpos), + scale=0.7, + color=(0.8, 0.8, 0.8), + h_align='right', + v_align='center') + + if index == 0: + ba.widget(edit=self._name_widget, up_widget=filter_text) + if self._stats_button: + ba.widget(edit=self._stats_button, up_widget=filter_text) + + self._ping_widget = ba.textwidget(parent=columnwidget, + size=(0, 0), + position=(sub_scroll_width * 0.94 + + hpos, 20 + vpos), + scale=0.7, + h_align='right', + v_align='center') + if party.ping is None: + ba.textwidget(edit=self._ping_widget, + text='-', + color=(0.5, 0.5, 0.5)) + else: + ba.textwidget(edit=self._ping_widget, + text=str(int(party.ping)), + color=(0, 1, 0) if party.ping <= ping_good else + (1, 1, 0) if party.ping <= ping_med else (1, 0, 0)) + + party.clean_display_index = index + + +def _get_popup_window_scale() -> float: + uiscale = ba.app.ui.uiscale + return (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + + +_party = None + + +def on_stats_click(self, widget, party): + global _party + _party = party + choices = ['connect', 'copyqueue', "save"] + DisChoices = [ba.Lstr(resource="ipp", fallback_value="Connect by IP"), ba.Lstr( + resource="copy id", fallback_value="Copy Queue ID"), ba.Lstr(value="Save")] + if party.stats_addr: + choices.append('stats') + if 'discord' in party.stats_addr: + txt = "Discord" + elif 'yout' in party.stats_addr: + txt = "Youtube" + else: + txt = party.stats_addr[0:13] + DisChoices.append(ba.Lstr(value=txt)) + PopupMenuWindow( + position=widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=choices, + choices_display=DisChoices, + current_choice="stats", + delegate=self) + + +def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None: + pass + + +def popup_menu_selected_choice(self, window: popup.PopupMenu, + choice: str) -> None: + """Called when a menu entry is selected.""" + # Unused arg. + + if choice == 'stats': + url = _party.stats_addr.replace( + '${ACCOUNT}', + _ba.get_v1_account_misc_read_val_2('resolvedAccountID', + 'UNKNOWN')) + ba.open_url(url) + elif choice == 'connect': + + PartyQuickConnect(_party.address, _party.port) + elif choice == 'save': + config = ba.app.config + ip_add = _party.address + p_port = _party.port + title = _party.name + if not isinstance(config.get('Saved Servers'), dict): + config['Saved Servers'] = {} + config['Saved Servers'][f'{ip_add}@{p_port}'] = { + 'addr': ip_add, + 'port': p_port, + 'name': title + } + config.commit() + ba.screenmessage("Server saved to manual") + ba.playsound(ba.getsound('gunCocking')) + elif choice == "copyqueue": + ba.clipboard_set_text(_party.queue) + ba.playsound(ba.getsound('gunCocking')) + + +def replace(): + manualtab.ManualGatherTab._build_favorites_tab = new_build_favorites_tab + manualtab.ManualGatherTab._on_favorites_connect_press = new_on_favorites_connect_press + manualtab.ManualGatherTab.auto_retry_dec = auto_retry_dec + manualtab.ManualGatherTab.auto_retry_inc = auto_retry_inc + publictab.UIRow.update = update + publictab.UIRow._clear = _clear + publictab.UIRow.on_stats_click = on_stats_click + publictab.UIRow.popup_menu_closing = popup_menu_closing + publictab.UIRow.popup_menu_selected_choice = popup_menu_selected_choice + + +class PartyQuickConnect(ba.Window): + def __init__(self, address: str, port: int): + self._width = 800 + self._height = 400 + self._white_tex = ba.gettexture('white') + self.lineup_tex = ba.gettexture('playerLineup') + self.lineup_1_transparent_model = ba.getmodel( + 'playerLineup1Transparent') + self.eyes_model = ba.getmodel('plasticEyesTransparent') + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + color=(0.45, 0.63, 0.15), + transition='in_scale', + scale=(1.4 if uiscale is ba.UIScale.SMALL else + 1.2 if uiscale is ba.UIScale.MEDIUM else 1.0))) + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + scale=1.0, + position=(60, self._height - 80), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + self.IP = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.55 + 60), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text="IP: "+address + " PORT: "+str(port), + maxwidth=self._width * 0.65) + self._title_text = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.55), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text="Retrying....", + maxwidth=self._width * 0.65) + self._line_image = ba.imagewidget( + parent=self._root_widget, + color=(0.0, 0.0, 0.0), + opacity=0.2, + position=(40.0, 120), + size=(800-190+80, 4.0), + texture=self._white_tex) + self.dude_x = 60 + self._body_image_target = ba.buttonwidget( + parent=self._root_widget, + size=(1 * 60, 1 * 80), + color=(random.random(), random.random(), random.random()), + label='', + texture=self.lineup_tex, + position=(40, 110), + model_transparent=self.lineup_1_transparent_model) + self._eyes_image = ba.imagewidget( + parent=self._root_widget, + size=(1 * 36, 1 * 18), + texture=self.lineup_tex, + color=(1, 1, 1), + position=(40, 165), + model_transparent=self.eyes_model) + # self._body_image_target2 = ba.imagewidget( + # parent=self._root_widget, + # size=(1* 60, 1 * 80), + # color=(1,0.3,0.4), + # texture=self.lineup_tex, + # position=(700,130), + # model_transparent=self.lineup_1_transparent_model) + self.closed = False + self.retry_count = 1 + self.direction = "right" + self.connect(address, port) + self.move_R = ba.Timer(0.01, ba.Call(self.move_right), + timetype=ba.TimeType.REAL, repeat=True) + + def move_right(self): + if self._body_image_target and self._eyes_image: + ba.buttonwidget(edit=self._body_image_target, position=(self.dude_x, 110)) + ba.imagewidget(edit=self._eyes_image, position=(self.dude_x+10, 165)) + else: + self.move_R = None + if self.direction == "right": + self.dude_x += 2 + if self.dude_x >= 650: + self.direction = "left" + else: + self.dude_x -= 2 + if self.dude_x <= 50: + self.direction = "right" + + def connect(self, address, port): + if not self.closed and (_ba.get_connection_to_host_info() == {} or _ba.get_connection_to_host_info()['build_number'] == 0): + ba.textwidget(edit=self._title_text, text="Retrying....("+str(self.retry_count)+")") + self.retry_count += 1 + _ba.connect_to_party(address, port=port) + self._retry_timer = ba.Timer(1.5, ba.Call( + self.connect, address, port), timetype=ba.TimeType.REAL) + + def close(self) -> None: + """Close the ui.""" + self.closed = True + ba.containerwidget(edit=self._root_widget, transition='out_scale') + +# ba_meta export plugin + + +class InitalRun(ba.Plugin): + def __init__(self): + replace()