mirror of
https://github.com/bombsquad-community/plugin-manager.git
synced 2026-08-27 01:12:38 +00:00
merge conflicts
This commit is contained in:
commit
d770fd9ee1
14 changed files with 3129 additions and 267 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -1,5 +1,24 @@
|
|||
## Plugin Manager (dd-mm-yyyy)
|
||||
|
||||
### 0.3.2 (30-04-2023)
|
||||
|
||||
- Fix sometimes same sound would repeat twice when pressing a button.
|
||||
- Low key attempt to experiment with staging branch by changing current tag in `plugin_manager.py`.
|
||||
- Assume underscores as spaces when searching for plugins in game.
|
||||
|
||||
### 0.3.1 (04-03-2023)
|
||||
|
||||
- Resize the plugin window to limit the overlapping of plugin description.
|
||||
|
||||
### 0.3.0 (12-02-2023)
|
||||
|
||||
- Displays a tutorial button in the plugin window, whenever there is a supported url present in the plugin data.
|
||||
|
||||
### 0.2.2 (18-01-2023)
|
||||
|
||||
- Auto add new line breaks in long plugin descriptions.
|
||||
- Fixed an issue where pressing back on the main plugin manager window would play the sound twice.
|
||||
|
||||
### 0.2.1 (17-12-2022)
|
||||
|
||||
- Add Google DNS as a fallback for Jio ISP DNS blocking resolution of raw.githubusercontent.com domain.
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -1,8 +1,3 @@
|
|||
[![DownloadIcon]][DownloadLink]
|
||||
|
||||
[DownloadIcon]:https://img.shields.io/badge/Download-5555ff?style=for-the-badge&logoColor=white&logo=DocuSign
|
||||
[DownloadLink]:https://cdn.jsdelivr.net/gh/bombsquad-community/plugin-manager/plugin_manager.py
|
||||
|
||||
[](https://github.com/bombsquad-community/plugin-manager/actions/workflows/ci.yml)
|
||||
|
||||
# plugin-manager
|
||||
|
|
@ -10,6 +5,11 @@
|
|||
A plugin manager for the game - [Bombsquad](https://www.froemling.net/apps/bombsquad). Plugin manager is a plugin in itself,
|
||||
which makes further modding of your game more convenient by providing easier access to community created content.
|
||||
|
||||
[![DownloadIcon]][DownloadLink]
|
||||
|
||||
[DownloadIcon]:https://img.shields.io/badge/Download-5555ff?style=for-the-badge&logoColor=white&logo=DocuSign
|
||||
[DownloadLink]:https://cdn.jsdelivr.net/gh/bombsquad-community/plugin-manager/plugin_manager.py
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
|
@ -178,6 +178,14 @@ That's it! Now you can make a [pull request](../../compare) with both the update
|
|||
repository in your plugin manager by adding `rikkolovescats/sahilp-plugins` as a custom source through the category
|
||||
selection popup window in-game.
|
||||
|
||||
#### Known 3rd Party Plugin Sources
|
||||
|
||||
If you maintain or know of a 3rd party plugin source, let us know and we'll add it below so people can know about it. It
|
||||
will also help us to notify the maintainers of any future breaking changes in plugin manager that could affect 3rd party
|
||||
plugin sources.
|
||||
|
||||
https://github.com/rikkolovescats/sahilp-plugins
|
||||
|
||||
|
||||
## Tests
|
||||
|
||||
|
|
|
|||
24
index.json
24
index.json
|
|
@ -1,6 +1,30 @@
|
|||
{
|
||||
"plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py",
|
||||
"versions": {
|
||||
"0.3.2": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "789f95f",
|
||||
"released_on": "30-04-2023",
|
||||
"md5sum": "ff679f8411e426e5e4e92a2f958eec02"
|
||||
},
|
||||
"0.3.1": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "0b856ba",
|
||||
"released_on": "04-03-2023",
|
||||
"md5sum": "52fdce0f242b1bc52a1cbf2e7d78d230"
|
||||
},
|
||||
"0.3.0": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "cb5df36",
|
||||
"released_on": "12-02-2023",
|
||||
"md5sum": "d149fedf64b002c97fbb883ae5629d49"
|
||||
},
|
||||
"0.2.2": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "2672a5a",
|
||||
"released_on": "18-01-2023",
|
||||
"md5sum": "2ef9761e4a02057cd93db3d280427f12"
|
||||
},
|
||||
"0.2.1": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "8ac1032",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# ba_meta require api 7
|
||||
import ba
|
||||
import _ba
|
||||
from bastd.ui import popup
|
||||
from bastd.ui import popup, confirm
|
||||
|
||||
import urllib.request
|
||||
import http.client
|
||||
|
|
@ -24,8 +24,10 @@ _env = _ba.env()
|
|||
_uiscale = ba.app.ui.uiscale
|
||||
|
||||
|
||||
PLUGIN_MANAGER_VERSION = "0.2.1"
|
||||
PLUGIN_MANAGER_VERSION = "0.3.2"
|
||||
REPOSITORY_URL = "https://github.com/bombsquad-community/plugin-manager"
|
||||
# Current tag can be changed to "staging" or any other branch in
|
||||
# plugin manager repo for testing purpose.
|
||||
CURRENT_TAG = "main"
|
||||
INDEX_META = "{repository_url}/{content_type}/{tag}/index.json"
|
||||
HEADERS = {
|
||||
|
|
@ -104,10 +106,6 @@ async def async_stream_network_response_to_file(request, file, md5sum=None, retr
|
|||
return content
|
||||
|
||||
|
||||
def play_sound():
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
|
||||
|
||||
def partial_format(string_template, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
string_template = string_template.replace("{" + key + "}", value)
|
||||
|
|
@ -312,8 +310,11 @@ class Category:
|
|||
|
||||
async def fetch_metadata(self):
|
||||
if self._metadata is None:
|
||||
# Let's keep depending on the "main" branch for 3rd party sources
|
||||
# even if we're using a different branch of plugin manager's repository.
|
||||
tag = "main" if self.is_3rd_party else CURRENT_TAG
|
||||
request = urllib.request.Request(
|
||||
self.meta_url.format(content_type="raw", tag=CURRENT_TAG),
|
||||
self.meta_url.format(content_type="raw", tag=tag),
|
||||
headers=self.request_headers,
|
||||
)
|
||||
response = await async_send_network_request(request)
|
||||
|
|
@ -788,13 +789,37 @@ class PluginWindow(popup.PopupWindow):
|
|||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(self.draw_ui())
|
||||
|
||||
def get_description(self, minimum_character_offset=40):
|
||||
"""
|
||||
Splits the loong plugin description into multiple lines.
|
||||
"""
|
||||
string = self.plugin.info["description"]
|
||||
string_length = len(string)
|
||||
|
||||
partitioned_string = ""
|
||||
partitioned_string_length = len(partitioned_string)
|
||||
|
||||
while partitioned_string_length != string_length:
|
||||
next_empty_space = string[partitioned_string_length +
|
||||
minimum_character_offset:].find(" ")
|
||||
next_word_end_position = partitioned_string_length + \
|
||||
minimum_character_offset + max(0, next_empty_space)
|
||||
partitioned_string += string[partitioned_string_length:next_word_end_position]
|
||||
if next_empty_space != -1:
|
||||
# Insert a line break here, there's still more partitioning to do.
|
||||
partitioned_string += "\n"
|
||||
partitioned_string_length = len(partitioned_string)
|
||||
|
||||
return partitioned_string
|
||||
|
||||
async def draw_ui(self):
|
||||
# print(ba.app.plugins.active_plugins)
|
||||
play_sound()
|
||||
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
b_text_color = (0.75, 0.7, 0.8)
|
||||
s = 1.1 if _uiscale is ba.UIScale.SMALL else 1.27 if ba.UIScale.MEDIUM else 1.57
|
||||
width = 360 * s
|
||||
height = 100 + 100 * s
|
||||
s = 1.25 if _uiscale is ba.UIScale.SMALL else 1.39 if ba.UIScale.MEDIUM else 1.67
|
||||
width = 400 * s
|
||||
height = 120 + 100 * s
|
||||
color = (1, 1, 1)
|
||||
text_scale = 0.7 * s
|
||||
self._transition_out = 'out_scale'
|
||||
|
|
@ -803,7 +828,7 @@ class PluginWindow(popup.PopupWindow):
|
|||
self._root_widget = ba.containerwidget(size=(width, height),
|
||||
# parent=_ba.get_special_widget(
|
||||
# 'overlay_stack'),
|
||||
on_outside_click_call=self._ok,
|
||||
on_outside_click_call=self._cancel,
|
||||
transition=transition,
|
||||
scale=(2.1 if _uiscale is ba.UIScale.SMALL else 1.5
|
||||
if _uiscale is ba.UIScale.MEDIUM else 1.0),
|
||||
|
|
@ -837,7 +862,7 @@ class PluginWindow(popup.PopupWindow):
|
|||
ba.textwidget(parent=self._root_widget,
|
||||
position=(width * 0.49, pos), size=(0, 0),
|
||||
h_align='center', v_align='center',
|
||||
text=self.plugin.info["description"],
|
||||
text=self.get_description(),
|
||||
scale=text_scale * 0.6, color=color,
|
||||
maxwidth=width * 0.95)
|
||||
b1_color = None
|
||||
|
|
@ -907,10 +932,10 @@ class PluginWindow(popup.PopupWindow):
|
|||
text_scale=1,
|
||||
label=button3_label)
|
||||
ba.containerwidget(edit=self._root_widget,
|
||||
on_cancel_call=self._ok)
|
||||
on_cancel_call=self._cancel)
|
||||
|
||||
open_pos_x = (300 if _uiscale is ba.UIScale.SMALL else
|
||||
360 if _uiscale is ba.UIScale.MEDIUM else 350)
|
||||
open_pos_x = (390 if _uiscale is ba.UIScale.SMALL else
|
||||
450 if _uiscale is ba.UIScale.MEDIUM else 440)
|
||||
open_pos_y = (100 if _uiscale is ba.UIScale.SMALL else
|
||||
110 if _uiscale is ba.UIScale.MEDIUM else 120)
|
||||
open_button = ba.buttonwidget(parent=self._root_widget,
|
||||
|
|
@ -937,6 +962,45 @@ class PluginWindow(popup.PopupWindow):
|
|||
rotate=25,
|
||||
scale=0.45)
|
||||
|
||||
# Below snippet handles the tutorial button in the plugin window
|
||||
tutorial_url = self.plugin.info["external_url"]
|
||||
if tutorial_url:
|
||||
def tutorial_confirm_window():
|
||||
text = "This will take you to \n\""+self.plugin.info["external_url"] + "\""
|
||||
tutorial_confirm_window = confirm.ConfirmWindow(
|
||||
text=text,
|
||||
action=lambda: ba.open_url(self.plugin.info["external_url"]),
|
||||
)
|
||||
open_pos_x = (440 if _uiscale is ba.UIScale.SMALL else
|
||||
500 if _uiscale is ba.UIScale.MEDIUM else 490)
|
||||
open_pos_y = (100 if _uiscale is ba.UIScale.SMALL else
|
||||
110 if _uiscale is ba.UIScale.MEDIUM else 120)
|
||||
open_button = ba.buttonwidget(parent=self._root_widget,
|
||||
autoselect=True,
|
||||
position=(open_pos_x, open_pos_y),
|
||||
size=(40, 40),
|
||||
button_type="square",
|
||||
label="",
|
||||
# color=ba.app.ui.title_color,
|
||||
color=(0.6, 0.53, 0.63),
|
||||
|
||||
on_activate_call=tutorial_confirm_window)
|
||||
|
||||
ba.imagewidget(parent=self._root_widget,
|
||||
position=(open_pos_x, open_pos_y),
|
||||
size=(40, 40),
|
||||
color=(0.8, 0.95, 1),
|
||||
texture=ba.gettexture("frameInset"),
|
||||
draw_controller=open_button)
|
||||
ba.textwidget(parent=self._root_widget,
|
||||
position=(open_pos_x - 3, open_pos_y + 12),
|
||||
text="Tutorial",
|
||||
size=(10, 10),
|
||||
draw_controller=open_button,
|
||||
color=(1, 1, 1, 1),
|
||||
rotate=25,
|
||||
scale=0.45)
|
||||
|
||||
if to_draw_button4:
|
||||
settings_pos_x = (60 if _uiscale is ba.UIScale.SMALL else
|
||||
60 if _uiscale is ba.UIScale.MEDIUM else 60)
|
||||
|
|
@ -963,7 +1027,10 @@ class PluginWindow(popup.PopupWindow):
|
|||
# ba.containerwidget(edit=self._root_widget, start_button=button3)
|
||||
|
||||
def _ok(self) -> None:
|
||||
play_sound()
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_scale')
|
||||
|
||||
def _cancel(self) -> None:
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_scale')
|
||||
|
||||
def button(fn):
|
||||
|
|
@ -1130,7 +1197,6 @@ class PluginManager:
|
|||
|
||||
class PluginSourcesWindow(popup.PopupWindow):
|
||||
def __init__(self, origin_widget):
|
||||
play_sound()
|
||||
self.selected_source = None
|
||||
|
||||
self.scale_origin = origin_widget.get_screen_space_center()
|
||||
|
|
@ -1301,7 +1367,7 @@ class PluginSourcesWindow(popup.PopupWindow):
|
|||
self.draw_sources()
|
||||
|
||||
def _ok(self) -> None:
|
||||
play_sound()
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_scale')
|
||||
|
||||
|
||||
|
|
@ -1336,7 +1402,7 @@ class PluginCategoryWindow(popup.PopupMenuWindow):
|
|||
PluginSourcesWindow(origin_widget=self.root_widget)
|
||||
|
||||
def _ok(self) -> None:
|
||||
play_sound()
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
ba.containerwidget(edit=self.root_widget, transition='out_scale')
|
||||
|
||||
|
||||
|
|
@ -1420,7 +1486,6 @@ class PluginManagerWindow(ba.Window):
|
|||
)
|
||||
|
||||
def _back(self) -> None:
|
||||
play_sound()
|
||||
from bastd.ui.settings.allsettings import AllSettingsWindow
|
||||
ba.containerwidget(edit=self._root_widget,
|
||||
transition=self._transition_out)
|
||||
|
|
@ -1615,6 +1680,8 @@ class PluginManagerWindow(ba.Window):
|
|||
draw_controller=controller_button)
|
||||
|
||||
def search_term_filterer(self, plugin, search_term):
|
||||
# This helps resolve "plugin name" to "plugin_name".
|
||||
search_term = search_term.replace(" ", "_")
|
||||
if search_term in plugin.name:
|
||||
return True
|
||||
if search_term in plugin.info["description"].lower():
|
||||
|
|
@ -1711,7 +1778,6 @@ class PluginManagerWindow(ba.Window):
|
|||
PluginWindow(plugin, self._root_widget, lambda: self.draw_plugin_name(plugin))
|
||||
|
||||
def show_categories_window(self):
|
||||
play_sound()
|
||||
PluginCategoryWindow(
|
||||
self.plugin_manager.categories.keys(),
|
||||
self.selected_category,
|
||||
|
|
@ -1751,7 +1817,6 @@ class PluginManagerWindow(ba.Window):
|
|||
|
||||
class PluginManagerSettingsWindow(popup.PopupWindow):
|
||||
def __init__(self, plugin_manager, origin_widget):
|
||||
play_sound()
|
||||
self._plugin_manager = plugin_manager
|
||||
self.scale_origin = origin_widget.get_screen_space_center()
|
||||
self.settings = ba.app.config["Community Plugin Manager"]["Settings"].copy()
|
||||
|
|
@ -1958,7 +2023,7 @@ class PluginManagerSettingsWindow(popup.PopupWindow):
|
|||
self._update_button.delete()
|
||||
|
||||
def _ok(self) -> None:
|
||||
play_sound()
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_scale')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -250,6 +250,30 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"hot_bomb": {
|
||||
"description": "Get the bomb to explode on the enemy team to win.",
|
||||
"external_url": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "SEBASTIAN2059",
|
||||
"email": "",
|
||||
"discord": "SEBASTIAN2059#5751"
|
||||
},
|
||||
{
|
||||
"name": "zPanxo",
|
||||
"email": "",
|
||||
"discord": "zPanxo#7201"
|
||||
}
|
||||
],
|
||||
"versions": {
|
||||
"1.0.0": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "095a773",
|
||||
"released_on": "29-04-2023",
|
||||
"md5sum": "7296a71c9ed796ab1f54c2eb5d843bda"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shimla": {
|
||||
"description": "Death match with elevators, 2-D view.",
|
||||
"external_url": "https://www.youtube.com/channel/UCaQajfKHrTPgiOhuias5iPg",
|
||||
|
|
|
|||
1666
plugins/minigames/hot_bomb.py
Normal file
1666
plugins/minigames/hot_bomb.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,6 +3,30 @@
|
|||
"description": "Utilities",
|
||||
"plugins_base_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/utilities",
|
||||
"plugins": {
|
||||
"random_join": {
|
||||
"description": "Come visit the unknown servers around all the world! Plugin designed not to join servers with similar names more frequently than rare ones. Have fun!",
|
||||
"external_url": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "maxick",
|
||||
"email": "",
|
||||
"discord": "maxick#9227"
|
||||
},
|
||||
{
|
||||
"name": "LoupGarou",
|
||||
"email": "LoupGarou5418@outlook.com",
|
||||
"discord": "ʟօʊքɢǟʀօʊ#3063"
|
||||
}
|
||||
],
|
||||
"versions": {
|
||||
"1.0.0": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "2454845",
|
||||
"released_on": "26-12-2022",
|
||||
"md5sum": "7bac6bfe837ff89e7da10a0ab45691d1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"share_replay": {
|
||||
"description": "Export replays to mods folder and share them with friends or have a backup",
|
||||
"external_url": "",
|
||||
|
|
@ -14,6 +38,12 @@
|
|||
}
|
||||
],
|
||||
"versions": {
|
||||
"1.3.0": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "ec116b3",
|
||||
"released_on": "20-12-2022",
|
||||
"md5sum": "dbb9d85a5fb0041631dc12765a257fce"
|
||||
},
|
||||
"1.2.1": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "7753b87",
|
||||
|
|
@ -179,7 +209,7 @@
|
|||
},
|
||||
"colorscheme": {
|
||||
"description": "Create custom UI colorschemes!",
|
||||
"external_url": "https://www.youtube.com/watch?v=qatwWrBAvjc",
|
||||
"external_url": "https://www.youtube.com/watch?v=G6824StL4eg",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rikko",
|
||||
|
|
@ -299,6 +329,12 @@
|
|||
}
|
||||
],
|
||||
"versions": {
|
||||
"1.2.1": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "64e8a5c",
|
||||
"released_on": "18-12-2022",
|
||||
"md5sum": "5237713243bd3ba5dd20a5efc568f40d"
|
||||
},
|
||||
"1.2.0": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "b7036af",
|
||||
|
|
@ -324,6 +360,12 @@
|
|||
}
|
||||
],
|
||||
"versions": {
|
||||
"1.0.1": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "64e8a5c",
|
||||
"released_on": "18-12-2022",
|
||||
"md5sum": "7807b532802d17b77a0017c46ac1cbfb"
|
||||
},
|
||||
"1.0.0": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "e994af5",
|
||||
|
|
@ -362,6 +404,12 @@
|
|||
}
|
||||
],
|
||||
"versions": {
|
||||
"1.0.1": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "64e8a5c",
|
||||
"released_on": "18-12-2022",
|
||||
"md5sum": "8efcf38604e5519d66a858cc38868641"
|
||||
},
|
||||
"1.0.0": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "e994af5",
|
||||
|
|
@ -434,7 +482,7 @@
|
|||
}
|
||||
},
|
||||
"pro_unlocker": {
|
||||
"description": "Unlocks some pro-only features - custom colors, playlist maker, etc.\n(Please support the game developer if you can!)",
|
||||
"description": "Unlocks some pro-only features - custom colors, playlist maker, etc. (Please support the game developer if you can!)",
|
||||
"external_url": "",
|
||||
"authors": [
|
||||
{
|
||||
|
|
@ -510,7 +558,7 @@
|
|||
}
|
||||
},
|
||||
"bomb_radius_visualizer": {
|
||||
"description": "With this cutting edge technology, you precisely know\nhow close to the bomb you can tread.\nSupports modified blast radius values!",
|
||||
"description": "With this cutting edge technology, you precisely know how close to the bomb you can tread. Supports modified blast radius values!",
|
||||
"external_url": "",
|
||||
"authors": [
|
||||
{
|
||||
|
|
@ -565,6 +613,63 @@
|
|||
"md5sum": "a04c30c11a43443fe192fe70ad528f22"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autorun": {
|
||||
"description": "Run without holding any buttons. Made for beginners or players on mobile. Keeps your character maneuverable. Start running as usual to override.",
|
||||
"external_url": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "TheMikirog",
|
||||
"email": "",
|
||||
"discord": "TheMikirog#1984"
|
||||
}
|
||||
],
|
||||
"versions": {
|
||||
"1.0.0": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "cb2d952",
|
||||
"released_on": "01-01-2023",
|
||||
"md5sum": "22f54996dc55008267d09bf48a2cffe3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tnt_respawn_text": {
|
||||
"description": "Shows when a TNT box is about to respawn with non-intrusive text.",
|
||||
"external_url": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "TheMikirog",
|
||||
"email": "",
|
||||
"discord": "TheMikirog#1984"
|
||||
}
|
||||
],
|
||||
"versions": {
|
||||
"1.0.0": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "05ffa9f",
|
||||
"released_on": "14-01-2023",
|
||||
"md5sum": "cc1738b0326c9679453bdf1489ded483"
|
||||
}
|
||||
}
|
||||
},
|
||||
"allow_invisible_models": {
|
||||
"description": "Changing model to None will make it invisible instead of raising an exception.",
|
||||
"external_url": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rikko",
|
||||
"email": "rikkolovescats@proton.me",
|
||||
"discord": "Rikko#7383"
|
||||
}
|
||||
],
|
||||
"versions": {
|
||||
"1.0.0": {
|
||||
"api_version": 7,
|
||||
"commit_sha": "3221b3a",
|
||||
"released_on": "22-01-2023",
|
||||
"md5sum": "24913c665d05c3056c8ba390fe88155e"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ ssl._create_default_https_context = ssl._create_unverified_context
|
|||
def newconnect_to_party(address, port=43210, print_progress=False):
|
||||
global ip_add
|
||||
global p_port
|
||||
|
||||
|
||||
dd = _ba.get_connection_to_host_info()
|
||||
if (dd != {}):
|
||||
_ba.disconnect_from_host()
|
||||
|
|
@ -1724,7 +1724,7 @@ def fetchAccountInfo(account, loading_widget):
|
|||
if account in fdata:
|
||||
servers = fdata[account]
|
||||
url = f'https://{BCSSERVER}/player?key={base64.b64encode(account.encode("utf-8")).decode("utf-8")}&base64=true'
|
||||
|
||||
|
||||
data = urllib.request.urlopen(url)
|
||||
account_data = json.loads(data.read().decode('utf-8'))[0]
|
||||
pbid = account_data["pbid"]
|
||||
|
|
@ -2175,6 +2175,8 @@ class CustomAccountViewerWindow(viewer.AccountViewerWindow):
|
|||
ba.print_exception('Error displaying account info.')
|
||||
|
||||
# ba_meta export plugin
|
||||
|
||||
|
||||
class bySmoothy(ba.Plugin):
|
||||
def __init__(self):
|
||||
if _ba.env().get("build_number", 0) >= 20577:
|
||||
|
|
|
|||
15
plugins/utilities/allow_invisible_models.py
Normal file
15
plugins/utilities/allow_invisible_models.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# ba_meta require api 7
|
||||
import ba
|
||||
|
||||
original_getmodel = ba.getmodel
|
||||
|
||||
|
||||
def get_model_gracefully(model):
|
||||
if model is not None:
|
||||
return original_getmodel(model)
|
||||
|
||||
|
||||
# ba_meta export plugin
|
||||
class Main(ba.Plugin):
|
||||
def on_app_running(self):
|
||||
ba.getmodel = get_model_gracefully
|
||||
259
plugins/utilities/autorun.py
Normal file
259
plugins/utilities/autorun.py
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# ba_meta require api 7
|
||||
|
||||
"""
|
||||
AutoRun by TheMikirog
|
||||
Version 1
|
||||
|
||||
Run without holding any buttons. Made for beginners or players on mobile.
|
||||
Keeps your character maneuverable.
|
||||
Start running as usual to override.
|
||||
|
||||
Heavily commented for easy modding learning!
|
||||
|
||||
No Rights Reserved
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# Let's import everything we need and nothing more.
|
||||
import ba
|
||||
import bastd
|
||||
import math
|
||||
from ba._generated.enums import TimeType
|
||||
from bastd.actor.spaz import Spaz
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
"""
|
||||
This mod is much more "technical" than my other mods.
|
||||
I highly recommend checking out the code of this mod once you have a good understanding of programming.
|
||||
At the very least check out my other heavily commented mods like my Hot Potato gamemode. It's pretty dank!
|
||||
Normally you shouldn't flood your scripts with comments like that.
|
||||
I do it here to help people like you get the basic tools required to make your own mods similar to this one.
|
||||
If you write your own code, only comment what can't be easily inferred from reading the code alone.
|
||||
Consider this an interactive tutorial of sorts.
|
||||
|
||||
Let's start with the goal of this mod; the conception.
|
||||
If you play on mobile, the only way you get to run is if you press and hold any other action button like jump or punch.
|
||||
Basically, all action buttons do two things at once unless a gamemode disables one of those actions.
|
||||
Playing on a gamepad or keyboard gives you the luxury of a dedicated run button, which gives you much more control
|
||||
over your movement. This basically forces mobile players that are running to:
|
||||
- Punch and risk being open to attacks.
|
||||
- Kill all your momentum by jumping.
|
||||
- Using the bomb to run, but only after using that same button to throw an already held bomb.
|
||||
- Using an inconvenient out of the way grab button to avoid all of that hassle.
|
||||
It's a mess. Get a gamepad.
|
||||
This mod exists as an alternative to those who can't play on a gamepad, but don't want
|
||||
to be inconvenienced by running quirks if they JUST WANT TO PLAY.
|
||||
|
||||
The naive implementation of this would be to just running all the time, but here's the catch:
|
||||
Running makes turning less tight, which is the compromise for being really fast.
|
||||
If you want to have tighter turns, you'd release the run button for a split second, turn and press it again.
|
||||
Much easier and more convenient to do if you're on a gamepad.
|
||||
The goal of this mod is to replicate this behavior and making it automatic.
|
||||
My aim is to get the player moving as fast as possible without making it significantly harder to control.
|
||||
This is supposed to help mobile players, not handicap them.
|
||||
I can imagine the sweet relief of not being forced to babysit an action button just for running fast.
|
||||
Actually it should help gamepad users too, since holding your trigger can be exhausting
|
||||
or even impossible for those with physical disabilities.
|
||||
|
||||
For your information, I started writing this mod THREE times.
|
||||
Each time with the goal of trying out different ways of achieving my goals.
|
||||
I used the code and failures of the previous scripts to make the next one better.
|
||||
What you're seeing here is the final iteration; the finished product.
|
||||
Don't expect your code to look like this the first time, especially if you're trying something ballsy.
|
||||
You will fail, but don't be afraid to experiment.
|
||||
Only through experimentation you can forge a failure into a success.
|
||||
"""
|
||||
|
||||
# ba_meta export plugin
|
||||
|
||||
|
||||
class AutoRun(ba.Plugin):
|
||||
|
||||
# During my research and prototyping I figured I'd have to do some linear algebgra.
|
||||
# I didn't want to use libraries, since this is supposed to be a standalone mod.
|
||||
# Because of this I made certain functions from scratch that are easily accessible.
|
||||
# If you are curious over the details, look these up on the Internet.
|
||||
# I'll only briefly cover their purpose in the context of the mod.
|
||||
|
||||
# Here's the dot product function.
|
||||
# To keep it short, it returns the difference in angle between two vectors.
|
||||
# We're gonna use that knowledge to check how tight our turn is.
|
||||
# I'll touch on that later.
|
||||
def dot(vector_a, vector_b):
|
||||
return vector_a[0] * vector_b[0] + vector_a[1] * vector_b[1]
|
||||
|
||||
# This clamping function will make sure a certain value won't go above or below a certain threshold.
|
||||
# self.node.run attribute expects a value between 0-1, so this is one way of enforcing this.
|
||||
def clamp(num, min_value, max_value):
|
||||
num = max(min(num, max_value), min_value)
|
||||
return num
|
||||
|
||||
# A vector can be of any length, but we need them to be of length 1.
|
||||
# This vector normalization function changes the magnitude of a vector without changing its direction.
|
||||
def normalize(vector):
|
||||
length = math.hypot(vector[0], vector[1]) # Pythagoras says hi
|
||||
# Sometimes we'll get a [0,0] vector and dividing by 0 is iffy.
|
||||
# Let's leave the vector unchanged if that's the case.
|
||||
if length > 0:
|
||||
return [vector[0] / length, vector[1] / length]
|
||||
else:
|
||||
return vector
|
||||
|
||||
# We use a decorator to add extra code to existing code, increasing mod compatibility.
|
||||
# We're gonna use decorators ALOT in this mod.
|
||||
# Here I'm defining a new spaz init function that'll be replaced.
|
||||
def new_init(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
# Here's where we execute the original game's code, so it's not lost.
|
||||
# We want to add our code at the end of the existing code, so our code goes under that.
|
||||
func(*args, **kwargs)
|
||||
|
||||
# We define some variables that we need to keep track of.
|
||||
# For future reference, if you see args[0] anywhere, that is "self" in the original function.
|
||||
args[0].autorun_timer: ba.Timer | None = None
|
||||
args[0].autorun_override = False
|
||||
|
||||
# We wanna do our auto run calculations when the player moves their analog stick to make it responsive.
|
||||
# However doing this ONLY tracks changes in analog stick position and some bugs come up because of that.
|
||||
# For example moving via dpad on a gamepad can sometimes not execute the run at all.
|
||||
# To keep the behavior predictable, we also want to update our auto run functionality with a periodic timer.
|
||||
# We could ignore the update on analog stick movement, but then it feels terrible to play. We need both.
|
||||
# Update on analog movement for responsive controls, timer to foolproof everything else.
|
||||
|
||||
# To make our timer, we want to have access to our function responsible for doing the auto run logic.
|
||||
# The issue is that timers only work when a function is created within the context of the game.
|
||||
# Timer throws a tantrum if it references the run_update function, but NOT if that function is an intermediary.
|
||||
def spaz_autorun_update():
|
||||
AutoRun.run_update(args[0])
|
||||
|
||||
# We don't want this logic to be ran on bots, only players.
|
||||
# Check if we have a player assigned to that spaz. If we do, let's make our timer.
|
||||
if args[0].source_player:
|
||||
# And here's our timer.
|
||||
# It loops indefinitely thanks to the 'repeat' argument that is set to True.
|
||||
# Notice how it's the capital T Timer instead of the small letter.
|
||||
# That's important, because big T returns a timer object we can manipulate.
|
||||
# We need it assigned to a variable, because we have to delete it once it stops being relevant.
|
||||
args[0].autorun_timer = ba.Timer(
|
||||
0.1, spaz_autorun_update, timetype=TimeType.SIM, repeat=True)
|
||||
|
||||
return wrapper
|
||||
# Let's replace the original function with our modified version.
|
||||
bastd.actor.spaz.Spaz.__init__ = new_init(bastd.actor.spaz.Spaz.__init__)
|
||||
|
||||
# This is the bulk of our mod. Our run_update function.
|
||||
# The goal here is to change the self.node.run attribute of our character.
|
||||
# This attribute handles running behavior based on how far we pushed the running trigger.
|
||||
# 0 means not running and 1 means run trigger fully pressed.
|
||||
# On mobile it's always 0 and 1, but on gamepad you can have values between them
|
||||
# For example you can do a jog instead of a sprint.
|
||||
# We activate this function periodically via a timer and every time the player moves their analog stick.
|
||||
# The idea is to make it 1 when the player is running forward and make it 0
|
||||
# when the player makes the tightest turn possible.
|
||||
# We also want to account for how far the analog stick is pushed.
|
||||
def run_update(self) -> None:
|
||||
# Let's not run this code if our character does not exist or the player decides to run "manually".
|
||||
if not self.node or self.autorun_override:
|
||||
return
|
||||
|
||||
# Let's read our player's analog stick.
|
||||
# Notice how the vertical direction is inverted (there's a minus in front of the variable).
|
||||
# We want the directions to corespond to the game world.
|
||||
vertical = -self.node.move_up_down
|
||||
horizontal = self.node.move_left_right
|
||||
movement_vector = [horizontal, vertical]
|
||||
|
||||
# Get our character's facing direction
|
||||
facing_direction = (self.node.position[0] - self.node.position_forward[0],
|
||||
self.node.position[2] - self.node.position_forward[2])
|
||||
# We want our character's facing direction to be a normalized vector (magnitude of 1).
|
||||
facing_direction = AutoRun.normalize(facing_direction)
|
||||
|
||||
# We don't want to run our code if the player has their analog stick in a neutral position.
|
||||
if movement_vector == [0.0, 0.0]:
|
||||
return
|
||||
|
||||
# Get the difference between our current facing direction and where we plan on moving towards.
|
||||
# Check the dot function higher up in the script for details.
|
||||
dot = AutoRun.dot(facing_direction, AutoRun.normalize(movement_vector))
|
||||
if dot > 0.0:
|
||||
# Our dot value ranges from -1 to 1.
|
||||
# We want it from 0 to 1.
|
||||
# 0 being 180 degree turn, 1 being running exactly straight.
|
||||
dot = (dot + 1) / 2
|
||||
|
||||
# Let's read how far our player pushed his stick. 1 being full tilt, 0 being neutral.
|
||||
# Heres our homie Pythagoras once again
|
||||
run_power = math.hypot(movement_vector[0], movement_vector[1])
|
||||
|
||||
# I noticed the player starts running too fast if the stick is pushed half-way.
|
||||
# I changed the linear scale to be exponential.
|
||||
# easings.net is a great website that shows you different ways of converting a linear curve to some other kind.
|
||||
# Here I used the EaseInQuad easing, which is just raising the value to the power of 2.
|
||||
# This should make half-way pushes less severe.
|
||||
run_power = pow(run_power, 2)
|
||||
|
||||
# Just in case let's clamp our value from 0 to 1.
|
||||
run_power = AutoRun.clamp(run_power, 0.0, 1.0)
|
||||
|
||||
# Here we combine our dot result with how far we pushed our stick to get the final running value.
|
||||
# Clamping from 0 to 1 for good measure.
|
||||
self.node.run = AutoRun.clamp(run_power * dot, 0.0, 1.0)
|
||||
|
||||
# This function is called every time we want to run or touch a running trigger.
|
||||
# We have our auto run stuff, but we also want for our mod to play nice with the current running behavior.
|
||||
# We also want this to work with my Quickturn mod.
|
||||
def new_onrun(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
# When we hold an action button or press our running trigger at any point, our mod should stop interfering.
|
||||
# This won't work if your gamepad has borked triggers though.
|
||||
args[0].autorun_override = args[1]
|
||||
# Here's our original unchanged function
|
||||
func(*args, **kwargs)
|
||||
return wrapper
|
||||
# We replace the character running function with our modified version.
|
||||
bastd.actor.spaz.Spaz.on_run = new_onrun(bastd.actor.spaz.Spaz.on_run)
|
||||
|
||||
# There's two function that are called when our player pushes the analog stick - two for each axis.
|
||||
# Here's for the vertical axis.
|
||||
def new_updown(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
# Original function
|
||||
func(*args, **kwargs)
|
||||
# If we're not holding the run button and we're a player, run our auto run behavior.
|
||||
if not args[0].autorun_override and args[0].source_player:
|
||||
AutoRun.run_update(args[0])
|
||||
return wrapper
|
||||
# You get the idea.
|
||||
bastd.actor.spaz.Spaz.on_move_up_down = new_updown(bastd.actor.spaz.Spaz.on_move_up_down)
|
||||
|
||||
# Let's do the same for our horizontal axis.
|
||||
# Second verse same as the first.
|
||||
def new_leftright(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
func(*args, **kwargs)
|
||||
if not args[0].autorun_override and args[0].source_player:
|
||||
AutoRun.run_update(args[0])
|
||||
return wrapper
|
||||
bastd.actor.spaz.Spaz.on_move_left_right = new_leftright(
|
||||
bastd.actor.spaz.Spaz.on_move_left_right)
|
||||
|
||||
# There's one downside to the looping timer - it runs constantly even if the player is dead.
|
||||
# We don't want to waste computational power on something like that.
|
||||
# Let's kill our timer when the player dies.
|
||||
def new_handlemessage(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
# Only react to the death message.
|
||||
if isinstance(args[1], ba.DieMessage):
|
||||
# Kill the timer.
|
||||
args[0].autorun_timer = None
|
||||
# Original function.
|
||||
func(*args, **kwargs)
|
||||
return wrapper
|
||||
bastd.actor.spaz.Spaz.handlemessage = new_handlemessage(bastd.actor.spaz.Spaz.handlemessage)
|
||||
|
|
@ -63,6 +63,7 @@ https://ballistica.net/discord
|
|||
"""
|
||||
BCSURL = 'https://bcsserver.bombsquad.ga/bannedservers'
|
||||
|
||||
|
||||
def is_game_version_lower_than(version):
|
||||
"""
|
||||
Returns a boolean value indicating whether the current game
|
||||
|
|
@ -79,6 +80,7 @@ if is_game_version_lower_than("1.7.7"):
|
|||
else:
|
||||
ba_internal = ba.internal
|
||||
|
||||
|
||||
def updateBannedServersCache():
|
||||
response = None
|
||||
config = ba.app.config
|
||||
|
|
@ -508,61 +510,62 @@ def popup_menu_selected_choice(self, window: popup.PopupMenu,
|
|||
|
||||
|
||||
def _update_party_lists(self) -> None:
|
||||
if not self._party_lists_dirty:
|
||||
return
|
||||
starttime = time.time()
|
||||
config = ba.app.config
|
||||
bannedservers = config.get('Banned Servers',[])
|
||||
assert len(self._parties_sorted) == len(self._parties)
|
||||
if not self._party_lists_dirty:
|
||||
return
|
||||
starttime = time.time()
|
||||
config = ba.app.config
|
||||
bannedservers = config.get('Banned Servers', [])
|
||||
assert len(self._parties_sorted) == len(self._parties)
|
||||
|
||||
self._parties_sorted.sort(
|
||||
key=lambda p: (
|
||||
p[1].ping if p[1].ping is not None else 999999.0,
|
||||
p[1].index,
|
||||
)
|
||||
self._parties_sorted.sort(
|
||||
key=lambda p: (
|
||||
p[1].ping if p[1].ping is not None else 999999.0,
|
||||
p[1].index,
|
||||
)
|
||||
)
|
||||
|
||||
# If signed out or errored, show no parties.
|
||||
if (
|
||||
ba.internal.get_v1_account_state() != 'signed_in'
|
||||
or not self._have_valid_server_list
|
||||
):
|
||||
self._parties_displayed = {}
|
||||
else:
|
||||
if self._filter_value:
|
||||
filterval = self._filter_value.lower()
|
||||
self._parties_displayed = {
|
||||
k: v
|
||||
for k, v in self._parties_sorted
|
||||
if (filterval in v.name.lower() or filterval in v.address) and (v.address not in bannedservers if ENABLE_SERVER_BANNING else True)
|
||||
}
|
||||
else:
|
||||
self._parties_displayed = {
|
||||
k: v
|
||||
for k, v in self._parties_sorted
|
||||
if (v.address not in bannedservers if ENABLE_SERVER_BANNING else True)
|
||||
}
|
||||
|
||||
# Any time our selection disappears from the displayed list, go back to
|
||||
# auto-selecting the top entry.
|
||||
if (
|
||||
self._selection is not None
|
||||
and self._selection.entry_key not in self._parties_displayed
|
||||
):
|
||||
self._have_user_selected_row = False
|
||||
|
||||
# Whenever the user hasn't selected something, keep the first visible
|
||||
# row selected.
|
||||
if not self._have_user_selected_row and self._parties_displayed:
|
||||
firstpartykey = next(iter(self._parties_displayed))
|
||||
self._selection = Selection(firstpartykey, SelectionComponent.NAME)
|
||||
|
||||
self._party_lists_dirty = False
|
||||
if DEBUG_PROCESSING:
|
||||
print(
|
||||
f'Sorted {len(self._parties_sorted)} parties in'
|
||||
f' {time.time()-starttime:.5f}s.'
|
||||
)
|
||||
|
||||
# If signed out or errored, show no parties.
|
||||
if (
|
||||
ba.internal.get_v1_account_state() != 'signed_in'
|
||||
or not self._have_valid_server_list
|
||||
):
|
||||
self._parties_displayed = {}
|
||||
else:
|
||||
if self._filter_value:
|
||||
filterval = self._filter_value.lower()
|
||||
self._parties_displayed = {
|
||||
k: v
|
||||
for k, v in self._parties_sorted
|
||||
if (filterval in v.name.lower() or filterval in v.address) and (v.address not in bannedservers if ENABLE_SERVER_BANNING else True)
|
||||
}
|
||||
else:
|
||||
self._parties_displayed = {
|
||||
k: v
|
||||
for k, v in self._parties_sorted
|
||||
if (v.address not in bannedservers if ENABLE_SERVER_BANNING else True)
|
||||
}
|
||||
|
||||
# Any time our selection disappears from the displayed list, go back to
|
||||
# auto-selecting the top entry.
|
||||
if (
|
||||
self._selection is not None
|
||||
and self._selection.entry_key not in self._parties_displayed
|
||||
):
|
||||
self._have_user_selected_row = False
|
||||
|
||||
# Whenever the user hasn't selected something, keep the first visible
|
||||
# row selected.
|
||||
if not self._have_user_selected_row and self._parties_displayed:
|
||||
firstpartykey = next(iter(self._parties_displayed))
|
||||
self._selection = Selection(firstpartykey, SelectionComponent.NAME)
|
||||
|
||||
self._party_lists_dirty = False
|
||||
if DEBUG_PROCESSING:
|
||||
print(
|
||||
f'Sorted {len(self._parties_sorted)} parties in'
|
||||
f' {time.time()-starttime:.5f}s.'
|
||||
)
|
||||
|
||||
def replace():
|
||||
manualtab.ManualGatherTab._build_favorites_tab = new_build_favorites_tab
|
||||
|
|
@ -697,5 +700,5 @@ class InitalRun(ba.Plugin):
|
|||
def __init__(self):
|
||||
replace()
|
||||
config = ba.app.config
|
||||
if config["launchCount"]% 5 ==0:
|
||||
if config["launchCount"] % 5 == 0:
|
||||
updateBannedServersCache()
|
||||
|
|
|
|||
315
plugins/utilities/random_join.py
Normal file
315
plugins/utilities/random_join.py
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
import _ba
|
||||
import ba
|
||||
import ba.internal
|
||||
import random
|
||||
from bastd.ui.gather.publictab import PublicGatherTab, PartyEntry, PingThread
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
ClassType = TypeVar('ClassType')
|
||||
MethodType = TypeVar('Methodtype')
|
||||
|
||||
|
||||
def override(cls: ClassType) -> Callable[[MethodType], MethodType]:
|
||||
def decorator(newfunc: MethodType) -> MethodType:
|
||||
funcname = newfunc.__code__.co_name
|
||||
if hasattr(cls, funcname):
|
||||
oldfunc = getattr(cls, funcname)
|
||||
setattr(cls, f'_old_{funcname}', oldfunc)
|
||||
|
||||
setattr(cls, funcname, newfunc)
|
||||
return newfunc
|
||||
|
||||
return decorator
|
||||
|
||||
# Can this stuff break mro? (P.S. yes, so we're not using super() anymore).
|
||||
# Although it gives nice auto-completion.
|
||||
# And anyways, why not just GatherPublicTab = NewGatherPublicTab?
|
||||
# But hmm, if we imagine someone used `from blah.blah import Blah`, using
|
||||
# `blah.Blah = NewBlah` AFTERWARDS would be meaningless.
|
||||
|
||||
|
||||
class NewPublicGatherTab(PublicGatherTab, PingThread):
|
||||
|
||||
@override(PublicGatherTab)
|
||||
def _build_join_tab(self, region_width: float,
|
||||
region_height: float,
|
||||
oldfunc: Callable = None) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
self._old__build_join_tab(region_width, region_height)
|
||||
|
||||
# Copy-pasted from original function.
|
||||
c_width = region_width
|
||||
c_height = region_height - 20
|
||||
sub_scroll_height = c_height - 125
|
||||
sub_scroll_width = 830
|
||||
v = c_height - 35
|
||||
v -= 60
|
||||
|
||||
self._random_join_button = ba.buttonwidget(
|
||||
parent=self._container,
|
||||
label='random',
|
||||
size=(90, 45),
|
||||
position=(710, v + 10),
|
||||
on_activate_call=ba.WeakCall(self._join_random_server),
|
||||
)
|
||||
ba.widget(edit=self._random_join_button, up_widget=self._host_text,
|
||||
left_widget=self._filter_text)
|
||||
|
||||
# We could place it somewhere under plugin settings which is kind of
|
||||
# official way to customise plugins. Although it's too deep:
|
||||
# Gather Window -> Main Menu -> Settings -> Advanced -(scroll)->
|
||||
# Plugins -(scroll probably)-> RandomJoin Settings.
|
||||
self._random_join_settings_button = ba.buttonwidget(
|
||||
parent=self._container,
|
||||
icon=ba.gettexture('settingsIcon'),
|
||||
size=(40, 40),
|
||||
position=(820, v + 13),
|
||||
on_activate_call=ba.WeakCall(self._show_random_join_settings),
|
||||
)
|
||||
|
||||
@override(PublicGatherTab)
|
||||
def _show_random_join_settings(self) -> None:
|
||||
RandomJoinSettingsPopup(
|
||||
origin_widget=self._random_join_settings_button)
|
||||
|
||||
@override(PublicGatherTab)
|
||||
def _get_parties_list(self) -> list[PartyEntry]:
|
||||
if (self._parties_sorted and
|
||||
(randomjoin.maximum_ping == 9999 or
|
||||
# Ensure that we've pinged at least 10%.
|
||||
len([p for k, p in self._parties_sorted
|
||||
if p.ping is not None]) > len(self._parties_sorted) / 10)):
|
||||
randomjoin.cached_parties = [p for k, p in self._parties_sorted]
|
||||
return randomjoin.cached_parties
|
||||
|
||||
@override(PublicGatherTab)
|
||||
def _join_random_server(self) -> None:
|
||||
name_prefixes = set()
|
||||
parties = [p for p in self._get_parties_list() if
|
||||
(p.size >= randomjoin.minimum_players
|
||||
and p.size < p.size_max and (randomjoin.maximum_ping == 9999
|
||||
or (p.ping is not None
|
||||
and p.ping <= randomjoin.maximum_ping)))]
|
||||
|
||||
if not parties:
|
||||
ba.screenmessage('No suitable servers found; wait',
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
for party in parties:
|
||||
name_prefixes.add(party.name[:6])
|
||||
|
||||
random.choice(list(name_prefixes))
|
||||
|
||||
party = random.choice(
|
||||
[p for p in parties if p.name[:6] in name_prefixes])
|
||||
|
||||
ba.internal.connect_to_party(party.address, party.port)
|
||||
|
||||
|
||||
class RandomJoinSettingsPopup(ba.Window):
|
||||
def __init__(self, origin_widget: ba.Widget) -> None:
|
||||
c_width = 600
|
||||
c_height = 400
|
||||
uiscale = ba.app.ui.uiscale
|
||||
super().__init__(root_widget=ba.containerwidget(
|
||||
scale=(
|
||||
1.8
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.55
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.0
|
||||
),
|
||||
scale_origin_stack_offset=origin_widget.get_screen_space_center(),
|
||||
stack_offset=(0, -10)
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else (0, 15)
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else (0, 0),
|
||||
size=(c_width, c_height),
|
||||
transition='in_scale',
|
||||
))
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
text='Random Join Settings',
|
||||
scale=1.5,
|
||||
color=(0.6, 1.0, 0.6),
|
||||
maxwidth=c_width * 0.8,
|
||||
position=(c_width * 0.5, c_height - 60),
|
||||
)
|
||||
|
||||
v = c_height - 120
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
size=(0, 0),
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
text='Maximum ping',
|
||||
maxwidth=c_width * 0.3,
|
||||
position=(c_width * 0.4, v),
|
||||
)
|
||||
self._maximum_ping_edit = ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
size=(c_width * 0.3, 40),
|
||||
h_align='left',
|
||||
v_align='center',
|
||||
text=str(randomjoin.maximum_ping),
|
||||
editable=True,
|
||||
description='Maximum ping (ms)',
|
||||
position=(c_width * 0.6, v - 20),
|
||||
autoselect=True,
|
||||
max_chars=4,
|
||||
)
|
||||
v -= 60
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
size=(0, 0),
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
text='Minimum players',
|
||||
maxwidth=c_width * 0.3,
|
||||
position=(c_width * 0.4, v),
|
||||
)
|
||||
self._minimum_players_edit = ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
size=(c_width * 0.3, 40),
|
||||
h_align='left',
|
||||
v_align='center',
|
||||
text=str(randomjoin.minimum_players),
|
||||
editable=True,
|
||||
description='Minimum number of players',
|
||||
position=(c_width * 0.6, v - 20),
|
||||
autoselect=True,
|
||||
max_chars=4,
|
||||
)
|
||||
v -= 60
|
||||
|
||||
# Cancel button.
|
||||
self.cancel_button = btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
label=ba.Lstr(resource='cancelText'),
|
||||
size=(180, 60),
|
||||
color=(1.0, 0.2, 0.2),
|
||||
position=(40, 30),
|
||||
on_activate_call=self._cancel,
|
||||
autoselect=True,
|
||||
)
|
||||
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
|
||||
|
||||
# Save button.
|
||||
self.savebtn = btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
label=ba.Lstr(resource='saveText'),
|
||||
size=(180, 60),
|
||||
position=(c_width - 200, 30),
|
||||
on_activate_call=self._save,
|
||||
autoselect=True,
|
||||
)
|
||||
ba.containerwidget(edit=self._root_widget, start_button=btn)
|
||||
|
||||
def _save(self) -> None:
|
||||
errored = False
|
||||
minimum_players: int | None = None
|
||||
maximum_ping: int | None = None
|
||||
try:
|
||||
minimum_players = int(
|
||||
ba.textwidget(query=self._minimum_players_edit))
|
||||
except ValueError:
|
||||
ba.screenmessage('"Minimum players" should be integer',
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
errored = True
|
||||
try:
|
||||
maximum_ping = int(
|
||||
ba.textwidget(query=self._maximum_ping_edit))
|
||||
except ValueError:
|
||||
ba.screenmessage('"Maximum ping" should be integer',
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
errored = True
|
||||
if errored:
|
||||
return
|
||||
|
||||
assert minimum_players is not None
|
||||
assert maximum_ping is not None
|
||||
|
||||
if minimum_players < 0:
|
||||
ba.screenmessage('"Minimum players" should be at least 0',
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
errored = True
|
||||
|
||||
if maximum_ping <= 0:
|
||||
ba.screenmessage('"Maximum ping" should be greater than 0',
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
ba.screenmessage('(use 9999 as dont-care value)',
|
||||
color=(1, 0, 0))
|
||||
errored = True
|
||||
|
||||
if errored:
|
||||
return
|
||||
|
||||
randomjoin.maximum_ping = maximum_ping
|
||||
randomjoin.minimum_players = minimum_players
|
||||
|
||||
randomjoin.commit_config()
|
||||
ba.playsound(ba.getsound('shieldUp'))
|
||||
self._transition_out()
|
||||
|
||||
def _cancel(self) -> None:
|
||||
ba.playsound(ba.getsound('shieldDown'))
|
||||
self._transition_out()
|
||||
|
||||
def _transition_out(self) -> None:
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_scale')
|
||||
|
||||
|
||||
class RandomJoin:
|
||||
def __init__(self) -> None:
|
||||
self.cached_parties: list[PartyEntry] = []
|
||||
self.maximum_ping: int = 9999
|
||||
self.minimum_players: int = 2
|
||||
self.load_config()
|
||||
|
||||
def load_config(self) -> None:
|
||||
cfg = ba.app.config.get('Random Join', {
|
||||
'maximum_ping': self.maximum_ping,
|
||||
'minimum_players': self.minimum_players,
|
||||
})
|
||||
try:
|
||||
self.maximum_ping = cfg['maximum_ping']
|
||||
self.minimum_players = cfg['minimum_players']
|
||||
except KeyError:
|
||||
ba.screenmessage('Error: RandomJoin config is broken, resetting..',
|
||||
color=(1, 0, 0), log=True)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
self.commit_config()
|
||||
|
||||
def commit_config(self) -> None:
|
||||
ba.app.config['Random Join'] = {
|
||||
'maximum_ping': self.maximum_ping,
|
||||
'minimum_players': self.minimum_players,
|
||||
}
|
||||
ba.app.config.commit()
|
||||
|
||||
|
||||
randomjoin = RandomJoin()
|
||||
|
||||
|
||||
# ba_meta require api 7
|
||||
# ba_meta export ba.Plugin
|
||||
class RandomJoinPlugin(ba.Plugin):
|
||||
def on_app_running(self) -> None:
|
||||
# I feel bad that all patching logic happens not here.
|
||||
pass
|
||||
|
|
@ -1,10 +1,24 @@
|
|||
"""
|
||||
Plugin by LoupGarou a.k.a Loup/Soup
|
||||
Discord →ʟօʊքɢǟʀօʊ#3063
|
||||
Share replays easily with your friends or have a backup
|
||||
|
||||
Exported replays are stored in replays folder which is inside mods folder
|
||||
You can start sharing replays by opening the watch window and going to share replay tab
|
||||
|
||||
Feel free to let me know if you use this plugin,i love to hear that :)
|
||||
|
||||
Message me in discord if you find some bug
|
||||
Use this code for your experiments or plugin but please dont rename this plugin and distribute with your name,don't do that,its bad'
|
||||
"""
|
||||
|
||||
# ba_meta require api 7
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, cast
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence, Callable, List, Dict, Tuple, Optional, Union
|
||||
|
||||
from os import listdir, mkdir, path, sep
|
||||
from os import listdir, mkdir, path, sep, remove
|
||||
from shutil import copy, copytree
|
||||
|
||||
import ba
|
||||
|
|
@ -12,11 +26,23 @@ import _ba
|
|||
from enum import Enum
|
||||
from bastd.ui.tabs import TabRow
|
||||
from bastd.ui.confirm import ConfirmWindow
|
||||
from bastd.ui.watch import WatchWindow as ww
|
||||
from bastd.ui.watch import WatchWindow
|
||||
from bastd.ui.popup import PopupWindow
|
||||
|
||||
# mod by ʟօʊքɢǟʀօʊ
|
||||
# export replays to mods folder and share with your friends or have a backup
|
||||
|
||||
title = "SHARE REPLAY"
|
||||
internal_dir = _ba.get_replays_dir()+sep
|
||||
external_dir = path.join(_ba.env()["python_directory_user"], "replays"+sep)
|
||||
uiscale = ba.app.ui.uiscale
|
||||
|
||||
# colors
|
||||
pink = (1, 0.2, 0.8)
|
||||
green = (0.4, 1, 0.4)
|
||||
red = (1, 0, 0)
|
||||
blue = (0.26, 0.65, 0.94)
|
||||
blue_highlight = (0.4, 0.7, 1)
|
||||
b_color = (0.6, 0.53, 0.63)
|
||||
b_textcolor = (0.75, 0.7, 0.8)
|
||||
|
||||
|
||||
def Print(*args, color=None, top=None):
|
||||
|
|
@ -35,174 +61,30 @@ def cprint(*args):
|
|||
_ba.chatmessage(out)
|
||||
|
||||
|
||||
title = "SHARE REPLAY"
|
||||
internal_dir = _ba.get_replays_dir()+sep
|
||||
external_dir = path.join(_ba.env()["python_directory_user"], "replays"+sep)
|
||||
|
||||
# colors
|
||||
pink = (1, 0.2, 0.8)
|
||||
green = (0.4, 1, 0.4)
|
||||
red = (1, 0, 0)
|
||||
blue = (0.26, 0.65, 0.94)
|
||||
blue_highlight = (0.4, 0.7, 1)
|
||||
|
||||
if not path.exists(external_dir):
|
||||
mkdir(external_dir)
|
||||
Print("You are ready to share replays", color=pink)
|
||||
|
||||
|
||||
class Help(PopupWindow):
|
||||
def __init__(self):
|
||||
uiscale = ba.app.ui.uiscale
|
||||
self.width = 1000
|
||||
self.height = 300
|
||||
def override(cls: ClassType) -> Callable[[MethodType], MethodType]:
|
||||
def decorator(newfunc: MethodType) -> MethodType:
|
||||
funcname = newfunc.__code__.co_name
|
||||
if hasattr(cls, funcname):
|
||||
oldfunc = getattr(cls, funcname)
|
||||
setattr(cls, f'_old_{funcname}', oldfunc)
|
||||
|
||||
PopupWindow.__init__(self,
|
||||
position=(0.0, 0.0),
|
||||
size=(self.width, self.height),
|
||||
scale=1.2,)
|
||||
setattr(cls, funcname, newfunc)
|
||||
return newfunc
|
||||
|
||||
ba.containerwidget(edit=self.root_widget, on_outside_click_call=self.close)
|
||||
ba.textwidget(parent=self.root_widget, position=(0, self.height * 0.6),
|
||||
text=f">Replays are exported to\n {external_dir}\n>Copy replays to the above folder to be able to import them into the game\n>I would live to hear from you,meet me on discord\n -LoupGarou(author)")
|
||||
|
||||
def close(self):
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
ba.containerwidget(edit=self.root_widget, transition="out_right",)
|
||||
return decorator
|
||||
|
||||
|
||||
class SettingWindow():
|
||||
def __init__(self):
|
||||
self.draw_ui()
|
||||
ba.containerwidget(edit=self.root, cancel_button=self.close_button)
|
||||
self.selected_name = None
|
||||
# setting tab when window opens
|
||||
self.on_tab_select(self.TabId.INTERNAL)
|
||||
self.tab_id = self.TabId.INTERNAL
|
||||
|
||||
class TabId(Enum):
|
||||
INTERNAL = "internal"
|
||||
EXTERNAL = "external"
|
||||
class CommonUtilities:
|
||||
|
||||
def sync_confirmation(self):
|
||||
ConfirmWindow(text="WARNING:\nreplays with same name in mods folder\n will be overwritten",
|
||||
action=self.sync, cancel_is_selected=True)
|
||||
|
||||
def on_select_text(self, widget, name):
|
||||
existing_widgets = self.scroll2.get_children()
|
||||
for i in existing_widgets:
|
||||
ba.textwidget(edit=i, color=(1, 1, 1))
|
||||
ba.textwidget(edit=widget, color=(1, 1, 0))
|
||||
self.selected_name = name
|
||||
|
||||
def on_tab_select(self, tab_id):
|
||||
self.tab_id = tab_id
|
||||
if tab_id == self.TabId.INTERNAL:
|
||||
dir_list = listdir(internal_dir)
|
||||
ba.buttonwidget(edit=self.share_button, label="EXPORT", icon=ba.gettexture("upButton"),)
|
||||
else:
|
||||
dir_list = listdir(external_dir)
|
||||
ba.buttonwidget(edit=self.share_button, label="IMPORT",
|
||||
icon=ba.gettexture("downButton"),)
|
||||
self.tab_row.update_appearance(tab_id)
|
||||
dir_list = sorted(dir_list)
|
||||
existing_widgets = self.scroll2.get_children()
|
||||
if existing_widgets:
|
||||
for i in existing_widgets:
|
||||
i.delete()
|
||||
height = 900
|
||||
# making textwidgets for all replays
|
||||
for i in dir_list:
|
||||
height -= 40
|
||||
a = i
|
||||
i = ba.textwidget(
|
||||
parent=self.scroll2,
|
||||
size=(500, 50),
|
||||
text=i.split(".")[0],
|
||||
position=(10, height),
|
||||
selectable=True,
|
||||
max_chars=40,
|
||||
corner_scale=1.3,
|
||||
click_activate=True,)
|
||||
ba.textwidget(edit=i, on_activate_call=ba.Call(self.on_select_text, i, a))
|
||||
|
||||
def draw_ui(self):
|
||||
self.uiscale = ba.app.ui.uiscale
|
||||
self.root = ba.Window(ba.containerwidget(
|
||||
size=(900, 670), on_outside_click_call=self.close, transition="in_right")).get_root_widget()
|
||||
|
||||
self.close_button = ba.buttonwidget(
|
||||
parent=self.root,
|
||||
position=(90, 560),
|
||||
button_type='backSmall',
|
||||
size=(60, 60),
|
||||
label=ba.charstr(ba.SpecialChar.BACK),
|
||||
scale=1.5,
|
||||
on_activate_call=self.close)
|
||||
|
||||
ba.textwidget(
|
||||
parent=self.root,
|
||||
size=(200, 100),
|
||||
position=(350, 550),
|
||||
scale=2,
|
||||
selectable=False,
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
text=title,
|
||||
color=green)
|
||||
|
||||
ba.buttonwidget(
|
||||
parent=self.root,
|
||||
position=(650, 580),
|
||||
size=(35, 35),
|
||||
texture=ba.gettexture("achievementEmpty"),
|
||||
label="",
|
||||
on_activate_call=Help)
|
||||
|
||||
tabdefs = [(self.TabId.INTERNAL, 'INTERNAL'), (self.TabId.EXTERNAL, "EXTERNAL")]
|
||||
self.tab_row = TabRow(self.root, tabdefs, pos=(150, 500-5),
|
||||
size=(500, 300), on_select_call=self.on_tab_select)
|
||||
|
||||
self.share_button = ba.buttonwidget(
|
||||
parent=self.root,
|
||||
position=(720, 400),
|
||||
size=(110, 50),
|
||||
scale=1.5,
|
||||
button_type="square",
|
||||
label="EXPORT",
|
||||
text_scale=2,
|
||||
icon=ba.gettexture("upButton"),
|
||||
on_activate_call=self.share)
|
||||
|
||||
sync_button = ba.buttonwidget(
|
||||
parent=self.root,
|
||||
position=(720, 300),
|
||||
size=(110, 50),
|
||||
scale=1.5,
|
||||
button_type="square",
|
||||
label="SYNC",
|
||||
text_scale=2,
|
||||
icon=ba.gettexture("ouyaYButton"),
|
||||
on_activate_call=self.sync_confirmation)
|
||||
|
||||
scroll = ba.scrollwidget(
|
||||
parent=self.root,
|
||||
size=(600, 400),
|
||||
position=(100, 100),)
|
||||
self.scroll2 = ba.columnwidget(parent=scroll, size=(
|
||||
500, 900))
|
||||
|
||||
def share(self):
|
||||
if self.selected_name is None:
|
||||
Print("Select a replay", color=red)
|
||||
return
|
||||
if self.tab_id == self.TabId.INTERNAL:
|
||||
self.export()
|
||||
else:
|
||||
self.importx()
|
||||
|
||||
# image={"texture":ba.gettexture("bombColor"),"tint_texture":None,"tint_color":None,"tint2_color":None})
|
||||
|
||||
def sync(self):
|
||||
internal_list = listdir(internal_dir)
|
||||
external_list = listdir(external_dir)
|
||||
|
|
@ -215,13 +97,233 @@ class SettingWindow():
|
|||
copy(external_dir+sep+i, internal_dir+sep+i)
|
||||
Print("Synced all replays", color=pink)
|
||||
|
||||
def export(self):
|
||||
copy(internal_dir+self.selected_name, external_dir+self.selected_name)
|
||||
Print(self.selected_name[0:-4]+" exported", top=True, color=pink)
|
||||
def _copy(self, selected_replay, tab_id):
|
||||
if selected_replay is None:
|
||||
Print("Select a replay", color=red)
|
||||
return
|
||||
elif tab_id == MyTabId.INTERNAL:
|
||||
copy(internal_dir+selected_replay, external_dir+selected_replay)
|
||||
Print(selected_replay[0:-4]+" exported", top=True, color=pink)
|
||||
else:
|
||||
copy(external_dir+selected_replay, internal_dir+selected_replay)
|
||||
Print(selected_replay[0:-4]+" imported", top=True, color=green)
|
||||
|
||||
def importx(self):
|
||||
copy(external_dir+self.selected_name, internal_dir+self.selected_name)
|
||||
Print(self.selected_name[0:-4]+" imported", top=True, color=green)
|
||||
def delete_replay(self, selected_replay, tab_id, cls_inst):
|
||||
if selected_replay is None:
|
||||
Print("Select a replay", color=red)
|
||||
return
|
||||
|
||||
def do_it():
|
||||
if tab_id == MyTabId.INTERNAL:
|
||||
remove(internal_dir+selected_replay)
|
||||
elif tab_id == MyTabId.EXTERNAL:
|
||||
remove(external_dir+selected_replay)
|
||||
cls_inst.on_tab_select(tab_id) # updating the tab
|
||||
Print(selected_replay[0:-4]+" was deleted", top=True, color=red)
|
||||
ConfirmWindow(text=f"Delete \"{selected_replay.split('.')[0]}\" \nfrom {'internal directory' if tab_id==MyTabId.INTERNAL else 'external directory'}?",
|
||||
action=do_it, cancel_is_selected=True)
|
||||
|
||||
|
||||
CommonUtils = CommonUtilities()
|
||||
|
||||
|
||||
class MyTabId(Enum):
|
||||
INTERNAL = "internal"
|
||||
EXTERNAL = "external"
|
||||
SHARE_REPLAYS = "share_replay"
|
||||
|
||||
|
||||
class Help(PopupWindow):
|
||||
def __init__(self):
|
||||
self.width = 1200
|
||||
self.height = 250
|
||||
self.root_widget = ba.Window(ba.containerwidget(
|
||||
size=(self.width, self.height), on_outside_click_call=self.close, transition="in_right")).get_root_widget()
|
||||
|
||||
ba.containerwidget(edit=self.root_widget, on_outside_click_call=self.close)
|
||||
ba.textwidget(parent=self.root_widget, position=(0, self.height * 0.7), corner_scale=1.2, color=green,
|
||||
text=f"»Replays are exported to\n {external_dir}\n»Copy replays to the above folder to be able to import them into the game\n»I would love to hear from you,meet me on discord\n -LoupGarou(author)")
|
||||
|
||||
def close(self):
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
ba.containerwidget(edit=self.root_widget, transition="out_right",)
|
||||
|
||||
|
||||
class ShareTabUi(WatchWindow):
|
||||
def __init__(self, root_widget=None):
|
||||
self.tab_id = MyTabId.INTERNAL
|
||||
self.selected_replay = None
|
||||
|
||||
if root_widget is None:
|
||||
self.root = ba.Window(ba.containerwidget(
|
||||
size=(1000, 600), on_outside_click_call=self.close, transition="in_right")).get_root_widget()
|
||||
|
||||
else:
|
||||
self.root = root_widget
|
||||
|
||||
self.draw_ui()
|
||||
|
||||
def on_select_text(self, widget, name):
|
||||
existing_widgets = self.scroll2.get_children()
|
||||
for i in existing_widgets:
|
||||
ba.textwidget(edit=i, color=(1, 1, 1))
|
||||
ba.textwidget(edit=widget, color=(1.0, 1, 0.4))
|
||||
self.selected_replay = name
|
||||
|
||||
def on_tab_select(self, tab_id):
|
||||
self.selected_replay = None
|
||||
self.tab_id = tab_id
|
||||
t_scale = 1.6
|
||||
|
||||
if tab_id == MyTabId.INTERNAL:
|
||||
dir_list = listdir(internal_dir)
|
||||
ba.buttonwidget(edit=self.share_button, label="Export\nReplay")
|
||||
else:
|
||||
dir_list = listdir(external_dir)
|
||||
ba.buttonwidget(edit=self.share_button, label="Import\nReplay")
|
||||
|
||||
self.tab_row.update_appearance(tab_id)
|
||||
dir_list = sorted(dir_list)
|
||||
existing_widgets = self.scroll2.get_children()
|
||||
if existing_widgets: # deleting textwidgets from old tab
|
||||
for i in existing_widgets:
|
||||
i.delete()
|
||||
height = 900
|
||||
for i in dir_list: # making textwidgets for all replays
|
||||
height -= 50
|
||||
a = i
|
||||
i = ba.textwidget(
|
||||
parent=self.scroll2,
|
||||
size=(self._my_replays_scroll_width/t_scale, 30),
|
||||
text=i.split(".")[0],
|
||||
position=(20, height),
|
||||
selectable=True,
|
||||
max_chars=40,
|
||||
corner_scale=t_scale,
|
||||
click_activate=True,
|
||||
always_highlight=True,)
|
||||
ba.textwidget(edit=i, on_activate_call=ba.Call(self.on_select_text, i, a))
|
||||
|
||||
def draw_ui(self):
|
||||
self._r = 'watchWindow'
|
||||
x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
|
||||
scroll_buffer_h = 130 + 2 * x_inset
|
||||
self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
|
||||
self._height = (
|
||||
578
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 670
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 800)
|
||||
self._scroll_width = self._width - scroll_buffer_h
|
||||
self._scroll_height = self._height - 180
|
||||
#
|
||||
c_width = self._scroll_width
|
||||
c_height = self._scroll_height - 20
|
||||
sub_scroll_height = c_height - 63
|
||||
self._my_replays_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 = (
|
||||
107
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 142
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 190
|
||||
)
|
||||
b_space_extra = (
|
||||
0
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else -2
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else -5
|
||||
)
|
||||
|
||||
b_color = (0.6, 0.53, 0.63)
|
||||
b_textcolor = (0.75, 0.7, 0.8)
|
||||
btnv = (c_height - (48
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 45
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 40) - b_height)
|
||||
btnh = 40 if uiscale is ba.UIScale.SMALL else 40
|
||||
smlh = 190 if uiscale is ba.UIScale.SMALL else 225
|
||||
tscl = 1.0 if uiscale is ba.UIScale.SMALL else 1.2
|
||||
|
||||
stab_width = 500
|
||||
stab_height = 300
|
||||
stab_h = smlh
|
||||
|
||||
v -= sub_scroll_height + 23
|
||||
scroll = ba.scrollwidget(
|
||||
parent=self.root,
|
||||
position=(smlh, v),
|
||||
size=(sub_scroll_width, sub_scroll_height),
|
||||
)
|
||||
|
||||
self.scroll2 = ba.columnwidget(parent=scroll,
|
||||
size=(sub_scroll_width, sub_scroll_height))
|
||||
|
||||
tabdefs = [(MyTabId.INTERNAL, 'INTERNAL'), (MyTabId.EXTERNAL, "EXTERNAL")]
|
||||
self.tab_row = TabRow(self.root, tabdefs, pos=(stab_h, sub_scroll_height),
|
||||
size=(stab_width, stab_height), on_select_call=self.on_tab_select)
|
||||
|
||||
helpbtn_space = 20
|
||||
helpbtn_v = stab_h+stab_width+helpbtn_space+120
|
||||
helpbtn_h = sub_scroll_height+helpbtn_space
|
||||
|
||||
ba.buttonwidget(
|
||||
parent=self.root,
|
||||
position=(helpbtn_v, helpbtn_h),
|
||||
size=(35, 35),
|
||||
button_type="square",
|
||||
label="?",
|
||||
text_scale=1.5,
|
||||
color=b_color,
|
||||
textcolor=b_textcolor,
|
||||
on_activate_call=Help)
|
||||
|
||||
def call_copy(): return CommonUtils._copy(self.selected_replay, self.tab_id)
|
||||
self.share_button = ba.buttonwidget(
|
||||
parent=self.root,
|
||||
size=(b_width, b_height),
|
||||
position=(btnh, btnv),
|
||||
button_type="square",
|
||||
label="Export\nReplay",
|
||||
text_scale=tscl,
|
||||
color=b_color,
|
||||
textcolor=b_textcolor,
|
||||
on_activate_call=call_copy)
|
||||
|
||||
btnv -= b_height + b_space_extra
|
||||
sync_button = ba.buttonwidget(
|
||||
parent=self.root,
|
||||
size=(b_width, b_height),
|
||||
position=(btnh, btnv),
|
||||
button_type="square",
|
||||
label="Sync\nReplay",
|
||||
text_scale=tscl,
|
||||
color=b_color,
|
||||
textcolor=b_textcolor,
|
||||
on_activate_call=CommonUtils.sync_confirmation)
|
||||
|
||||
btnv -= b_height + b_space_extra
|
||||
def call_delete(): return CommonUtils.delete_replay(self.selected_replay, self.tab_id, self)
|
||||
delete_replay_button = ba.buttonwidget(
|
||||
parent=self.root,
|
||||
size=(b_width, b_height),
|
||||
position=(btnh, btnv),
|
||||
button_type="square",
|
||||
label=ba.Lstr(resource=self._r + '.deleteReplayButtonText'),
|
||||
text_scale=tscl,
|
||||
color=b_color,
|
||||
textcolor=b_textcolor,
|
||||
on_activate_call=call_delete)
|
||||
|
||||
self.on_tab_select(MyTabId.INTERNAL)
|
||||
|
||||
def close(self):
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
|
|
@ -232,33 +334,73 @@ class SettingWindow():
|
|||
|
||||
#ba.widget(edit=self.enable_button, up_widget=decrease_button, down_widget=self.lower_text,left_widget=save_button, right_widget=save_button)
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
# ----------------------------------------------------------------------------------------------------
|
||||
|
||||
ww.__old_init__ = ww.__init__
|
||||
class ShareTab(WatchWindow):
|
||||
|
||||
@override(WatchWindow)
|
||||
def __init__(self,
|
||||
transition: str | None = 'in_right',
|
||||
origin_widget: ba.Widget | None = None,
|
||||
oldmethod=None):
|
||||
self.my_tab_container = None
|
||||
self._old___init__(transition, origin_widget)
|
||||
|
||||
def new_init(self, transition="in_right", origin_widget=None):
|
||||
self.__old_init__(transition, origin_widget)
|
||||
self._share_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width*0.70, self._height*0.80),
|
||||
size=(220, 60),
|
||||
scale=1.0,
|
||||
color=green,
|
||||
icon=ba.gettexture('usersButton'),
|
||||
iconscale=1.5,
|
||||
label=title,
|
||||
on_activate_call=SettingWindow)
|
||||
self._tab_row.tabs[self.TabID.MY_REPLAYS].button.delete() # deleting old tab button
|
||||
|
||||
tabdefs = [(self.TabID.MY_REPLAYS,
|
||||
ba.Lstr(resource=self._r + '.myReplaysText'),),
|
||||
(MyTabId.SHARE_REPLAYS, "Share Replays"),]
|
||||
|
||||
uiscale = ba.app.ui.uiscale
|
||||
x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
|
||||
tab_buffer_h = 750 + 2 * x_inset
|
||||
self._tab_row = TabRow(
|
||||
self._root_widget,
|
||||
tabdefs,
|
||||
pos=((tab_buffer_h / 1.5) * 0.5, self._height - 130),
|
||||
size=((self._width - tab_buffer_h)*2, 50),
|
||||
on_select_call=self._set_tab)
|
||||
|
||||
self._tab_row.update_appearance(self.TabID.MY_REPLAYS)
|
||||
|
||||
@override(WatchWindow)
|
||||
def _set_tab(self, tab_id, oldfunc=None):
|
||||
self._old__set_tab(tab_id)
|
||||
if self.my_tab_container:
|
||||
self.my_tab_container.delete()
|
||||
if tab_id == MyTabId.SHARE_REPLAYS:
|
||||
|
||||
scroll_left = (self._width - self._scroll_width) * 0.5
|
||||
scroll_bottom = self._height - self._scroll_height - 79 - 48
|
||||
|
||||
c_width = self._scroll_width
|
||||
c_height = self._scroll_height - 20
|
||||
sub_scroll_height = c_height - 63
|
||||
self._my_replays_scroll_width = sub_scroll_width = (
|
||||
680 if uiscale is ba.UIScale.SMALL else 640
|
||||
)
|
||||
|
||||
self.my_tab_container = ba.containerwidget(
|
||||
parent=self._root_widget,
|
||||
position=(scroll_left,
|
||||
scroll_bottom + (self._scroll_height - c_height) * 0.5,),
|
||||
size=(c_width, c_height),
|
||||
background=False,
|
||||
selection_loops_to_parent=True,
|
||||
)
|
||||
|
||||
ShareTabUi(self.my_tab_container)
|
||||
|
||||
|
||||
# ba_meta export plugin
|
||||
|
||||
class Loup(ba.Plugin):
|
||||
def on_app_running(self):
|
||||
ww.__init__ = new_init
|
||||
WatchWindow.__init__ = ShareTab.__init__
|
||||
|
||||
def has_settings_ui(self):
|
||||
return True
|
||||
|
||||
def show_settings_ui(self, button):
|
||||
SettingWindow()
|
||||
Print("Open share replay tab in replay window to share your replays", color=blue)
|
||||
|
|
|
|||
215
plugins/utilities/tnt_respawn_text.py
Normal file
215
plugins/utilities/tnt_respawn_text.py
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# ba_meta require api 7
|
||||
|
||||
"""
|
||||
TNT Respawn Text by TheMikirog
|
||||
Version 1
|
||||
|
||||
Shows when a TNT box is about to respawn with non-intrusive text.
|
||||
|
||||
Heavily commented for easy modding learning!
|
||||
|
||||
No Rights Reserved
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# Let's import everything we need and nothing more.
|
||||
import ba
|
||||
import bastd
|
||||
import math
|
||||
import random
|
||||
from bastd.actor.bomb import Bomb
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
"""
|
||||
Turns out TNT respawning got changed during the 1.5 update.
|
||||
At first I planned to make an accurate timer text that just counts down seconds to respawn.
|
||||
However, to prevent timer stacking, Eric decided to update the timer every 1.1s seconds instead of the standard 1.0s.
|
||||
This makes it a pain to mod, so instead I had to make some compromises and go for a percentage charge instead.
|
||||
|
||||
The goal here is to make it easier to intuit when the box respawns, so you can still play around it.
|
||||
Percentage until respawn is still more helpful than absolutely nothing.
|
||||
I wanted to keep the original TNT box's respawn design here, so I didn't touch the timer.
|
||||
I prefer adding onto existing behavior than editing existing code.
|
||||
This mod is supposed to be a quality of life thing after all.
|
||||
"""
|
||||
|
||||
|
||||
# ba_meta export plugin
|
||||
class TNTRespawnText(ba.Plugin):
|
||||
|
||||
# This clamping function will make sure a certain value can't go above or below a certain threshold.
|
||||
# We're gonna need this functionality in just a bit.
|
||||
def clamp(num, min_value, max_value):
|
||||
num = max(min(num, max_value), min_value)
|
||||
return num
|
||||
|
||||
# This function gets called every time the TNT dies. Doesn't matter how.
|
||||
# Explosions, getting thrown out of bounds, stuff.
|
||||
# I want the text appearing animation to start as soon as the TNT box blows up.
|
||||
def on_tnt_exploded(self):
|
||||
self.tnt_has_callback = False
|
||||
self._respawn_text.color = (1.0, 1.0, 1.0)
|
||||
ba.animate(
|
||||
self._respawn_text,
|
||||
'opacity',
|
||||
{
|
||||
0: 0.0,
|
||||
self._respawn_time * 0.5: 0.175,
|
||||
self._respawn_time: 0.4
|
||||
},
|
||||
)
|
||||
|
||||
# We're gonna use the magic of decorators to expand the original code with new stuff.
|
||||
# This even works with other mods too! Don't replace functions, use decorators!
|
||||
# Anyway we're gonna access the TNTSpawner class' init function.
|
||||
def new_init(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
# The update function is not only called by a timer, but also manually
|
||||
# during the original init function's execution.
|
||||
# This means the code expects a variable that doesn't exist.
|
||||
# Let's make it prematurely.
|
||||
# args[0] is "self" in the original game code.
|
||||
args[0]._respawn_text = None
|
||||
|
||||
# This is where the original game's code is executed.
|
||||
func(*args, **kwargs)
|
||||
|
||||
# For each TNT we make we want to add a callback.
|
||||
# It's basically a flag that tells the TNT to call a function.
|
||||
# We don't want to add several of the same flag at once.
|
||||
# We set this to True every time we add a callback.
|
||||
# We check for this variable before adding a new one.
|
||||
args[0].tnt_has_callback = True
|
||||
|
||||
# Let's make the text.
|
||||
# We tap into the spawner position in order to decide where the text should be.
|
||||
respawn_text_position = (args[0]._position[0],
|
||||
args[0]._position[1] - 0.4,
|
||||
args[0]._position[2])
|
||||
args[0]._respawn_text = ba.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'text': "", # we'll set the text later
|
||||
'in_world': True,
|
||||
'position': respawn_text_position,
|
||||
'shadow': 1.0,
|
||||
'flatness': 1.0,
|
||||
'color': (1.0, 1.0, 1.0),
|
||||
'opacity': 0.0,
|
||||
'scale': 0.0225,
|
||||
'h_align': 'center',
|
||||
'v_align': 'center',
|
||||
},
|
||||
)
|
||||
# Here we add our callback.
|
||||
# Timers don't like calling functions that are outside of the game's "universe".
|
||||
# If we call the function directly, we get a PyCallable error.
|
||||
# We make a dummy function to avoid this.
|
||||
|
||||
def tnt_callback():
|
||||
TNTRespawnText.on_tnt_exploded(args[0])
|
||||
|
||||
# One disadvantage of the documentation is that it doesn't tell you all functions related to the node system.
|
||||
# To learn about all possible atttributes and functions you just gotta explore the code and experiment.
|
||||
# This add_death_action function of the node system is used in the original game
|
||||
# to let the player know if the node got removed.
|
||||
# For bombs that would be explosions or when they go out of bounds.
|
||||
# This is used to increase the owner's bomb count by one.
|
||||
# Here however we'll use this function to manipulate our text logic.
|
||||
# We want to animate our text the moment the TNT box dies.
|
||||
args[0]._tnt.node.add_death_action(tnt_callback)
|
||||
return wrapper
|
||||
# Let's replace the original init function with our modified version.
|
||||
bastd.actor.bomb.TNTSpawner.__init__ = new_init(bastd.actor.bomb.TNTSpawner.__init__)
|
||||
|
||||
# Our modified update function.
|
||||
# This gets called every 1.1s. Check the TNTSpawner class in the game's code for details.
|
||||
def new_update(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
# Check if our TNT box is still kickin'.
|
||||
tnt_alive = args[0]._tnt is not None and args[0]._tnt.node
|
||||
|
||||
func(*args, **kwargs) # original code
|
||||
|
||||
# The first time this code executes, nothing happens.
|
||||
# However once our text node is created properly, let's do some work.
|
||||
if args[0]._respawn_text:
|
||||
|
||||
# Let's make a value that will represent percentage.
|
||||
# 0 means timer started and 100 means ready.
|
||||
value = args[0]._wait_time / args[0]._respawn_time
|
||||
|
||||
# It's annoying when the number jumps from 99% to 100% and it's delayed.
|
||||
# Let's make sure this happens less often.
|
||||
# I turned a linear curve into an exponential one.
|
||||
value = math.pow(value - 0.001, 2)
|
||||
|
||||
# Let's turn the value into a percentage.
|
||||
value = math.floor(value * 100)
|
||||
|
||||
# Let's make sure it's actually between 0 and 100.
|
||||
value = TNTRespawnText.clamp(value, 0, 100)
|
||||
|
||||
# Let's finish it off with a percentage symbol and preso!
|
||||
args[0]._respawn_text.text = str(value)+"%"
|
||||
|
||||
# When the timer ticks, we do different things depending on the time and the state of our TNT box.
|
||||
if not tnt_alive:
|
||||
# Code goes here if we don't have a TNT box and we reached 100%.
|
||||
if args[0]._tnt is None or args[0]._wait_time >= args[0]._respawn_time and args[0]._respawn_text:
|
||||
# Animate the text "bounce" to draw attention
|
||||
ba.animate(
|
||||
args[0]._respawn_text,
|
||||
'scale',
|
||||
{
|
||||
0: args[0]._respawn_text.scale * 1.2,
|
||||
0.3: args[0]._respawn_text.scale * 1.05,
|
||||
0.6: args[0]._respawn_text.scale * 1.025,
|
||||
1.1: args[0]._respawn_text.scale
|
||||
},
|
||||
)
|
||||
# Fade the text away
|
||||
ba.animate(
|
||||
args[0]._respawn_text,
|
||||
'opacity',
|
||||
{
|
||||
0: args[0]._respawn_text.opacity,
|
||||
1.1: 0.0
|
||||
},
|
||||
)
|
||||
# Make sure it says 100%, because our value we calculated earlier might not be accurate at that point.
|
||||
args[0]._respawn_text.text = "100%"
|
||||
|
||||
# Make our text orange.
|
||||
args[0]._respawn_text.color = (1.0, 0.75, 0.5)
|
||||
|
||||
# Make some sparks to draw the eye.
|
||||
ba.emitfx(
|
||||
position=args[0]._position,
|
||||
count=int(5.0 + random.random() * 10),
|
||||
scale=0.8,
|
||||
spread=1.25,
|
||||
chunk_type='spark',
|
||||
)
|
||||
# What if we still have our TNT box?
|
||||
else:
|
||||
# If the TNT box is fresly spawned spawned earlier in the function, chances are it doesn't have a callback.
|
||||
# If it has, ignore. Otherwise let's add it.
|
||||
# Cloning code that already exists in init is not very clean, but that'll do.
|
||||
if args[0].tnt_has_callback:
|
||||
return
|
||||
|
||||
def tnt_callback():
|
||||
TNTRespawnText.on_tnt_exploded(args[0])
|
||||
args[0]._tnt.node.add_death_action(tnt_callback)
|
||||
return wrapper
|
||||
|
||||
# Let's replace the original update function with our modified version.
|
||||
bastd.actor.bomb.TNTSpawner._update = new_update(bastd.actor.bomb.TNTSpawner._update)
|
||||
Loading…
Add table
Add a link
Reference in a new issue