mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-10-20 00:00:39 +00:00
1.7.5 master sync
This commit is contained in:
parent
421c488c6a
commit
62103ea678
61 changed files with 2001 additions and 1364 deletions
30
dist/ba_data/python/ba/_app.py
vendored
30
dist/ba_data/python/ba/_app.py
vendored
|
|
@ -29,6 +29,7 @@ if TYPE_CHECKING:
|
|||
from ba._cloud import CloudSubsystem
|
||||
from bastd.actor import spazappearance
|
||||
from ba._accountv2 import AccountV2Subsystem
|
||||
from ba._level import Level
|
||||
|
||||
|
||||
class App:
|
||||
|
|
@ -274,6 +275,7 @@ class App:
|
|||
|
||||
# Co-op Campaigns.
|
||||
self.campaigns: dict[str, ba.Campaign] = {}
|
||||
self.custom_coop_practice_games: list[str] = []
|
||||
|
||||
# Server Mode.
|
||||
self.server: ba.ServerController | None = None
|
||||
|
|
@ -342,6 +344,7 @@ class App:
|
|||
from bastd.actor import spazappearance
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
|
||||
self._aioloop = _asyncio.setup_asyncio()
|
||||
|
||||
cfg = self.config
|
||||
|
|
@ -432,8 +435,6 @@ class App:
|
|||
|
||||
# from ba._dependency import test_depset
|
||||
# test_depset()
|
||||
if bool(False):
|
||||
self._test_https()
|
||||
|
||||
def _update_state(self) -> None:
|
||||
if self._app_paused:
|
||||
|
|
@ -527,6 +528,15 @@ class App:
|
|||
# FIXME: This should not be an actor attr.
|
||||
activity.paused_text = None
|
||||
|
||||
def add_coop_practice_level(self, level: Level) -> None:
|
||||
"""Adds an individual level to the 'practice' section in Co-op."""
|
||||
|
||||
# Assign this level to our catch-all campaign.
|
||||
self.campaigns['Challenges'].addlevel(level)
|
||||
|
||||
# Make note to add it to our challenges UI.
|
||||
self.custom_coop_practice_games.append(f'Challenges:{level.name}')
|
||||
|
||||
def return_to_main_menu_session_gracefully(self,
|
||||
reset_ui: bool = True) -> None:
|
||||
"""Attempt to cleanly get back to the main menu."""
|
||||
|
|
@ -643,19 +653,3 @@ class App:
|
|||
"""
|
||||
self._initial_login_completed = True
|
||||
self._update_state()
|
||||
|
||||
def _test_https(self) -> None:
|
||||
"""Testing https support.
|
||||
|
||||
(would be nice to get this working on our custom Python builds; need
|
||||
to wrangle certificates somehow).
|
||||
"""
|
||||
import urllib.request
|
||||
try:
|
||||
with urllib.request.urlopen('https://example.com') as url:
|
||||
val = url.read()
|
||||
_ba.screenmessage('HTTPS SUCCESS!')
|
||||
print('HTTPS TEST SUCCESS', len(val))
|
||||
except Exception as exc:
|
||||
_ba.screenmessage('HTTPS FAIL.')
|
||||
print('HTTPS TEST FAIL:', exc)
|
||||
|
|
|
|||
6
dist/ba_data/python/ba/_assetmanager.py
vendored
6
dist/ba_data/python/ba/_assetmanager.py
vendored
|
|
@ -17,6 +17,7 @@ import sys
|
|||
|
||||
from efro.dataclassio import (ioprepped, IOAttrs, dataclass_from_json,
|
||||
dataclass_to_json)
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bacommon.assets import AssetPackageFlavor
|
||||
|
|
@ -165,7 +166,6 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
|
|||
|
||||
"""
|
||||
# pylint: disable=consider-using-with
|
||||
|
||||
import socket
|
||||
|
||||
# We don't want to keep the provided AssetGather alive, but we want
|
||||
|
|
@ -175,7 +175,9 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
|
|||
|
||||
# Pass a very short timeout to urllib so we have opportunities
|
||||
# to cancel even with network blockage.
|
||||
req = urllib.request.urlopen(url, timeout=1)
|
||||
req = urllib.request.urlopen(url,
|
||||
context=_ba.app.net.sslcontext,
|
||||
timeout=1)
|
||||
file_size = int(req.headers['Content-Length'])
|
||||
print(f'\nDownloading: {filename} Bytes: {file_size:,}')
|
||||
|
||||
|
|
|
|||
16
dist/ba_data/python/ba/_asyncio.py
vendored
16
dist/ba_data/python/ba/_asyncio.py
vendored
|
|
@ -11,6 +11,9 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
|
|
@ -19,6 +22,8 @@ if TYPE_CHECKING:
|
|||
_asyncio_timer: ba.Timer | None = None
|
||||
_asyncio_event_loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
DEBUG_TIMING = os.environ.get('BA_DEBUG_TIMING') == '1'
|
||||
|
||||
|
||||
def setup_asyncio() -> asyncio.AbstractEventLoop:
|
||||
"""Setup asyncio functionality for the logic thread."""
|
||||
|
|
@ -53,7 +58,18 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
|
|||
def run_cycle() -> None:
|
||||
assert _asyncio_event_loop is not None
|
||||
_asyncio_event_loop.call_soon(_asyncio_event_loop.stop)
|
||||
starttime = time.monotonic() if DEBUG_TIMING else 0
|
||||
_asyncio_event_loop.run_forever()
|
||||
endtime = time.monotonic() if DEBUG_TIMING else 0
|
||||
|
||||
# Let's aim to have nothing take longer than 1/120 of a second.
|
||||
if DEBUG_TIMING:
|
||||
warn_time = 1.0 / 120
|
||||
duration = endtime - starttime
|
||||
if duration > warn_time:
|
||||
logging.warning(
|
||||
'Asyncio loop step took %.4fs; ideal max is %.4f',
|
||||
duration, warn_time)
|
||||
|
||||
global _asyncio_timer # pylint: disable=invalid-name
|
||||
_asyncio_timer = _ba.Timer(1.0 / 30.0,
|
||||
|
|
|
|||
468
dist/ba_data/python/ba/_campaign.py
vendored
468
dist/ba_data/python/ba/_campaign.py
vendored
|
|
@ -28,10 +28,16 @@ class Campaign:
|
|||
Category: **App Classes**
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, sequential: bool = True):
|
||||
def __init__(self,
|
||||
name: str,
|
||||
sequential: bool = True,
|
||||
levels: list[ba.Level] | None = None):
|
||||
self._name = name
|
||||
self._levels: list[ba.Level] = []
|
||||
self._sequential = sequential
|
||||
self._levels: list[ba.Level] = []
|
||||
if levels is not None:
|
||||
for level in levels:
|
||||
self.addlevel(level)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
|
@ -43,12 +49,15 @@ class Campaign:
|
|||
"""Whether this Campaign's levels must be played in sequence."""
|
||||
return self._sequential
|
||||
|
||||
def addlevel(self, level: ba.Level) -> None:
|
||||
def addlevel(self, level: ba.Level, index: int | None = None) -> None:
|
||||
"""Adds a ba.Level to the Campaign."""
|
||||
if level.campaign is not None:
|
||||
raise RuntimeError('Level already belongs to a campaign.')
|
||||
level.set_campaign(self, len(self._levels))
|
||||
self._levels.append(level)
|
||||
if index is None:
|
||||
self._levels.append(level)
|
||||
else:
|
||||
self._levels.insert(index, level)
|
||||
|
||||
@property
|
||||
def levels(self) -> list[ba.Level]:
|
||||
|
|
@ -91,9 +100,8 @@ class Campaign:
|
|||
|
||||
def init_campaigns() -> None:
|
||||
"""Fill out initial default Campaigns."""
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=cyclic-import
|
||||
from ba import _level
|
||||
from ba._level import Level
|
||||
from bastd.game.onslaught import OnslaughtGame
|
||||
from bastd.game.football import FootballCoopGame
|
||||
from bastd.game.runaround import RunaroundGame
|
||||
|
|
@ -109,244 +117,218 @@ def init_campaigns() -> None:
|
|||
|
||||
# FIXME: Once translations catch up, we can convert these to use the
|
||||
# generic display-name '${GAME} Training' type stuff.
|
||||
campaign = Campaign('Easy')
|
||||
campaign.addlevel(
|
||||
_level.Level('Onslaught Training',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'training_easy'},
|
||||
preview_texture_name='doomShroomPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Rookie Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'rookie_easy'},
|
||||
preview_texture_name='courtyardPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Rookie Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'rookie_easy'},
|
||||
preview_texture_name='footballStadiumPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Pro Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='doomShroomPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Pro Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='footballStadiumPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Pro Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='towerDPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Uber Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='courtyardPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Uber Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='footballStadiumPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Uber Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='towerDPreview'))
|
||||
register_campaign(campaign)
|
||||
register_campaign(
|
||||
Campaign(
|
||||
'Easy',
|
||||
levels=[
|
||||
Level('Onslaught Training',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'training_easy'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Rookie Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'rookie_easy'},
|
||||
preview_texture_name='courtyardPreview'),
|
||||
Level('Rookie Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'rookie_easy'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Pro Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Pro Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Pro Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Uber Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='courtyardPreview'),
|
||||
Level('Uber Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Uber Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='towerDPreview')
|
||||
],
|
||||
))
|
||||
|
||||
# "hard" mode
|
||||
campaign = Campaign('Default')
|
||||
campaign.addlevel(
|
||||
_level.Level('Onslaught Training',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'training'},
|
||||
preview_texture_name='doomShroomPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Rookie Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'rookie'},
|
||||
preview_texture_name='courtyardPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Rookie Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'rookie'},
|
||||
preview_texture_name='footballStadiumPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Pro Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='doomShroomPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Pro Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='footballStadiumPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Pro Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='towerDPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Uber Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='courtyardPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Uber Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='footballStadiumPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Uber Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='towerDPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('The Last Stand',
|
||||
gametype=TheLastStandGame,
|
||||
settings={},
|
||||
preview_texture_name='rampagePreview'))
|
||||
register_campaign(campaign)
|
||||
register_campaign(
|
||||
Campaign(
|
||||
'Default',
|
||||
levels=[
|
||||
Level('Onslaught Training',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'training'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Rookie Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'rookie'},
|
||||
preview_texture_name='courtyardPreview'),
|
||||
Level('Rookie Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'rookie'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Pro Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Pro Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Pro Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Uber Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='courtyardPreview'),
|
||||
Level('Uber Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Uber Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('The Last Stand',
|
||||
gametype=TheLastStandGame,
|
||||
settings={},
|
||||
preview_texture_name='rampagePreview')
|
||||
],
|
||||
))
|
||||
|
||||
# challenges: our 'official' random extra co-op levels
|
||||
campaign = Campaign('Challenges', sequential=False)
|
||||
campaign.addlevel(
|
||||
_level.Level('Infinite Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'endless'},
|
||||
preview_texture_name='doomShroomPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Infinite Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'endless'},
|
||||
preview_texture_name='towerDPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Race',
|
||||
displayname='${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={
|
||||
'map': 'Big G',
|
||||
'Laps': 3,
|
||||
'Bomb Spawning': 0
|
||||
},
|
||||
preview_texture_name='bigGPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Pro Race',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={
|
||||
'map': 'Big G',
|
||||
'Laps': 3,
|
||||
'Bomb Spawning': 1000
|
||||
},
|
||||
preview_texture_name='bigGPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Lake Frigid Race',
|
||||
displayname='${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={
|
||||
'map': 'Lake Frigid',
|
||||
'Laps': 6,
|
||||
'Mine Spawning': 2000,
|
||||
'Bomb Spawning': 0
|
||||
},
|
||||
preview_texture_name='lakeFrigidPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Football',
|
||||
displayname='${GAME}',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='footballStadiumPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Pro Football',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'tournament_pro'},
|
||||
preview_texture_name='footballStadiumPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Runaround',
|
||||
displayname='${GAME}',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='towerDPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Uber Runaround',
|
||||
displayname='Uber ${GAME}',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'tournament_uber'},
|
||||
preview_texture_name='towerDPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('The Last Stand',
|
||||
displayname='${GAME}',
|
||||
gametype=TheLastStandGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='rampagePreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Tournament Infinite Onslaught',
|
||||
displayname='Infinite Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'endless_tournament'},
|
||||
preview_texture_name='doomShroomPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Tournament Infinite Runaround',
|
||||
displayname='Infinite Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'endless_tournament'},
|
||||
preview_texture_name='towerDPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Target Practice',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=TargetPracticeGame,
|
||||
settings={},
|
||||
preview_texture_name='doomShroomPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Target Practice B',
|
||||
displayname='${GAME}',
|
||||
gametype=TargetPracticeGame,
|
||||
settings={
|
||||
'Target Count': 2,
|
||||
'Enable Impact Bombs': False,
|
||||
'Enable Triple Bombs': False
|
||||
},
|
||||
preview_texture_name='doomShroomPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Meteor Shower',
|
||||
displayname='${GAME}',
|
||||
gametype=MeteorShowerGame,
|
||||
settings={},
|
||||
preview_texture_name='rampagePreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Epic Meteor Shower',
|
||||
displayname='${GAME}',
|
||||
gametype=MeteorShowerGame,
|
||||
settings={'Epic Mode': True},
|
||||
preview_texture_name='rampagePreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Easter Egg Hunt',
|
||||
displayname='${GAME}',
|
||||
gametype=EasterEggHuntGame,
|
||||
settings={},
|
||||
preview_texture_name='towerDPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level('Pro Easter Egg Hunt',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=EasterEggHuntGame,
|
||||
settings={'Pro Mode': True},
|
||||
preview_texture_name='towerDPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level(
|
||||
name='Ninja Fight', # (unique id not seen by player)
|
||||
displayname='${GAME}', # (readable name seen by player)
|
||||
gametype=NinjaFightGame,
|
||||
settings={'preset': 'regular'},
|
||||
preview_texture_name='courtyardPreview'))
|
||||
campaign.addlevel(
|
||||
_level.Level(name='Pro Ninja Fight',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=NinjaFightGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='courtyardPreview'))
|
||||
register_campaign(campaign)
|
||||
register_campaign(
|
||||
Campaign(
|
||||
'Challenges',
|
||||
sequential=False,
|
||||
levels=[
|
||||
Level('Infinite Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'endless'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Infinite Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'endless'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Race',
|
||||
displayname='${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={
|
||||
'map': 'Big G',
|
||||
'Laps': 3,
|
||||
'Bomb Spawning': 0
|
||||
},
|
||||
preview_texture_name='bigGPreview'),
|
||||
Level('Pro Race',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={
|
||||
'map': 'Big G',
|
||||
'Laps': 3,
|
||||
'Bomb Spawning': 1000
|
||||
},
|
||||
preview_texture_name='bigGPreview'),
|
||||
Level('Lake Frigid Race',
|
||||
displayname='${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={
|
||||
'map': 'Lake Frigid',
|
||||
'Laps': 6,
|
||||
'Mine Spawning': 2000,
|
||||
'Bomb Spawning': 0
|
||||
},
|
||||
preview_texture_name='lakeFrigidPreview'),
|
||||
Level('Football',
|
||||
displayname='${GAME}',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Pro Football',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'tournament_pro'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Runaround',
|
||||
displayname='${GAME}',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Uber Runaround',
|
||||
displayname='Uber ${GAME}',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'tournament_uber'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('The Last Stand',
|
||||
displayname='${GAME}',
|
||||
gametype=TheLastStandGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='rampagePreview'),
|
||||
Level('Tournament Infinite Onslaught',
|
||||
displayname='Infinite Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'endless_tournament'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Tournament Infinite Runaround',
|
||||
displayname='Infinite Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'endless_tournament'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Target Practice',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=TargetPracticeGame,
|
||||
settings={},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Target Practice B',
|
||||
displayname='${GAME}',
|
||||
gametype=TargetPracticeGame,
|
||||
settings={
|
||||
'Target Count': 2,
|
||||
'Enable Impact Bombs': False,
|
||||
'Enable Triple Bombs': False
|
||||
},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Meteor Shower',
|
||||
displayname='${GAME}',
|
||||
gametype=MeteorShowerGame,
|
||||
settings={},
|
||||
preview_texture_name='rampagePreview'),
|
||||
Level('Epic Meteor Shower',
|
||||
displayname='${GAME}',
|
||||
gametype=MeteorShowerGame,
|
||||
settings={'Epic Mode': True},
|
||||
preview_texture_name='rampagePreview'),
|
||||
Level('Easter Egg Hunt',
|
||||
displayname='${GAME}',
|
||||
gametype=EasterEggHuntGame,
|
||||
settings={},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Pro Easter Egg Hunt',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=EasterEggHuntGame,
|
||||
settings={'Pro Mode': True},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level(
|
||||
name='Ninja Fight', # (unique id not seen by player)
|
||||
displayname='${GAME}', # (readable name seen by player)
|
||||
gametype=NinjaFightGame,
|
||||
settings={'preset': 'regular'},
|
||||
preview_texture_name='courtyardPreview'),
|
||||
Level(name='Pro Ninja Fight',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=NinjaFightGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='courtyardPreview')
|
||||
],
|
||||
))
|
||||
|
|
|
|||
8
dist/ba_data/python/ba/_cloud.py
vendored
8
dist/ba_data/python/ba/_cloud.py
vendored
|
|
@ -56,6 +56,14 @@ class CloudSubsystem:
|
|||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.PingMessage,
|
||||
on_response: Callable[[bacommon.cloud.PingResponse | Exception], None],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: Message,
|
||||
|
|
|
|||
6
dist/ba_data/python/ba/_hooks.py
vendored
6
dist/ba_data/python/ba/_hooks.py
vendored
|
|
@ -204,6 +204,12 @@ def no_game_circle_message() -> None:
|
|||
_ba.screenmessage(Lstr(resource='noGameCircleText'), color=(1, 0, 0))
|
||||
|
||||
|
||||
def google_play_purchases_not_available_message() -> None:
|
||||
from ba._language import Lstr
|
||||
_ba.screenmessage(Lstr(resource='googlePlayPurchasesNotAvailableText'),
|
||||
color=(1, 0, 0))
|
||||
|
||||
|
||||
def empty_call() -> None:
|
||||
pass
|
||||
|
||||
|
|
|
|||
20
dist/ba_data/python/ba/_meta.py
vendored
20
dist/ba_data/python/ba/_meta.py
vendored
|
|
@ -44,7 +44,7 @@ class MetadataSubsystem:
|
|||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.metascan: ScanResults | None = None
|
||||
self.scanresults: ScanResults | None = None
|
||||
self.extra_scan_dirs: list[str] = []
|
||||
|
||||
def on_app_running(self) -> None:
|
||||
|
|
@ -58,7 +58,7 @@ class MetadataSubsystem:
|
|||
|
||||
Should be called only once at launch."""
|
||||
app = _ba.app
|
||||
if self.metascan is not None:
|
||||
if self.scanresults is not None:
|
||||
print('WARNING: meta scan run more than once.')
|
||||
pythondirs = ([app.python_directory_app, app.python_directory_user] +
|
||||
self.extra_scan_dirs)
|
||||
|
|
@ -133,7 +133,7 @@ class MetadataSubsystem:
|
|||
|
||||
def get_scan_results(self) -> ScanResults:
|
||||
"""Return meta scan results; block if the scan is not yet complete."""
|
||||
if self.metascan is None:
|
||||
if self.scanresults is None:
|
||||
print('WARNING: ba.meta.get_scan_results()'
|
||||
' called before scan completed.'
|
||||
' This can cause hitches.')
|
||||
|
|
@ -141,12 +141,12 @@ class MetadataSubsystem:
|
|||
# Now wait a bit for the scan to complete.
|
||||
# Eventually error though if it doesn't.
|
||||
starttime = time.time()
|
||||
while self.metascan is None:
|
||||
while self.scanresults is None:
|
||||
time.sleep(0.05)
|
||||
if time.time() - starttime > 10.0:
|
||||
raise TimeoutError(
|
||||
'timeout waiting for meta scan to complete.')
|
||||
return self.metascan
|
||||
return self.scanresults
|
||||
|
||||
def get_game_types(self) -> list[type[ba.GameActivity]]:
|
||||
"""Return available game types."""
|
||||
|
|
@ -206,7 +206,7 @@ class ScanThread(threading.Thread):
|
|||
# We also, however, immediately make results available.
|
||||
# This is because the game thread may be blocked waiting
|
||||
# for them so we can't push a call or we'd get deadlock.
|
||||
_ba.app.meta.metascan = results
|
||||
_ba.app.meta.scanresults = results
|
||||
|
||||
|
||||
class DirectoryScan:
|
||||
|
|
@ -288,7 +288,7 @@ class DirectoryScan:
|
|||
|
||||
# If we find a module requiring a different api version, warn
|
||||
# and ignore.
|
||||
if required_api is not None and required_api < CURRENT_API_VERSION:
|
||||
if required_api is not None and required_api <= CURRENT_API_VERSION:
|
||||
self.results.warnings += (
|
||||
f'Warning: {subpath} requires api {required_api} but'
|
||||
f' we are running {CURRENT_API_VERSION}; ignoring module.\n')
|
||||
|
|
@ -403,13 +403,13 @@ class DirectoryScan:
|
|||
if len(lines) > 1:
|
||||
self.results.warnings += (
|
||||
'Warning: ' + str(subpath) +
|
||||
': multiple "# ba_meta api require <NUM>" lines found;'
|
||||
': multiple "# ba_meta require api <NUM>" lines found;'
|
||||
' ignoring module.\n')
|
||||
elif not lines and toplevel and meta_lines:
|
||||
# If we're a top-level module containing meta lines but
|
||||
# no valid api require, complain.
|
||||
# no valid "require api" line found, complain.
|
||||
self.results.warnings += (
|
||||
'Warning: ' + str(subpath) +
|
||||
': no valid "# ba_meta api require <NUM>" line found;'
|
||||
': no valid "# ba_meta require api <NUM>" line found;'
|
||||
' ignoring module.\n')
|
||||
return None
|
||||
|
|
|
|||
2
dist/ba_data/python/ba/_music.py
vendored
2
dist/ba_data/python/ba/_music.py
vendored
|
|
@ -211,7 +211,7 @@ class MusicSubsystem:
|
|||
return 'Mac' in uas
|
||||
if entry_type in ('musicFile', 'musicFolder'):
|
||||
return ('android' in uas
|
||||
and _ba.android_get_external_storage_path() is not None)
|
||||
and _ba.android_get_external_files_dir() is not None)
|
||||
if entry_type == 'default':
|
||||
return True
|
||||
return False
|
||||
|
|
|
|||
62
dist/ba_data/python/ba/_net.py
vendored
62
dist/ba_data/python/ba/_net.py
vendored
|
|
@ -3,6 +3,7 @@
|
|||
"""Networking related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ssl
|
||||
import copy
|
||||
import threading
|
||||
import weakref
|
||||
|
|
@ -29,14 +30,32 @@ class NetworkSubsystem:
|
|||
# as it is updated by a background thread.
|
||||
self.zone_pings_lock = threading.Lock()
|
||||
|
||||
# Region IDs mapped to average pings. This will remain empty
|
||||
# Zone IDs mapped to average pings. This will remain empty
|
||||
# until enough pings have been run to be reasonably certain
|
||||
# that a nearby server has been pinged.
|
||||
self.zone_pings: dict[str, float] = {}
|
||||
|
||||
self._sslcontext: ssl.SSLContext | None = None
|
||||
|
||||
# For debugging.
|
||||
self.v1_test_log: str = ''
|
||||
self.v1_ctest_results: dict[int, str] = {}
|
||||
self.server_time_offset_hours: float | None = None
|
||||
|
||||
@property
|
||||
def sslcontext(self) -> ssl.SSLContext:
|
||||
"""Create/return our shared SSLContext.
|
||||
|
||||
This can be reused for all standard urllib requests/etc.
|
||||
"""
|
||||
# Note: I've run into older Android devices taking upwards of 1 second
|
||||
# to put together a default SSLContext, so recycling one can definitely
|
||||
# be a worthwhile optimization. This was suggested to me in this
|
||||
# thread by one of Python's SSL maintainers:
|
||||
# https://github.com/python/cpython/issues/94637
|
||||
if self._sslcontext is None:
|
||||
self._sslcontext = ssl.create_default_context()
|
||||
return self._sslcontext
|
||||
|
||||
|
||||
def get_ip_address_type(addr: str) -> socket.AddressFamily:
|
||||
|
|
@ -108,30 +127,36 @@ class MasterServerCallThread(threading.Thread):
|
|||
self._callback(arg)
|
||||
|
||||
def run(self) -> None:
|
||||
# pylint: disable=too-many-branches, consider-using-with
|
||||
# pylint: disable=consider-using-with
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
import json
|
||||
|
||||
from efro.error import is_urllib_network_error
|
||||
from efro.error import is_urllib_communication_error
|
||||
from ba import _general
|
||||
|
||||
response_data: Any = None
|
||||
url: str | None = None
|
||||
try:
|
||||
self._data = _general.utf8_all(self._data)
|
||||
_ba.set_thread_name('BA_ServerCallThread')
|
||||
if self._request_type == 'get':
|
||||
url = (_ba.get_master_server_address() + '/' + self._request +
|
||||
'?' + urllib.parse.urlencode(self._data))
|
||||
response = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
(_ba.get_master_server_address() + '/' +
|
||||
self._request + '?' +
|
||||
urllib.parse.urlencode(self._data)), None,
|
||||
{'User-Agent': _ba.app.user_agent_string}),
|
||||
url, None, {'User-Agent': _ba.app.user_agent_string}),
|
||||
context=_ba.app.net.sslcontext,
|
||||
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
|
||||
elif self._request_type == 'post':
|
||||
url = _ba.get_master_server_address() + '/' + self._request
|
||||
response = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
_ba.get_master_server_address() + '/' + self._request,
|
||||
url,
|
||||
urllib.parse.urlencode(self._data).encode(),
|
||||
{'User-Agent': _ba.app.user_agent_string}),
|
||||
context=_ba.app.net.sslcontext,
|
||||
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
|
||||
else:
|
||||
raise TypeError('Invalid request_type: ' + self._request_type)
|
||||
|
|
@ -151,29 +176,18 @@ class MasterServerCallThread(threading.Thread):
|
|||
raise TypeError(f'invalid responsetype: {self._response_type}')
|
||||
|
||||
except Exception as exc:
|
||||
do_print = False
|
||||
response_data = None
|
||||
|
||||
# Ignore common network errors; note unexpected ones.
|
||||
if is_urllib_network_error(exc):
|
||||
pass
|
||||
elif (self._response_type == MasterServerResponseType.JSON
|
||||
and isinstance(exc, json.decoder.JSONDecodeError)):
|
||||
# FIXME: should handle this better; could mean either the
|
||||
# server sent us bad data or it got corrupted along the way.
|
||||
pass
|
||||
else:
|
||||
do_print = True
|
||||
|
||||
if do_print:
|
||||
# Any other error here is unexpected,
|
||||
# so let's make a note of it,
|
||||
if not is_urllib_communication_error(exc, url=url):
|
||||
print(f'Error in MasterServerCallThread'
|
||||
f' (response-type={self._response_type},'
|
||||
f' (url={url},'
|
||||
f' response-type={self._response_type},'
|
||||
f' response-data={response_data}):')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
response_data = None
|
||||
|
||||
if self._callback is not None:
|
||||
_ba.pushcall(_general.Call(self._run_callback, response_data),
|
||||
from_other_thread=True)
|
||||
|
|
|
|||
12
dist/ba_data/python/ba/_plugin.py
vendored
12
dist/ba_data/python/ba/_plugin.py
vendored
|
|
@ -112,13 +112,15 @@ class PluginSubsystem:
|
|||
# or workspaces.
|
||||
if disappeared_plugs:
|
||||
_ba.playsound(_ba.getsound('shieldDown'))
|
||||
_ba.screenmessage(Lstr(resource='pluginsRemovedText',
|
||||
subs=[('${NUM}',
|
||||
str(len(disappeared_plugs)))]),
|
||||
color=(1, 1, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='pluginsRemovedText',
|
||||
subs=[('${NUM}', str(len(disappeared_plugs)))]),
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
plugnames = ', '.join(disappeared_plugs)
|
||||
_ba.log(
|
||||
f'{len(disappeared_plugs)} plugin(s) no longer found:'
|
||||
f' {disappeared_plugs}',
|
||||
f' {plugnames}.',
|
||||
to_server=False)
|
||||
for goneplug in disappeared_plugs:
|
||||
del _ba.app.config['Plugins'][goneplug]
|
||||
|
|
|
|||
2
dist/ba_data/python/ba/_workspace.py
vendored
2
dist/ba_data/python/ba/_workspace.py
vendored
|
|
@ -115,7 +115,7 @@ class WorkspaceSubsystem:
|
|||
tpartial(
|
||||
self._errmsg,
|
||||
Lstr(resource='workspaceSyncReuseText',
|
||||
subs=[('$WORKSPACE', workspacename)])),
|
||||
subs=[('${WORKSPACE}', workspacename)])),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
||||
|
|
|
|||
28
dist/ba_data/python/ba/modutils.py
vendored
28
dist/ba_data/python/ba/modutils.py
vendored
|
|
@ -17,23 +17,29 @@ def get_human_readable_user_scripts_path() -> str:
|
|||
|
||||
This is NOT a valid filesystem path; may be something like "(SD Card)".
|
||||
"""
|
||||
from ba import _language
|
||||
app = _ba.app
|
||||
path: str | None = app.python_directory_user
|
||||
if path is None:
|
||||
return '<Not Available>'
|
||||
|
||||
# On newer versions of android, the user's external storage dir is probably
|
||||
# only visible to the user's processes and thus not really useful printed
|
||||
# in its entirety; lets print it as <External Storage>/myfilepath.
|
||||
# These days, on Android, we use getExternalFilesDir() as the base of our
|
||||
# app's user-scripts dir, which gives us paths like:
|
||||
# /storage/emulated/0/Android/data/net.froemling.bombsquad/files
|
||||
# Userspace apps tend to show that as:
|
||||
# Android/data/net.froemling.bombsquad/files
|
||||
# We'd like to display it that way, but I'm not sure if there's a clean
|
||||
# way to get the root of the external storage area (/storage/emulated/0)
|
||||
# so that we could strip it off. There is
|
||||
# Environment.getExternalStorageDirectory() but that is deprecated.
|
||||
# So for now let's just be conservative and trim off recognized prefixes
|
||||
# and show the whole ugly path as a fallback.
|
||||
# Note that we used to use externalStorageText resource but gonna try
|
||||
# without it for now. (simply 'foo' instead of <External Storage>/foo).
|
||||
if app.platform == 'android':
|
||||
ext_storage_path: str | None = (
|
||||
_ba.android_get_external_storage_path())
|
||||
if (ext_storage_path is not None
|
||||
and app.python_directory_user.startswith(ext_storage_path)):
|
||||
path = ('<' +
|
||||
_language.Lstr(resource='externalStorageText').evaluate() +
|
||||
'>' + app.python_directory_user[len(ext_storage_path):])
|
||||
for pre in ['/storage/emulated/0/']:
|
||||
if path.startswith(pre):
|
||||
path = path.removeprefix(pre)
|
||||
break
|
||||
return path
|
||||
|
||||
|
||||
|
|
|
|||
2
dist/ba_data/python/bacommon/bacloud.py
vendored
2
dist/ba_data/python/bacommon/bacloud.py
vendored
|
|
@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
|||
|
||||
# Version is sent to the master-server with all commands. Can be incremented
|
||||
# if we need to change behavior server-side to go along with client changes.
|
||||
BACLOUD_VERSION = 6
|
||||
BACLOUD_VERSION = 7
|
||||
|
||||
|
||||
@ioprepped
|
||||
|
|
|
|||
16
dist/ba_data/python/bacommon/cloud.py
vendored
16
dist/ba_data/python/bacommon/cloud.py
vendored
|
|
@ -76,6 +76,22 @@ class LoginProxyCompleteMessage(Message):
|
|||
proxyid: Annotated[str, IOAttrs('p')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class PingMessage(Message):
|
||||
"""Standard ping."""
|
||||
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response]]:
|
||||
return [PingResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class PingResponse(Response):
|
||||
"""pong."""
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class TestMessage(Message):
|
||||
|
|
|
|||
4
dist/ba_data/python/bacommon/net.py
vendored
4
dist/ba_data/python/bacommon/net.py
vendored
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, Any, Annotated
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
|
@ -27,6 +28,9 @@ class ServerNodeEntry:
|
|||
class ServerNodeQueryResponse:
|
||||
"""A response to a query about server-nodes."""
|
||||
|
||||
# The current utc time on the master server.
|
||||
time: Annotated[datetime.datetime, IOAttrs('t')]
|
||||
|
||||
# If present, something went wrong, and this describes it.
|
||||
error: Annotated[str | None, IOAttrs('e', store_default=False)] = None
|
||||
|
||||
|
|
|
|||
23
dist/ba_data/python/bacommon/transfer.py
vendored
23
dist/ba_data/python/bacommon/transfer.py
vendored
|
|
@ -5,13 +5,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
from efro.dataclassio import ioprepped, IOAttrs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
pass
|
||||
|
||||
|
||||
@ioprepped
|
||||
|
|
@ -40,15 +41,17 @@ class DirectoryManifest:
|
|||
paths: list[str] = []
|
||||
|
||||
if path.is_dir():
|
||||
# Build the full list of package-relative paths.
|
||||
# Build the full list of relative paths.
|
||||
for basename, _dirnames, filenames in os.walk(path):
|
||||
for filename in filenames:
|
||||
fullname = os.path.join(basename, filename)
|
||||
assert fullname.startswith(pathstr)
|
||||
paths.append(fullname[len(pathstr) + 1:])
|
||||
# Make sure we end up with forward slashes no matter
|
||||
# what the os.* stuff above here was using.
|
||||
paths.append(Path(fullname[len(pathstr) + 1:]).as_posix())
|
||||
elif path.exists():
|
||||
# Just return a single file entry if path is not a dir.
|
||||
paths.append(pathstr)
|
||||
paths.append(path.as_posix())
|
||||
|
||||
def _get_file_info(filepath: str) -> tuple[str, DirectoryManifestFile]:
|
||||
sha = hashlib.sha256()
|
||||
|
|
@ -70,6 +73,18 @@ class DirectoryManifest:
|
|||
with ThreadPoolExecutor(max_workers=cpus) as executor:
|
||||
return cls(files=dict(executor.map(_get_file_info, paths)))
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Log any odd data in the manifest; for debugging."""
|
||||
import logging
|
||||
for fpath, _fentry in self.files.items():
|
||||
# We want to be dealing in only forward slashes; make sure
|
||||
# that's the case (wondering if we'll ever see backslashes
|
||||
# for escape purposes).
|
||||
if '\\' in fpath:
|
||||
logging.exception("Found unusual path in manifest: '%s'.",
|
||||
fpath)
|
||||
break # 1 error is enough for now.
|
||||
|
||||
@classmethod
|
||||
def get_empty_hash(cls) -> str:
|
||||
"""Return the hash for an empty file."""
|
||||
|
|
|
|||
2
dist/ba_data/python/bastd/actor/spaz.py
vendored
2
dist/ba_data/python/bastd/actor/spaz.py
vendored
|
|
@ -606,6 +606,7 @@ class Spaz(ba.Actor):
|
|||
"""
|
||||
assert self.node
|
||||
self.node.boxing_gloves = True
|
||||
self._has_boxing_gloves = True
|
||||
if self._demo_mode: # Preserve old behavior.
|
||||
self._punch_power_scale = 1.7
|
||||
self._punch_cooldown = 300
|
||||
|
|
@ -754,7 +755,6 @@ class Spaz(ba.Actor):
|
|||
ba.WeakCall(self._bomb_wear_off),
|
||||
timeformat=ba.TimeFormat.MILLISECONDS))
|
||||
elif msg.poweruptype == 'punch':
|
||||
self._has_boxing_gloves = True
|
||||
tex = PowerupBoxFactory.get().tex_punch
|
||||
self._flash_billboard(tex)
|
||||
self.equip_boxing_gloves()
|
||||
|
|
|
|||
3
dist/ba_data/python/bastd/game/onslaught.py
vendored
3
dist/ba_data/python/bastd/game/onslaught.py
vendored
|
|
@ -5,6 +5,9 @@
|
|||
# Yes this is a long one..
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
# ba_meta require api 7
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
|
|
|||
3
dist/ba_data/python/bastd/game/runaround.py
vendored
3
dist/ba_data/python/bastd/game/runaround.py
vendored
|
|
@ -5,6 +5,9 @@
|
|||
# We wear the cone of shame.
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
# ba_meta require api 7
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
|
|
|||
799
dist/ba_data/python/bastd/ui/coop/browser.py
vendored
799
dist/ba_data/python/bastd/ui/coop/browser.py
vendored
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
|
|
@ -18,6 +17,8 @@ from bastd.ui.store.browser import StoreBrowserWindow
|
|||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from bastd.ui.coop.tournamentbutton import TournamentButton
|
||||
|
||||
|
||||
class CoopBrowserWindow(ba.Window):
|
||||
"""Window for browsing co-op levels/games/etc."""
|
||||
|
|
@ -175,8 +176,6 @@ class CoopBrowserWindow(ba.Window):
|
|||
'Selected Coop Campaign Level', None))
|
||||
self._selected_custom_level = (cfg.get('Selected Coop Custom Level',
|
||||
None))
|
||||
self._selected_challenge_level = (cfg.get(
|
||||
'Selected Coop Challenge Level', None))
|
||||
|
||||
# Don't want initial construction affecting our last-selected.
|
||||
self._do_selection_callbacks = False
|
||||
|
|
@ -283,6 +282,7 @@ class CoopBrowserWindow(ba.Window):
|
|||
import bastd.ui.tournamentscores as _unused8
|
||||
import bastd.ui.tournamententry as _unused9
|
||||
import bastd.ui.play as _unused10
|
||||
import bastd.ui.coop.tournamentbutton as _unused11
|
||||
|
||||
def _update(self) -> None:
|
||||
# Do nothing if we've somehow outlived our actual UI.
|
||||
|
|
@ -335,21 +335,21 @@ class CoopBrowserWindow(ba.Window):
|
|||
# Decrement time on our tournament buttons.
|
||||
ads_enabled = _ba.have_incentivized_ad()
|
||||
for tbtn in self._tournament_buttons:
|
||||
tbtn['time_remaining'] = max(0, tbtn['time_remaining'] - 1)
|
||||
if tbtn['time_remaining_value_text'] is not None:
|
||||
tbtn.time_remaining = max(0, tbtn.time_remaining - 1)
|
||||
if tbtn.time_remaining_value_text is not None:
|
||||
ba.textwidget(
|
||||
edit=tbtn['time_remaining_value_text'],
|
||||
text=ba.timestring(tbtn['time_remaining'],
|
||||
edit=tbtn.time_remaining_value_text,
|
||||
text=ba.timestring(tbtn.time_remaining,
|
||||
centi=False,
|
||||
suppress_format_warning=True) if
|
||||
(tbtn['has_time_remaining']
|
||||
(tbtn.has_time_remaining
|
||||
and self._tourney_data_up_to_date) else '-')
|
||||
|
||||
# Also adjust the ad icon visibility.
|
||||
if tbtn.get('allow_ads', False) and _ba.has_video_ads():
|
||||
ba.imagewidget(edit=tbtn['entry_fee_ad_image'],
|
||||
if tbtn.allow_ads and _ba.has_video_ads():
|
||||
ba.imagewidget(edit=tbtn.entry_fee_ad_image,
|
||||
opacity=1.0 if ads_enabled else 0.25)
|
||||
ba.textwidget(edit=tbtn['entry_fee_text_remaining'],
|
||||
ba.textwidget(edit=tbtn.entry_fee_text_remaining,
|
||||
color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2))
|
||||
|
||||
self._update_hard_mode_lock_image()
|
||||
|
|
@ -363,232 +363,21 @@ class CoopBrowserWindow(ba.Window):
|
|||
ba.print_exception('Error updating campaign lock.')
|
||||
|
||||
def _update_for_data(self, data: list[dict[str, Any]] | None) -> None:
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-branches
|
||||
from ba.internal import getcampaign, get_tournament_prize_strings
|
||||
|
||||
# If the number of tournaments or challenges in the data differs from
|
||||
# our current arrangement, refresh with the new number.
|
||||
if ((data is None and self._tournament_button_count != 0)
|
||||
or (data is not None and
|
||||
(len(data) != self._tournament_button_count))):
|
||||
self._tournament_button_count = len(
|
||||
data) if data is not None else 0
|
||||
self._tournament_button_count = (len(data)
|
||||
if data is not None else 0)
|
||||
ba.app.config['Tournament Rows'] = self._tournament_button_count
|
||||
self._refresh()
|
||||
|
||||
# Update all of our tourney buttons based on whats in data.
|
||||
for i, tbtn in enumerate(self._tournament_buttons):
|
||||
assert data is not None
|
||||
entry: dict[str, Any] = data[i]
|
||||
prize_y_offs = (34 if 'prizeRange3' in entry else
|
||||
20 if 'prizeRange2' in entry else 12)
|
||||
x_offs = 90
|
||||
|
||||
# This seems to be a false alarm.
|
||||
# pylint: disable=unbalanced-tuple-unpacking
|
||||
pr1, pv1, pr2, pv2, pr3, pv3 = (
|
||||
get_tournament_prize_strings(entry))
|
||||
# pylint: enable=unbalanced-tuple-unpacking
|
||||
enabled = 'requiredLeague' not in entry
|
||||
ba.buttonwidget(edit=tbtn['button'],
|
||||
color=(0.5, 0.7, 0.2) if enabled else
|
||||
(0.5, 0.5, 0.5))
|
||||
ba.imagewidget(edit=tbtn['lock_image'],
|
||||
opacity=0.0 if enabled else 1.0)
|
||||
ba.textwidget(edit=tbtn['prize_range_1_text'],
|
||||
text='-' if pr1 == '' else pr1,
|
||||
position=(tbtn['button_x'] + 365 + x_offs,
|
||||
tbtn['button_y'] + tbtn['button_scale_y'] -
|
||||
93 + prize_y_offs))
|
||||
|
||||
# We want to draw values containing tickets a bit smaller
|
||||
# (scratch that; we now draw medals a bit bigger).
|
||||
ticket_char = ba.charstr(ba.SpecialChar.TICKET_BACKING)
|
||||
prize_value_scale_large = 1.0
|
||||
prize_value_scale_small = 1.0
|
||||
|
||||
ba.textwidget(edit=tbtn['prize_value_1_text'],
|
||||
text='-' if pv1 == '' else pv1,
|
||||
scale=prize_value_scale_large if ticket_char
|
||||
not in pv1 else prize_value_scale_small,
|
||||
position=(tbtn['button_x'] + 380 + x_offs,
|
||||
tbtn['button_y'] + tbtn['button_scale_y'] -
|
||||
93 + prize_y_offs))
|
||||
|
||||
ba.textwidget(edit=tbtn['prize_range_2_text'],
|
||||
text=pr2,
|
||||
position=(tbtn['button_x'] + 365 + x_offs,
|
||||
tbtn['button_y'] + tbtn['button_scale_y'] -
|
||||
93 - 45 + prize_y_offs))
|
||||
ba.textwidget(edit=tbtn['prize_value_2_text'],
|
||||
text=pv2,
|
||||
scale=prize_value_scale_large if ticket_char
|
||||
not in pv2 else prize_value_scale_small,
|
||||
position=(tbtn['button_x'] + 380 + x_offs,
|
||||
tbtn['button_y'] + tbtn['button_scale_y'] -
|
||||
93 - 45 + prize_y_offs))
|
||||
|
||||
ba.textwidget(edit=tbtn['prize_range_3_text'],
|
||||
text=pr3,
|
||||
position=(tbtn['button_x'] + 365 + x_offs,
|
||||
tbtn['button_y'] + tbtn['button_scale_y'] -
|
||||
93 - 90 + prize_y_offs))
|
||||
ba.textwidget(edit=tbtn['prize_value_3_text'],
|
||||
text=pv3,
|
||||
scale=prize_value_scale_large if ticket_char
|
||||
not in pv3 else prize_value_scale_small,
|
||||
position=(tbtn['button_x'] + 380 + x_offs,
|
||||
tbtn['button_y'] + tbtn['button_scale_y'] -
|
||||
93 - 90 + prize_y_offs))
|
||||
|
||||
leader_name = '-'
|
||||
leader_score: str | ba.Lstr = '-'
|
||||
if entry['scores']:
|
||||
score = tbtn['leader'] = copy.deepcopy(entry['scores'][0])
|
||||
leader_name = score[1]
|
||||
leader_score = (ba.timestring(
|
||||
score[0] * 10,
|
||||
centi=True,
|
||||
timeformat=ba.TimeFormat.MILLISECONDS,
|
||||
suppress_format_warning=True) if entry['scoreType']
|
||||
== 'time' else str(score[0]))
|
||||
else:
|
||||
tbtn['leader'] = None
|
||||
|
||||
ba.textwidget(edit=tbtn['current_leader_name_text'],
|
||||
text=ba.Lstr(value=leader_name))
|
||||
self._tournament_leader_score_type = (entry['scoreType'])
|
||||
ba.textwidget(edit=tbtn['current_leader_score_text'],
|
||||
text=leader_score)
|
||||
ba.buttonwidget(edit=tbtn['more_scores_button'],
|
||||
label=ba.Lstr(resource=self._r + '.seeMoreText'))
|
||||
out_of_time_text: str | ba.Lstr = (
|
||||
'-' if 'totalTime' not in entry else ba.Lstr(
|
||||
resource=self._r + '.ofTotalTimeText',
|
||||
subs=[('${TOTAL}',
|
||||
ba.timestring(entry['totalTime'],
|
||||
centi=False,
|
||||
suppress_format_warning=True))]))
|
||||
ba.textwidget(edit=tbtn['time_remaining_out_of_text'],
|
||||
text=out_of_time_text)
|
||||
|
||||
tbtn['time_remaining'] = entry['timeRemaining']
|
||||
tbtn['has_time_remaining'] = entry is not None
|
||||
tbtn['tournament_id'] = entry['tournamentID']
|
||||
tbtn['required_league'] = (None if 'requiredLeague' not in entry
|
||||
else entry['requiredLeague'])
|
||||
|
||||
game = ba.app.accounts_v1.tournament_info[
|
||||
tbtn['tournament_id']]['game']
|
||||
|
||||
if game is None:
|
||||
ba.textwidget(edit=tbtn['button_text'], text='-')
|
||||
ba.imagewidget(edit=tbtn['image'],
|
||||
texture=ba.gettexture('black'),
|
||||
opacity=0.2)
|
||||
else:
|
||||
campaignname, levelname = game.split(':')
|
||||
campaign = getcampaign(campaignname)
|
||||
max_players = ba.app.accounts_v1.tournament_info[
|
||||
tbtn['tournament_id']]['maxPlayers']
|
||||
txt = ba.Lstr(
|
||||
value='${A} ${B}',
|
||||
subs=[('${A}', campaign.getlevel(levelname).displayname),
|
||||
('${B}',
|
||||
ba.Lstr(resource='playerCountAbbreviatedText',
|
||||
subs=[('${COUNT}', str(max_players))]))])
|
||||
ba.textwidget(edit=tbtn['button_text'], text=txt)
|
||||
ba.imagewidget(
|
||||
edit=tbtn['image'],
|
||||
texture=campaign.getlevel(levelname).get_preview_texture(),
|
||||
opacity=1.0 if enabled else 0.5)
|
||||
|
||||
fee = entry['fee']
|
||||
|
||||
if fee is None:
|
||||
fee_var = None
|
||||
elif fee == 4:
|
||||
fee_var = 'price.tournament_entry_4'
|
||||
elif fee == 3:
|
||||
fee_var = 'price.tournament_entry_3'
|
||||
elif fee == 2:
|
||||
fee_var = 'price.tournament_entry_2'
|
||||
elif fee == 1:
|
||||
fee_var = 'price.tournament_entry_1'
|
||||
else:
|
||||
if fee != 0:
|
||||
print('Unknown fee value:', fee)
|
||||
fee_var = 'price.tournament_entry_0'
|
||||
|
||||
tbtn['allow_ads'] = allow_ads = entry['allowAds']
|
||||
|
||||
final_fee: int | None = (None if fee_var is None else
|
||||
_ba.get_v1_account_misc_read_val(
|
||||
fee_var, '?'))
|
||||
|
||||
final_fee_str: str | ba.Lstr
|
||||
if fee_var is None:
|
||||
final_fee_str = ''
|
||||
else:
|
||||
if final_fee == 0:
|
||||
final_fee_str = ba.Lstr(
|
||||
resource='getTicketsWindow.freeText')
|
||||
else:
|
||||
final_fee_str = (
|
||||
ba.charstr(ba.SpecialChar.TICKET_BACKING) +
|
||||
str(final_fee))
|
||||
|
||||
ad_tries_remaining = ba.app.accounts_v1.tournament_info[
|
||||
tbtn['tournament_id']]['adTriesRemaining']
|
||||
free_tries_remaining = ba.app.accounts_v1.tournament_info[
|
||||
tbtn['tournament_id']]['freeTriesRemaining']
|
||||
|
||||
# Now, if this fee allows ads and we support video ads, show
|
||||
# the 'or ad' version.
|
||||
if allow_ads and _ba.has_video_ads():
|
||||
ads_enabled = _ba.have_incentivized_ad()
|
||||
ba.imagewidget(edit=tbtn['entry_fee_ad_image'],
|
||||
opacity=1.0 if ads_enabled else 0.25)
|
||||
or_text = ba.Lstr(resource='orText',
|
||||
subs=[('${A}', ''),
|
||||
('${B}', '')]).evaluate().strip()
|
||||
ba.textwidget(edit=tbtn['entry_fee_text_or'], text=or_text)
|
||||
ba.textwidget(
|
||||
edit=tbtn['entry_fee_text_top'],
|
||||
position=(tbtn['button_x'] + 360,
|
||||
tbtn['button_y'] + tbtn['button_scale_y'] - 60),
|
||||
scale=1.3,
|
||||
text=final_fee_str)
|
||||
|
||||
# Possibly show number of ad-plays remaining.
|
||||
ba.textwidget(
|
||||
edit=tbtn['entry_fee_text_remaining'],
|
||||
position=(tbtn['button_x'] + 360,
|
||||
tbtn['button_y'] + tbtn['button_scale_y'] - 146),
|
||||
text='' if ad_tries_remaining in [None, 0] else
|
||||
('' + str(ad_tries_remaining)),
|
||||
color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2))
|
||||
else:
|
||||
ba.imagewidget(edit=tbtn['entry_fee_ad_image'], opacity=0.0)
|
||||
ba.textwidget(edit=tbtn['entry_fee_text_or'], text='')
|
||||
ba.textwidget(
|
||||
edit=tbtn['entry_fee_text_top'],
|
||||
position=(tbtn['button_x'] + 360,
|
||||
tbtn['button_y'] + tbtn['button_scale_y'] - 80),
|
||||
scale=1.3,
|
||||
text=final_fee_str)
|
||||
|
||||
# Possibly show number of free-plays remaining.
|
||||
ba.textwidget(
|
||||
edit=tbtn['entry_fee_text_remaining'],
|
||||
position=(tbtn['button_x'] + 360,
|
||||
tbtn['button_y'] + tbtn['button_scale_y'] - 100),
|
||||
text=('' if (free_tries_remaining in [None, 0]
|
||||
or final_fee != 0) else
|
||||
('' + str(free_tries_remaining))),
|
||||
color=(0.6, 0.6, 0.6, 1))
|
||||
tbtn.update_for_data(data[i])
|
||||
|
||||
def _on_tournament_query_response(self,
|
||||
data: dict[str, Any] | None) -> None:
|
||||
|
|
@ -715,10 +504,13 @@ class CoopBrowserWindow(ba.Window):
|
|||
items = [
|
||||
campaignname + ':Onslaught Training',
|
||||
campaignname + ':Rookie Onslaught',
|
||||
campaignname + ':Rookie Football', campaignname + ':Pro Onslaught',
|
||||
campaignname + ':Pro Football', campaignname + ':Pro Runaround',
|
||||
campaignname + ':Uber Onslaught', campaignname + ':Uber Football',
|
||||
campaignname + ':Uber Runaround'
|
||||
campaignname + ':Rookie Football',
|
||||
campaignname + ':Pro Onslaught',
|
||||
campaignname + ':Pro Football',
|
||||
campaignname + ':Pro Runaround',
|
||||
campaignname + ':Uber Onslaught',
|
||||
campaignname + ':Uber Football',
|
||||
campaignname + ':Uber Runaround',
|
||||
]
|
||||
items += [campaignname + ':The Last Stand']
|
||||
if self._selected_campaign_level is None:
|
||||
|
|
@ -772,6 +564,7 @@ class CoopBrowserWindow(ba.Window):
|
|||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.coop.gamebutton import GameButton
|
||||
from bastd.ui.coop.tournamentbutton import TournamentButton
|
||||
|
||||
# (Re)create the sub-container if need be.
|
||||
if self._subcontainer is not None:
|
||||
|
|
@ -839,7 +632,7 @@ class CoopBrowserWindow(ba.Window):
|
|||
|
||||
# Tournaments
|
||||
|
||||
self._tournament_buttons: list[dict[str, Any]] = []
|
||||
self._tournament_buttons: list[TournamentButton] = []
|
||||
|
||||
v -= 53
|
||||
# FIXME shouldn't use hard-coded strings here.
|
||||
|
|
@ -919,10 +712,15 @@ class CoopBrowserWindow(ba.Window):
|
|||
v2 = -2
|
||||
is_last_sel = True
|
||||
self._tournament_buttons.append(
|
||||
self._tournament_button(sc2, h, v2, is_last_sel))
|
||||
TournamentButton(sc2,
|
||||
h,
|
||||
v2,
|
||||
is_last_sel,
|
||||
on_pressed=ba.WeakCall(
|
||||
self.run_tournament)))
|
||||
v -= 200
|
||||
|
||||
# Custom Games.
|
||||
# Custom Games. (called 'Practice' in UI these days).
|
||||
v -= 50
|
||||
ba.textwidget(parent=w_parent,
|
||||
position=(h_base + 27, v + 30 + 198),
|
||||
|
|
@ -949,14 +747,13 @@ class CoopBrowserWindow(ba.Window):
|
|||
if _ba.get_v1_account_misc_read_val(
|
||||
'easter', False) or _ba.get_purchased('games.easter_egg_hunt'):
|
||||
items = [
|
||||
'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt'
|
||||
'Challenges:Easter Egg Hunt',
|
||||
'Challenges:Pro Easter Egg Hunt',
|
||||
] + items
|
||||
|
||||
# add all custom user levels here..
|
||||
# items += [
|
||||
# 'User:' + l.getname()
|
||||
# for l in getcampaign('User').getlevels()
|
||||
# ]
|
||||
# If we've defined custom games, put them at the beginning.
|
||||
if ba.app.custom_coop_practice_games:
|
||||
items = ba.app.custom_coop_practice_games + items
|
||||
|
||||
self._custom_h_scroll = custom_h_scroll = h_scroll = ba.hscrollwidget(
|
||||
parent=w_parent,
|
||||
|
|
@ -995,19 +792,19 @@ class CoopBrowserWindow(ba.Window):
|
|||
|
||||
for i, tbutton in enumerate(self._tournament_buttons):
|
||||
ba.widget(
|
||||
edit=tbutton['button'],
|
||||
edit=tbutton.button,
|
||||
up_widget=self._tournament_info_button
|
||||
if i == 0 else self._tournament_buttons[i - 1]['button'],
|
||||
down_widget=self._tournament_buttons[(i + 1)]['button']
|
||||
if i == 0 else self._tournament_buttons[i - 1].button,
|
||||
down_widget=self._tournament_buttons[(i + 1)].button
|
||||
if i + 1 < len(self._tournament_buttons) else custom_h_scroll)
|
||||
ba.widget(
|
||||
edit=tbutton['more_scores_button'],
|
||||
edit=tbutton.more_scores_button,
|
||||
down_widget=self._tournament_buttons[(
|
||||
i + 1)]['current_leader_name_text']
|
||||
i + 1)].current_leader_name_text
|
||||
if i + 1 < len(self._tournament_buttons) else custom_h_scroll)
|
||||
ba.widget(edit=tbutton['current_leader_name_text'],
|
||||
ba.widget(edit=tbutton.current_leader_name_text,
|
||||
up_widget=self._tournament_info_button if i == 0 else
|
||||
self._tournament_buttons[i - 1]['more_scores_button'])
|
||||
self._tournament_buttons[i - 1].more_scores_button)
|
||||
|
||||
for btn in self._custom_buttons:
|
||||
try:
|
||||
|
|
@ -1037,314 +834,6 @@ class CoopBrowserWindow(ba.Window):
|
|||
def _enable_selectable_callback(self) -> None:
|
||||
self._do_selection_callbacks = True
|
||||
|
||||
def _tournament_button(self, parent: ba.Widget, x: float, y: float,
|
||||
select: bool) -> dict[str, Any]:
|
||||
sclx = 300
|
||||
scly = 195.0
|
||||
data: dict[str, Any] = {
|
||||
'tournament_id': None,
|
||||
'time_remaining': 0,
|
||||
'has_time_remaining': False,
|
||||
'leader': None
|
||||
}
|
||||
data['button'] = btn = ba.buttonwidget(
|
||||
parent=parent,
|
||||
position=(x + 23, y + 4),
|
||||
size=(sclx, scly),
|
||||
label='',
|
||||
button_type='square',
|
||||
autoselect=True,
|
||||
on_activate_call=lambda: self.run(None, tournament_button=data))
|
||||
ba.widget(edit=btn,
|
||||
show_buffer_bottom=50,
|
||||
show_buffer_top=50,
|
||||
show_buffer_left=400,
|
||||
show_buffer_right=200)
|
||||
if select:
|
||||
ba.containerwidget(edit=parent,
|
||||
selected_child=btn,
|
||||
visible_child=btn)
|
||||
image_width = sclx * 0.85 * 0.75
|
||||
|
||||
data['image'] = ba.imagewidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 21 + sclx * 0.5 - image_width * 0.5, y + scly - 150),
|
||||
size=(image_width, image_width * 0.5),
|
||||
model_transparent=self.lsbt,
|
||||
model_opaque=self.lsbo,
|
||||
texture=ba.gettexture('black'),
|
||||
opacity=0.2,
|
||||
mask_texture=ba.gettexture('mapPreviewMask'))
|
||||
|
||||
data['lock_image'] = ba.imagewidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 21 + sclx * 0.5 - image_width * 0.25,
|
||||
y + scly - 150),
|
||||
size=(image_width * 0.5, image_width * 0.5),
|
||||
texture=ba.gettexture('lock'),
|
||||
opacity=0.0)
|
||||
|
||||
data['button_text'] = ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 20 + sclx * 0.5,
|
||||
y + scly - 35),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=sclx * 0.76,
|
||||
scale=0.85,
|
||||
color=(0.8, 1.0, 0.8, 1.0))
|
||||
|
||||
header_color = (0.43, 0.4, 0.5, 1)
|
||||
value_color = (0.6, 0.6, 0.6, 1)
|
||||
|
||||
x_offs = 0
|
||||
ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 360, y + scly - 20),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text=ba.Lstr(resource=self._r + '.entryFeeText'),
|
||||
v_align='center',
|
||||
maxwidth=100,
|
||||
scale=0.9,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
|
||||
data['entry_fee_text_top'] = ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 360,
|
||||
y + scly - 60),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=60,
|
||||
scale=1.3,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
data['entry_fee_text_or'] = ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 360,
|
||||
y + scly - 90),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='',
|
||||
v_align='center',
|
||||
maxwidth=60,
|
||||
scale=0.5,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
data['entry_fee_text_remaining'] = ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 360, y +
|
||||
scly - 90),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='',
|
||||
v_align='center',
|
||||
maxwidth=60,
|
||||
scale=0.5,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
|
||||
data['entry_fee_ad_image'] = ba.imagewidget(
|
||||
parent=parent,
|
||||
size=(40, 40),
|
||||
draw_controller=btn,
|
||||
position=(x + 360 - 20, y + scly - 140),
|
||||
opacity=0.0,
|
||||
texture=ba.gettexture('tv'))
|
||||
|
||||
x_offs += 50
|
||||
|
||||
ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 447 + x_offs, y + scly - 20),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text=ba.Lstr(resource=self._r + '.prizesText'),
|
||||
v_align='center',
|
||||
maxwidth=130,
|
||||
scale=0.9,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
|
||||
data['button_x'] = x
|
||||
data['button_y'] = y
|
||||
data['button_scale_y'] = scly
|
||||
|
||||
xo2 = 0
|
||||
prize_value_scale = 1.5
|
||||
|
||||
data['prize_range_1_text'] = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 355 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
maxwidth=50,
|
||||
text='-',
|
||||
scale=0.8,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
data['prize_value_1_text'] = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 380 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='left',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=100,
|
||||
scale=prize_value_scale,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
|
||||
data['prize_range_2_text'] = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 355 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
maxwidth=50,
|
||||
scale=0.8,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
data['prize_value_2_text'] = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 380 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='left',
|
||||
text='',
|
||||
v_align='center',
|
||||
maxwidth=100,
|
||||
scale=prize_value_scale,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
|
||||
data['prize_range_3_text'] = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 355 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
maxwidth=50,
|
||||
scale=0.8,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
data['prize_value_3_text'] = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 380 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='left',
|
||||
text='',
|
||||
v_align='center',
|
||||
maxwidth=100,
|
||||
scale=prize_value_scale,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
|
||||
ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 620 + x_offs, y + scly - 20),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text=ba.Lstr(resource=self._r + '.currentBestText'),
|
||||
v_align='center',
|
||||
maxwidth=180,
|
||||
scale=0.9,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
data['current_leader_name_text'] = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 620 + x_offs - (170 / 1.4) * 0.5,
|
||||
y + scly - 60 - 40 * 0.5),
|
||||
selectable=True,
|
||||
click_activate=True,
|
||||
autoselect=True,
|
||||
on_activate_call=lambda: self._show_leader(tournament_button=data),
|
||||
size=(170 / 1.4, 40),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=170,
|
||||
scale=1.4,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
data['current_leader_score_text'] = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 620 + x_offs, y + scly - 113 + 10),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=170,
|
||||
scale=1.8,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
|
||||
data['more_scores_button'] = ba.buttonwidget(
|
||||
parent=parent,
|
||||
position=(x + 620 + x_offs - 60, y + scly - 50 - 125),
|
||||
color=(0.5, 0.5, 0.6),
|
||||
textcolor=(0.7, 0.7, 0.8),
|
||||
label='-',
|
||||
size=(120, 40),
|
||||
autoselect=True,
|
||||
up_widget=data['current_leader_name_text'],
|
||||
text_scale=0.6,
|
||||
on_activate_call=lambda: self._show_scores(tournament_button=data))
|
||||
ba.widget(edit=data['current_leader_name_text'],
|
||||
down_widget=data['more_scores_button'])
|
||||
|
||||
ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 820 + x_offs, y + scly - 20),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text=ba.Lstr(resource=self._r + '.timeRemainingText'),
|
||||
v_align='center',
|
||||
maxwidth=180,
|
||||
scale=0.9,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
data['time_remaining_value_text'] = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 820 + x_offs, y + scly - 68),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=180,
|
||||
scale=2.0,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
data['time_remaining_out_of_text'] = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 820 + x_offs, y + scly - 110),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=120,
|
||||
scale=0.72,
|
||||
color=(0.4, 0.4, 0.5),
|
||||
flatness=1.0)
|
||||
return data
|
||||
|
||||
def _switch_to_league_rankings(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.account import show_sign_in_prompt
|
||||
|
|
@ -1378,100 +867,20 @@ class CoopBrowserWindow(ba.Window):
|
|||
show_tab=show_tab,
|
||||
back_location='CoopBrowserWindow').get_root_widget())
|
||||
|
||||
def _show_leader(self, tournament_button: dict[str, Any]) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.account.viewer import AccountViewerWindow
|
||||
tournament_id = tournament_button['tournament_id']
|
||||
|
||||
# FIXME: This assumes a single player entry in leader; should expand
|
||||
# this to work with multiple.
|
||||
if tournament_id is None or tournament_button['leader'] is None or len(
|
||||
tournament_button['leader'][2]) != 1:
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
AccountViewerWindow(
|
||||
account_id=tournament_button['leader'][2][0].get('a', None),
|
||||
profile_id=tournament_button['leader'][2][0].get('p', None),
|
||||
position=tournament_button['current_leader_name_text'].
|
||||
get_screen_space_center())
|
||||
|
||||
def _show_scores(self, tournament_button: dict[str, Any]) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.tournamentscores import TournamentScoresWindow
|
||||
tournament_id = tournament_button['tournament_id']
|
||||
if tournament_id is None:
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
TournamentScoresWindow(
|
||||
tournament_id=tournament_id,
|
||||
position=tournament_button['more_scores_button'].
|
||||
get_screen_space_center())
|
||||
|
||||
def is_tourney_data_up_to_date(self) -> bool:
|
||||
"""Return whether our tourney data is up to date."""
|
||||
return self._tourney_data_up_to_date
|
||||
|
||||
def run(self,
|
||||
game: str | None,
|
||||
tournament_button: dict[str, Any] | None = None) -> None:
|
||||
def run_game(self, game: str) -> None:
|
||||
"""Run the provided game."""
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-return-statements
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.confirm import ConfirmWindow
|
||||
from bastd.ui.tournamententry import TournamentEntryWindow
|
||||
from bastd.ui.purchase import PurchaseWindow
|
||||
from bastd.ui.account import show_sign_in_prompt
|
||||
args: dict[str, Any] = {}
|
||||
|
||||
# Do a bit of pre-flight for tournament options.
|
||||
if tournament_button is not None:
|
||||
|
||||
if _ba.get_v1_account_state() != 'signed_in':
|
||||
show_sign_in_prompt()
|
||||
return
|
||||
|
||||
if not self._tourney_data_up_to_date:
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource='tournamentCheckingStateText'),
|
||||
color=(1, 1, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
if tournament_button['tournament_id'] is None:
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource='internal.unavailableNoConnectionText'),
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
if tournament_button['required_league'] is not None:
|
||||
ba.screenmessage(ba.Lstr(
|
||||
resource='league.tournamentLeagueText',
|
||||
subs=[
|
||||
('${NAME}',
|
||||
ba.Lstr(
|
||||
translate=('leagueNames',
|
||||
tournament_button['required_league'])))
|
||||
]),
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
if tournament_button['time_remaining'] <= 0:
|
||||
ba.screenmessage(ba.Lstr(resource='tournamentEndedText'),
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
# Game is whatever the tournament tells us it is.
|
||||
game = ba.app.accounts_v1.tournament_info[
|
||||
tournament_button['tournament_id']]['game']
|
||||
|
||||
if tournament_button is None and game == 'Easy:The Last Stand':
|
||||
if game == 'Easy:The Last Stand':
|
||||
ConfirmWindow(ba.Lstr(resource='difficultyHardUnlockOnlyText',
|
||||
fallback_resource='difficultyHardOnlyText'),
|
||||
cancel_button=False,
|
||||
|
|
@ -1479,12 +888,11 @@ class CoopBrowserWindow(ba.Window):
|
|||
height=130)
|
||||
return
|
||||
|
||||
# Infinite onslaught/runaround require pro; bring up a store link if
|
||||
# need be.
|
||||
if tournament_button is None and game in (
|
||||
'Challenges:Infinite Runaround',
|
||||
'Challenges:Infinite Onslaught'
|
||||
) and not ba.app.accounts_v1.have_pro():
|
||||
# Infinite onslaught/runaround require pro; bring up a store link
|
||||
# if need be.
|
||||
if game in ('Challenges:Infinite Runaround',
|
||||
'Challenges:Infinite Onslaught'
|
||||
) and not ba.app.accounts_v1.have_pro():
|
||||
if _ba.get_v1_account_state() != 'signed_in':
|
||||
show_sign_in_prompt()
|
||||
else:
|
||||
|
|
@ -1495,7 +903,8 @@ class CoopBrowserWindow(ba.Window):
|
|||
if game in ['Challenges:Meteor Shower']:
|
||||
required_purchase = 'games.meteor_shower'
|
||||
elif game in [
|
||||
'Challenges:Target Practice', 'Challenges:Target Practice B'
|
||||
'Challenges:Target Practice',
|
||||
'Challenges:Target Practice B',
|
||||
]:
|
||||
required_purchase = 'games.target_practice'
|
||||
elif game in ['Challenges:Ninja Fight']:
|
||||
|
|
@ -1503,13 +912,14 @@ class CoopBrowserWindow(ba.Window):
|
|||
elif game in ['Challenges:Pro Ninja Fight']:
|
||||
required_purchase = 'games.ninja_fight'
|
||||
elif game in [
|
||||
'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt'
|
||||
'Challenges:Easter Egg Hunt',
|
||||
'Challenges:Pro Easter Egg Hunt',
|
||||
]:
|
||||
required_purchase = 'games.easter_egg_hunt'
|
||||
else:
|
||||
required_purchase = None
|
||||
|
||||
if (tournament_button is None and required_purchase is not None
|
||||
if (required_purchase is not None
|
||||
and not _ba.get_purchased(required_purchase)):
|
||||
if _ba.get_v1_account_state() != 'signed_in':
|
||||
show_sign_in_prompt()
|
||||
|
|
@ -1519,17 +929,57 @@ class CoopBrowserWindow(ba.Window):
|
|||
|
||||
self._save_state()
|
||||
|
||||
# For tournaments, we pop up the entry window.
|
||||
if tournament_button is not None:
|
||||
TournamentEntryWindow(
|
||||
tournament_id=tournament_button['tournament_id'],
|
||||
position=tournament_button['button'].get_screen_space_center())
|
||||
else:
|
||||
# Otherwise just dive right in.
|
||||
assert game is not None
|
||||
if ba.app.launch_coop_game(game, args=args):
|
||||
ba.containerwidget(edit=self._root_widget,
|
||||
transition='out_left')
|
||||
if ba.app.launch_coop_game(game, args=args):
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
|
||||
def run_tournament(self, tournament_button: TournamentButton) -> None:
|
||||
"""Run the provided tournament game."""
|
||||
from bastd.ui.account import show_sign_in_prompt
|
||||
from bastd.ui.tournamententry import TournamentEntryWindow
|
||||
|
||||
if _ba.get_v1_account_state() != 'signed_in':
|
||||
show_sign_in_prompt()
|
||||
return
|
||||
|
||||
if not self._tourney_data_up_to_date:
|
||||
ba.screenmessage(ba.Lstr(resource='tournamentCheckingStateText'),
|
||||
color=(1, 1, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
if tournament_button.tournament_id is None:
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource='internal.unavailableNoConnectionText'),
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
if tournament_button.required_league is not None:
|
||||
ba.screenmessage(
|
||||
ba.Lstr(
|
||||
resource='league.tournamentLeagueText',
|
||||
subs=[('${NAME}',
|
||||
ba.Lstr(
|
||||
translate=('leagueNames',
|
||||
tournament_button.required_league)))
|
||||
]),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
if tournament_button.time_remaining <= 0:
|
||||
ba.screenmessage(ba.Lstr(resource='tournamentEndedText'),
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
self._save_state()
|
||||
|
||||
assert tournament_button.tournament_id is not None
|
||||
TournamentEntryWindow(
|
||||
tournament_id=tournament_button.tournament_id,
|
||||
position=tournament_button.button.get_screen_space_center())
|
||||
|
||||
def _back(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
|
|
@ -1542,24 +992,6 @@ class CoopBrowserWindow(ba.Window):
|
|||
ba.app.ui.set_main_menu_window(
|
||||
PlayWindow(transition='in_left').get_root_widget())
|
||||
|
||||
def _restore_state(self) -> None:
|
||||
try:
|
||||
sel_name = ba.app.ui.window_states.get(type(self),
|
||||
{}).get('sel_name')
|
||||
if sel_name == 'Back':
|
||||
sel = self._back_button
|
||||
elif sel_name == 'Scroll':
|
||||
sel = self._scrollwidget
|
||||
elif sel_name == 'PowerRanking':
|
||||
sel = self._league_rank_button_widget
|
||||
elif sel_name == 'Store':
|
||||
sel = self._store_button_widget
|
||||
else:
|
||||
sel = self._scrollwidget
|
||||
ba.containerwidget(edit=self._root_widget, selected_child=sel)
|
||||
except Exception:
|
||||
ba.print_exception(f'Error restoring state for {self}.')
|
||||
|
||||
def _save_state(self) -> None:
|
||||
cfg = ba.app.config
|
||||
try:
|
||||
|
|
@ -1580,16 +1012,31 @@ class CoopBrowserWindow(ba.Window):
|
|||
|
||||
cfg['Selected Coop Row'] = self._selected_row
|
||||
cfg['Selected Coop Custom Level'] = self._selected_custom_level
|
||||
cfg['Selected Coop Challenge Level'] = self._selected_challenge_level
|
||||
cfg['Selected Coop Campaign Level'] = self._selected_campaign_level
|
||||
cfg.commit()
|
||||
|
||||
def _restore_state(self) -> None:
|
||||
try:
|
||||
sel_name = ba.app.ui.window_states.get(type(self),
|
||||
{}).get('sel_name')
|
||||
if sel_name == 'Back':
|
||||
sel = self._back_button
|
||||
elif sel_name == 'Scroll':
|
||||
sel = self._scrollwidget
|
||||
elif sel_name == 'PowerRanking':
|
||||
sel = self._league_rank_button_widget
|
||||
elif sel_name == 'Store':
|
||||
sel = self._store_button_widget
|
||||
else:
|
||||
sel = self._scrollwidget
|
||||
ba.containerwidget(edit=self._root_widget, selected_child=sel)
|
||||
except Exception:
|
||||
ba.print_exception(f'Error restoring state for {self}.')
|
||||
|
||||
def sel_change(self, row: str, game: str) -> None:
|
||||
"""(internal)"""
|
||||
if self._do_selection_callbacks:
|
||||
if row == 'custom':
|
||||
self._selected_custom_level = game
|
||||
if row == 'challenges':
|
||||
self._selected_challenge_level = game
|
||||
elif row == 'campaign':
|
||||
self._selected_campaign_level = game
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class GameButton:
|
|||
position=(x + 23, y + 4),
|
||||
size=(sclx, scly),
|
||||
label='',
|
||||
on_activate_call=ba.Call(window.run, game),
|
||||
on_activate_call=ba.Call(window.run_game, game),
|
||||
button_type='square',
|
||||
autoselect=True,
|
||||
on_select_call=ba.Call(window.sel_change, row, game))
|
||||
|
|
|
|||
561
dist/ba_data/python/bastd/ui/coop/tournamentbutton.py
vendored
Normal file
561
dist/ba_data/python/bastd/ui/coop/tournamentbutton.py
vendored
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Defines button for co-op games."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import copy
|
||||
|
||||
import ba
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class TournamentButton:
|
||||
"""Button showing a tournament in coop window."""
|
||||
|
||||
def __init__(self, parent: ba.Widget, x: float, y: float, select: bool,
|
||||
on_pressed: Callable[[TournamentButton], None]) -> None:
|
||||
self._r = 'coopSelectWindow'
|
||||
sclx = 300
|
||||
scly = 195.0
|
||||
self.on_pressed = on_pressed
|
||||
self.lsbt = ba.getmodel('level_select_button_transparent')
|
||||
self.lsbo = ba.getmodel('level_select_button_opaque')
|
||||
self.allow_ads = False
|
||||
self.tournament_id: str | None = None
|
||||
self.time_remaining: int = 0
|
||||
self.has_time_remaining: bool = False
|
||||
self.leader: Any = None
|
||||
self.required_league: str | None = None
|
||||
self.button = btn = ba.buttonwidget(
|
||||
parent=parent,
|
||||
position=(x + 23, y + 4),
|
||||
size=(sclx, scly),
|
||||
label='',
|
||||
button_type='square',
|
||||
autoselect=True,
|
||||
# on_activate_call=lambda: self.run(None, tournament_button=data)
|
||||
on_activate_call=ba.WeakCall(self._pressed))
|
||||
ba.widget(edit=btn,
|
||||
show_buffer_bottom=50,
|
||||
show_buffer_top=50,
|
||||
show_buffer_left=400,
|
||||
show_buffer_right=200)
|
||||
if select:
|
||||
ba.containerwidget(edit=parent,
|
||||
selected_child=btn,
|
||||
visible_child=btn)
|
||||
image_width = sclx * 0.85 * 0.75
|
||||
|
||||
self.image = ba.imagewidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 21 + sclx * 0.5 - image_width * 0.5, y + scly - 150),
|
||||
size=(image_width, image_width * 0.5),
|
||||
model_transparent=self.lsbt,
|
||||
model_opaque=self.lsbo,
|
||||
texture=ba.gettexture('black'),
|
||||
opacity=0.2,
|
||||
mask_texture=ba.gettexture('mapPreviewMask'))
|
||||
|
||||
self.lock_image = ba.imagewidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 21 + sclx * 0.5 - image_width * 0.25,
|
||||
y + scly - 150),
|
||||
size=(image_width * 0.5, image_width * 0.5),
|
||||
texture=ba.gettexture('lock'),
|
||||
opacity=0.0)
|
||||
|
||||
self.button_text = ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 20 + sclx * 0.5,
|
||||
y + scly - 35),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=sclx * 0.76,
|
||||
scale=0.85,
|
||||
color=(0.8, 1.0, 0.8, 1.0))
|
||||
|
||||
header_color = (0.43, 0.4, 0.5, 1)
|
||||
value_color = (0.6, 0.6, 0.6, 1)
|
||||
|
||||
x_offs = 0
|
||||
ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 360, y + scly - 20),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text=ba.Lstr(resource=self._r + '.entryFeeText'),
|
||||
v_align='center',
|
||||
maxwidth=100,
|
||||
scale=0.9,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
|
||||
self.entry_fee_text_top = ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 360,
|
||||
y + scly - 60),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=60,
|
||||
scale=1.3,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
self.entry_fee_text_or = ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 360,
|
||||
y + scly - 90),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='',
|
||||
v_align='center',
|
||||
maxwidth=60,
|
||||
scale=0.5,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
self.entry_fee_text_remaining = ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 360,
|
||||
y + scly - 90),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='',
|
||||
v_align='center',
|
||||
maxwidth=60,
|
||||
scale=0.5,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
|
||||
self.entry_fee_ad_image = ba.imagewidget(parent=parent,
|
||||
size=(40, 40),
|
||||
draw_controller=btn,
|
||||
position=(x + 360 - 20,
|
||||
y + scly - 140),
|
||||
opacity=0.0,
|
||||
texture=ba.gettexture('tv'))
|
||||
|
||||
x_offs += 50
|
||||
|
||||
ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 447 + x_offs, y + scly - 20),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text=ba.Lstr(resource=self._r + '.prizesText'),
|
||||
v_align='center',
|
||||
maxwidth=130,
|
||||
scale=0.9,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
|
||||
self.button_x = x
|
||||
self.button_y = y
|
||||
self.button_scale_y = scly
|
||||
|
||||
xo2 = 0
|
||||
prize_value_scale = 1.5
|
||||
|
||||
self.prize_range_1_text = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 355 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
maxwidth=50,
|
||||
text='-',
|
||||
scale=0.8,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
self.prize_value_1_text = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 380 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='left',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=100,
|
||||
scale=prize_value_scale,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
|
||||
self.prize_range_2_text = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 355 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
maxwidth=50,
|
||||
scale=0.8,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
self.prize_value_2_text = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 380 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='left',
|
||||
text='',
|
||||
v_align='center',
|
||||
maxwidth=100,
|
||||
scale=prize_value_scale,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
|
||||
self.prize_range_3_text = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 355 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
maxwidth=50,
|
||||
scale=0.8,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
self.prize_value_3_text = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 380 + xo2 + x_offs, y + scly - 93),
|
||||
size=(0, 0),
|
||||
h_align='left',
|
||||
text='',
|
||||
v_align='center',
|
||||
maxwidth=100,
|
||||
scale=prize_value_scale,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
|
||||
ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 620 + x_offs, y + scly - 20),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text=ba.Lstr(resource=self._r + '.currentBestText'),
|
||||
v_align='center',
|
||||
maxwidth=180,
|
||||
scale=0.9,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
self.current_leader_name_text = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 620 + x_offs - (170 / 1.4) * 0.5,
|
||||
y + scly - 60 - 40 * 0.5),
|
||||
selectable=True,
|
||||
click_activate=True,
|
||||
autoselect=True,
|
||||
on_activate_call=ba.WeakCall(self._show_leader),
|
||||
size=(170 / 1.4, 40),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=170,
|
||||
scale=1.4,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
self.current_leader_score_text = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 620 + x_offs, y + scly - 113 + 10),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=170,
|
||||
scale=1.8,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
|
||||
self.more_scores_button = ba.buttonwidget(
|
||||
parent=parent,
|
||||
position=(x + 620 + x_offs - 60, y + scly - 50 - 125),
|
||||
color=(0.5, 0.5, 0.6),
|
||||
textcolor=(0.7, 0.7, 0.8),
|
||||
label='-',
|
||||
size=(120, 40),
|
||||
autoselect=True,
|
||||
up_widget=self.current_leader_name_text,
|
||||
text_scale=0.6,
|
||||
on_activate_call=ba.WeakCall(self._show_scores))
|
||||
ba.widget(edit=self.current_leader_name_text,
|
||||
down_widget=self.more_scores_button)
|
||||
|
||||
ba.textwidget(parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 820 + x_offs, y + scly - 20),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text=ba.Lstr(resource=self._r + '.timeRemainingText'),
|
||||
v_align='center',
|
||||
maxwidth=180,
|
||||
scale=0.9,
|
||||
color=header_color,
|
||||
flatness=1.0)
|
||||
self.time_remaining_value_text = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 820 + x_offs, y + scly - 68),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=180,
|
||||
scale=2.0,
|
||||
color=value_color,
|
||||
flatness=1.0)
|
||||
self.time_remaining_out_of_text = ba.textwidget(
|
||||
parent=parent,
|
||||
draw_controller=btn,
|
||||
position=(x + 820 + x_offs, y + scly - 110),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
text='-',
|
||||
v_align='center',
|
||||
maxwidth=120,
|
||||
scale=0.72,
|
||||
color=(0.4, 0.4, 0.5),
|
||||
flatness=1.0)
|
||||
|
||||
def _pressed(self) -> None:
|
||||
self.on_pressed(self)
|
||||
|
||||
def _show_leader(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.account.viewer import AccountViewerWindow
|
||||
tournament_id = self.tournament_id
|
||||
|
||||
# FIXME: This assumes a single player entry in leader; should expand
|
||||
# this to work with multiple.
|
||||
if tournament_id is None or self.leader is None or len(
|
||||
self.leader[2]) != 1:
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
AccountViewerWindow(
|
||||
account_id=self.leader[2][0].get('a', None),
|
||||
profile_id=self.leader[2][0].get('p', None),
|
||||
position=self.current_leader_name_text.get_screen_space_center())
|
||||
|
||||
def _show_scores(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.tournamentscores import TournamentScoresWindow
|
||||
tournament_id = self.tournament_id
|
||||
if tournament_id is None:
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
TournamentScoresWindow(
|
||||
tournament_id=tournament_id,
|
||||
position=self.more_scores_button.get_screen_space_center())
|
||||
|
||||
def update_for_data(self, entry: dict[str, Any]) -> None:
|
||||
"""Update for new incoming data."""
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-branches
|
||||
from ba.internal import getcampaign, get_tournament_prize_strings
|
||||
prize_y_offs = (34 if 'prizeRange3' in entry else
|
||||
20 if 'prizeRange2' in entry else 12)
|
||||
x_offs = 90
|
||||
|
||||
# This seems to be a false alarm.
|
||||
# pylint: disable=unbalanced-tuple-unpacking
|
||||
pr1, pv1, pr2, pv2, pr3, pv3 = (get_tournament_prize_strings(entry))
|
||||
# pylint: enable=unbalanced-tuple-unpacking
|
||||
enabled = 'requiredLeague' not in entry
|
||||
ba.buttonwidget(edit=self.button,
|
||||
color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5))
|
||||
ba.imagewidget(edit=self.lock_image, opacity=0.0 if enabled else 1.0)
|
||||
ba.textwidget(edit=self.prize_range_1_text,
|
||||
text='-' if pr1 == '' else pr1,
|
||||
position=(self.button_x + 365 + x_offs, self.button_y +
|
||||
self.button_scale_y - 93 + prize_y_offs))
|
||||
|
||||
# We want to draw values containing tickets a bit smaller
|
||||
# (scratch that; we now draw medals a bit bigger).
|
||||
ticket_char = ba.charstr(ba.SpecialChar.TICKET_BACKING)
|
||||
prize_value_scale_large = 1.0
|
||||
prize_value_scale_small = 1.0
|
||||
|
||||
ba.textwidget(edit=self.prize_value_1_text,
|
||||
text='-' if pv1 == '' else pv1,
|
||||
scale=prize_value_scale_large
|
||||
if ticket_char not in pv1 else prize_value_scale_small,
|
||||
position=(self.button_x + 380 + x_offs, self.button_y +
|
||||
self.button_scale_y - 93 + prize_y_offs))
|
||||
|
||||
ba.textwidget(edit=self.prize_range_2_text,
|
||||
text=pr2,
|
||||
position=(self.button_x + 365 + x_offs, self.button_y +
|
||||
self.button_scale_y - 93 - 45 + prize_y_offs))
|
||||
ba.textwidget(edit=self.prize_value_2_text,
|
||||
text=pv2,
|
||||
scale=prize_value_scale_large
|
||||
if ticket_char not in pv2 else prize_value_scale_small,
|
||||
position=(self.button_x + 380 + x_offs, self.button_y +
|
||||
self.button_scale_y - 93 - 45 + prize_y_offs))
|
||||
|
||||
ba.textwidget(edit=self.prize_range_3_text,
|
||||
text=pr3,
|
||||
position=(self.button_x + 365 + x_offs, self.button_y +
|
||||
self.button_scale_y - 93 - 90 + prize_y_offs))
|
||||
ba.textwidget(edit=self.prize_value_3_text,
|
||||
text=pv3,
|
||||
scale=prize_value_scale_large
|
||||
if ticket_char not in pv3 else prize_value_scale_small,
|
||||
position=(self.button_x + 380 + x_offs, self.button_y +
|
||||
self.button_scale_y - 93 - 90 + prize_y_offs))
|
||||
|
||||
leader_name = '-'
|
||||
leader_score: str | ba.Lstr = '-'
|
||||
if entry['scores']:
|
||||
score = self.leader = copy.deepcopy(entry['scores'][0])
|
||||
leader_name = score[1]
|
||||
leader_score = (ba.timestring(
|
||||
score[0] * 10,
|
||||
centi=True,
|
||||
timeformat=ba.TimeFormat.MILLISECONDS,
|
||||
suppress_format_warning=True)
|
||||
if entry['scoreType'] == 'time' else str(score[0]))
|
||||
else:
|
||||
self.leader = None
|
||||
|
||||
ba.textwidget(edit=self.current_leader_name_text,
|
||||
text=ba.Lstr(value=leader_name))
|
||||
ba.textwidget(edit=self.current_leader_score_text, text=leader_score)
|
||||
ba.buttonwidget(edit=self.more_scores_button,
|
||||
label=ba.Lstr(resource=self._r + '.seeMoreText'))
|
||||
out_of_time_text: str | ba.Lstr = (
|
||||
'-' if 'totalTime' not in entry else ba.Lstr(
|
||||
resource=self._r + '.ofTotalTimeText',
|
||||
subs=[('${TOTAL}',
|
||||
ba.timestring(entry['totalTime'],
|
||||
centi=False,
|
||||
suppress_format_warning=True))]))
|
||||
ba.textwidget(edit=self.time_remaining_out_of_text,
|
||||
text=out_of_time_text)
|
||||
|
||||
self.time_remaining = entry['timeRemaining']
|
||||
self.has_time_remaining = entry is not None
|
||||
self.tournament_id = entry['tournamentID']
|
||||
self.required_league = (None if 'requiredLeague' not in entry else
|
||||
entry['requiredLeague'])
|
||||
|
||||
game = ba.app.accounts_v1.tournament_info[self.tournament_id]['game']
|
||||
|
||||
if game is None:
|
||||
ba.textwidget(edit=self.button_text, text='-')
|
||||
ba.imagewidget(edit=self.image,
|
||||
texture=ba.gettexture('black'),
|
||||
opacity=0.2)
|
||||
else:
|
||||
campaignname, levelname = game.split(':')
|
||||
campaign = getcampaign(campaignname)
|
||||
max_players = ba.app.accounts_v1.tournament_info[
|
||||
self.tournament_id]['maxPlayers']
|
||||
txt = ba.Lstr(value='${A} ${B}',
|
||||
subs=[('${A}',
|
||||
campaign.getlevel(levelname).displayname),
|
||||
('${B}',
|
||||
ba.Lstr(resource='playerCountAbbreviatedText',
|
||||
subs=[('${COUNT}', str(max_players))
|
||||
]))])
|
||||
ba.textwidget(edit=self.button_text, text=txt)
|
||||
ba.imagewidget(
|
||||
edit=self.image,
|
||||
texture=campaign.getlevel(levelname).get_preview_texture(),
|
||||
opacity=1.0 if enabled else 0.5)
|
||||
|
||||
fee = entry['fee']
|
||||
|
||||
if fee is None:
|
||||
fee_var = None
|
||||
elif fee == 4:
|
||||
fee_var = 'price.tournament_entry_4'
|
||||
elif fee == 3:
|
||||
fee_var = 'price.tournament_entry_3'
|
||||
elif fee == 2:
|
||||
fee_var = 'price.tournament_entry_2'
|
||||
elif fee == 1:
|
||||
fee_var = 'price.tournament_entry_1'
|
||||
else:
|
||||
if fee != 0:
|
||||
print('Unknown fee value:', fee)
|
||||
fee_var = 'price.tournament_entry_0'
|
||||
|
||||
self.allow_ads = allow_ads = entry['allowAds']
|
||||
|
||||
final_fee: int | None = (None if fee_var is None else
|
||||
_ba.get_v1_account_misc_read_val(
|
||||
fee_var, '?'))
|
||||
|
||||
final_fee_str: str | ba.Lstr
|
||||
if fee_var is None:
|
||||
final_fee_str = ''
|
||||
else:
|
||||
if final_fee == 0:
|
||||
final_fee_str = ba.Lstr(resource='getTicketsWindow.freeText')
|
||||
else:
|
||||
final_fee_str = (ba.charstr(ba.SpecialChar.TICKET_BACKING) +
|
||||
str(final_fee))
|
||||
|
||||
ad_tries_remaining = ba.app.accounts_v1.tournament_info[
|
||||
self.tournament_id]['adTriesRemaining']
|
||||
free_tries_remaining = ba.app.accounts_v1.tournament_info[
|
||||
self.tournament_id]['freeTriesRemaining']
|
||||
|
||||
# Now, if this fee allows ads and we support video ads, show
|
||||
# the 'or ad' version.
|
||||
if allow_ads and _ba.has_video_ads():
|
||||
ads_enabled = _ba.have_incentivized_ad()
|
||||
ba.imagewidget(edit=self.entry_fee_ad_image,
|
||||
opacity=1.0 if ads_enabled else 0.25)
|
||||
or_text = ba.Lstr(resource='orText',
|
||||
subs=[('${A}', ''),
|
||||
('${B}', '')]).evaluate().strip()
|
||||
ba.textwidget(edit=self.entry_fee_text_or, text=or_text)
|
||||
ba.textwidget(edit=self.entry_fee_text_top,
|
||||
position=(self.button_x + 360,
|
||||
self.button_y + self.button_scale_y - 60),
|
||||
scale=1.3,
|
||||
text=final_fee_str)
|
||||
|
||||
# Possibly show number of ad-plays remaining.
|
||||
ba.textwidget(edit=self.entry_fee_text_remaining,
|
||||
position=(self.button_x + 360,
|
||||
self.button_y + self.button_scale_y - 146),
|
||||
text='' if ad_tries_remaining in [None, 0] else
|
||||
('' + str(ad_tries_remaining)),
|
||||
color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2))
|
||||
else:
|
||||
ba.imagewidget(edit=self.entry_fee_ad_image, opacity=0.0)
|
||||
ba.textwidget(edit=self.entry_fee_text_or, text='')
|
||||
ba.textwidget(edit=self.entry_fee_text_top,
|
||||
position=(self.button_x + 360,
|
||||
self.button_y + self.button_scale_y - 80),
|
||||
scale=1.3,
|
||||
text=final_fee_str)
|
||||
|
||||
# Possibly show number of free-plays remaining.
|
||||
ba.textwidget(
|
||||
edit=self.entry_fee_text_remaining,
|
||||
position=(self.button_x + 360,
|
||||
self.button_y + self.button_scale_y - 100),
|
||||
text=('' if (free_tries_remaining in [None, 0]
|
||||
or final_fee != 0) else
|
||||
('' + str(free_tries_remaining))),
|
||||
color=(0.6, 0.6, 0.6, 1),
|
||||
)
|
||||
7
dist/ba_data/python/bastd/ui/creditslist.py
vendored
7
dist/ba_data/python/bastd/ui/creditslist.py
vendored
|
|
@ -207,10 +207,15 @@ class CreditsListWindow(ba.Window):
|
|||
'\n' + '\n'.join(translation_names.splitlines()[:146]) +
|
||||
'\n'.join(translation_names.splitlines()[146:]) + '\n'
|
||||
'\n'
|
||||
' Shout Out to Awesome Mods / Modders:\n\n'
|
||||
' Shout Out to Awesome Mods / Modders / Contributors:\n\n'
|
||||
' BombDash ModPack\n'
|
||||
' TheMikirog & SoK - BombSquad Joyride Modpack\n'
|
||||
' Mrmaxmeier - BombSquad-Community-Mod-Manager\n'
|
||||
' Ritiek Malhotra \n'
|
||||
' Dliwk\n'
|
||||
' vishal332008\n'
|
||||
' itsre3\n'
|
||||
' Drooopyyy\n'
|
||||
'\n'
|
||||
' Holiday theme vector art designed by Freepik\n'
|
||||
'\n'
|
||||
|
|
|
|||
|
|
@ -705,8 +705,8 @@ class ManualGatherTab(GatherTab):
|
|||
from_other_thread=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
from efro.error import is_udp_network_error
|
||||
if is_udp_network_error(exc):
|
||||
from efro.error import is_udp_communication_error
|
||||
if is_udp_communication_error(exc):
|
||||
ba.pushcall(ba.Call(
|
||||
_safe_set_text, self._checking_state_text,
|
||||
ba.Lstr(resource='gatherWindow.'
|
||||
|
|
|
|||
|
|
@ -219,9 +219,9 @@ class AddrFetchThread(threading.Thread):
|
|||
sock.close()
|
||||
ba.pushcall(ba.Call(self._call, val), from_other_thread=True)
|
||||
except Exception as exc:
|
||||
from efro.error import is_udp_network_error
|
||||
from efro.error import is_udp_communication_error
|
||||
# Ignore expected network errors; log others.
|
||||
if is_udp_network_error(exc):
|
||||
if is_udp_communication_error(exc):
|
||||
pass
|
||||
else:
|
||||
ba.print_exception()
|
||||
|
|
@ -271,8 +271,8 @@ class PingThread(threading.Thread):
|
|||
ping if accessible else None),
|
||||
from_other_thread=True)
|
||||
except Exception as exc:
|
||||
from efro.error import is_udp_network_error
|
||||
if is_udp_network_error(exc):
|
||||
from efro.error import is_udp_communication_error
|
||||
if is_udp_communication_error(exc):
|
||||
pass
|
||||
else:
|
||||
ba.print_exception('Error on gather ping', once=True)
|
||||
|
|
|
|||
14
dist/ba_data/python/bastd/ui/onscreenkeyboard.py
vendored
14
dist/ba_data/python/bastd/ui/onscreenkeyboard.py
vendored
|
|
@ -213,8 +213,8 @@ class OnScreenKeyboardWindow(ba.Window):
|
|||
|
||||
# Show change instructions only if we have more than one
|
||||
# keyboard option.
|
||||
if (ba.app.meta.metascan is not None
|
||||
and len(ba.app.meta.metascan.keyboards) > 1):
|
||||
if (ba.app.meta.scanresults is not None
|
||||
and len(ba.app.meta.scanresults.keyboards) > 1):
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
h_align='center',
|
||||
|
|
@ -238,8 +238,8 @@ class OnScreenKeyboardWindow(ba.Window):
|
|||
self._refresh()
|
||||
|
||||
def _get_keyboard(self) -> ba.Keyboard:
|
||||
assert ba.app.meta.metascan is not None
|
||||
classname = ba.app.meta.metascan.keyboards[self._keyboard_index]
|
||||
assert ba.app.meta.scanresults is not None
|
||||
classname = ba.app.meta.scanresults.keyboards[self._keyboard_index]
|
||||
kbclass = ba.getclass(classname, ba.Keyboard)
|
||||
return kbclass()
|
||||
|
||||
|
|
@ -317,11 +317,11 @@ class OnScreenKeyboardWindow(ba.Window):
|
|||
self._refresh()
|
||||
|
||||
def _next_keyboard(self) -> None:
|
||||
assert ba.app.meta.metascan is not None
|
||||
assert ba.app.meta.scanresults is not None
|
||||
self._keyboard_index = (self._keyboard_index + 1) % len(
|
||||
ba.app.meta.metascan.keyboards)
|
||||
ba.app.meta.scanresults.keyboards)
|
||||
self._load_keyboard()
|
||||
if len(ba.app.meta.metascan.keyboards) < 2:
|
||||
if len(ba.app.meta.scanresults.keyboards) < 2:
|
||||
ba.playsound(ba.getsound('error'))
|
||||
ba.screenmessage(ba.Lstr(resource='keyboardNoOthersAvailableText'),
|
||||
color=(1, 0, 0))
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
scale=(2.06 if uiscale is ba.UIScale.SMALL else
|
||||
1.4 if uiscale is ba.UIScale.MEDIUM else 1.0),
|
||||
stack_offset=(0, -25) if uiscale is ba.UIScale.SMALL else (0, 0)))
|
||||
|
||||
self._prev_lang = ''
|
||||
self._prev_lang_list: list[str] = []
|
||||
self._complete_langs_list: list | None = None
|
||||
|
|
@ -423,29 +424,6 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
v -= self._spacing * 2.1
|
||||
|
||||
this_button_width = 410
|
||||
self._show_user_mods_button = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
|
||||
size=(this_button_width, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource=self._r + '.showUserModsText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=show_user_scripts)
|
||||
if self._show_always_use_internal_keyboard:
|
||||
assert self._always_use_internal_keyboard_check_box is not None
|
||||
ba.widget(edit=self._always_use_internal_keyboard_check_box.widget,
|
||||
down_widget=self._show_user_mods_button)
|
||||
ba.widget(
|
||||
edit=self._show_user_mods_button,
|
||||
up_widget=self._always_use_internal_keyboard_check_box.widget)
|
||||
else:
|
||||
ba.widget(edit=self._show_user_mods_button,
|
||||
up_widget=self._kick_idle_players_check_box.widget)
|
||||
ba.widget(edit=self._kick_idle_players_check_box.widget,
|
||||
down_widget=self._show_user_mods_button)
|
||||
|
||||
v -= self._spacing * 2.0
|
||||
|
||||
self._modding_guide_button = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
|
||||
|
|
@ -454,8 +432,30 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
label=ba.Lstr(resource=self._r + '.moddingGuideText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=ba.Call(
|
||||
ba.open_url,
|
||||
'http://www.froemling.net/docs/bombsquad-modding-guide'))
|
||||
ba.open_url, 'http://ballistica.net/wiki/modding-guide'))
|
||||
if self._show_always_use_internal_keyboard:
|
||||
assert self._always_use_internal_keyboard_check_box is not None
|
||||
ba.widget(edit=self._always_use_internal_keyboard_check_box.widget,
|
||||
down_widget=self._modding_guide_button)
|
||||
ba.widget(
|
||||
edit=self._modding_guide_button,
|
||||
up_widget=self._always_use_internal_keyboard_check_box.widget)
|
||||
else:
|
||||
ba.widget(edit=self._modding_guide_button,
|
||||
up_widget=self._kick_idle_players_check_box.widget)
|
||||
ba.widget(edit=self._kick_idle_players_check_box.widget,
|
||||
down_widget=self._modding_guide_button)
|
||||
|
||||
v -= self._spacing * 2.0
|
||||
|
||||
self._show_user_mods_button = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
|
||||
size=(this_button_width, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource=self._r + '.showUserModsText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=show_user_scripts)
|
||||
|
||||
v -= self._spacing * 2.0
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import weakref
|
|||
from threading import Thread
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.error import CleanError
|
||||
import _ba
|
||||
import ba
|
||||
from bastd.ui.settings.testing import TestingWindow
|
||||
|
|
@ -148,10 +149,12 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None:
|
|||
call()
|
||||
duration = time.monotonic() - starttime
|
||||
_print(f'Succeeded in {duration:.2f}s.', color=(0, 1, 0))
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
duration = time.monotonic() - starttime
|
||||
_print(traceback.format_exc(), color=(1.0, 1.0, 0.3))
|
||||
msg = (str(exc)
|
||||
if isinstance(exc, CleanError) else traceback.format_exc())
|
||||
_print(msg, color=(1.0, 1.0, 0.3))
|
||||
_print(f'Failed in {duration:.2f}s.', color=(1, 0, 0))
|
||||
have_error[0] = True
|
||||
|
||||
|
|
@ -193,6 +196,9 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None:
|
|||
_print(f'\nContacting V2 master-server ({baseaddr})...')
|
||||
_print_test_results(lambda: _test_fetch(baseaddr))
|
||||
|
||||
_print('\nComparing local time to V2 server...')
|
||||
_print_test_results(_test_v2_time)
|
||||
|
||||
# Get V2 nearby zone
|
||||
with ba.app.net.zone_pings_lock:
|
||||
zone_pings = copy.deepcopy(ba.app.net.zone_pings)
|
||||
|
|
@ -206,6 +212,9 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None:
|
|||
_print(f'\nChecking nearest V2 zone ping ({nearstr})...')
|
||||
_print_test_results(lambda: _test_nearby_zone_ping(nearest_zone))
|
||||
|
||||
_print('\nSending V2 cloud message...')
|
||||
_print_test_results(_test_v2_cloud_message)
|
||||
|
||||
if have_error[0]:
|
||||
_print('\nDiagnostics complete. Some diagnostics failed.',
|
||||
color=(10, 0, 0))
|
||||
|
|
@ -271,12 +280,67 @@ def _test_v1_transaction() -> None:
|
|||
raise RuntimeError(results[0])
|
||||
|
||||
|
||||
def _test_v2_cloud_message() -> None:
|
||||
from dataclasses import dataclass
|
||||
import bacommon.cloud
|
||||
|
||||
@dataclass
|
||||
class _Results:
|
||||
errstr: str | None = None
|
||||
send_time: float | None = None
|
||||
response_time: float | None = None
|
||||
|
||||
results = _Results()
|
||||
|
||||
def _cb(response: bacommon.cloud.PingResponse | Exception) -> None:
|
||||
# Note: this runs in another thread so need to avoid exceptions.
|
||||
results.response_time = time.monotonic()
|
||||
if isinstance(response, Exception):
|
||||
results.errstr = str(response)
|
||||
if not isinstance(response, bacommon.cloud.PingResponse):
|
||||
results.errstr = f'invalid response type: {type(response)}.'
|
||||
|
||||
def _send() -> None:
|
||||
# Note: this runs in another thread so need to avoid exceptions.
|
||||
results.send_time = time.monotonic()
|
||||
ba.app.cloud.send_message_cb(bacommon.cloud.PingMessage(), _cb)
|
||||
|
||||
# This stuff expects to be run from the logic thread.
|
||||
ba.pushcall(_send, from_other_thread=True)
|
||||
|
||||
wait_start_time = time.monotonic()
|
||||
while True:
|
||||
if results.response_time is not None:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
if time.monotonic() - wait_start_time > 10.0:
|
||||
raise RuntimeError('Timeout waiting for cloud message response')
|
||||
if results.errstr is not None:
|
||||
raise RuntimeError(results.errstr)
|
||||
|
||||
|
||||
def _test_v2_time() -> None:
|
||||
offset = ba.app.net.server_time_offset_hours
|
||||
if offset is None:
|
||||
raise RuntimeError('no time offset found;'
|
||||
' perhaps unable to communicate with v2 server?')
|
||||
if abs(offset) >= 2.0:
|
||||
raise CleanError(
|
||||
f'Your device time is off from world time by {offset:.1f} hours.\n'
|
||||
'This may cause network operations to fail due to your device\n'
|
||||
' incorrectly treating SSL certificates as not-yet-valid, etc.\n'
|
||||
'Check your device time and time-zone settings to fix this.\n')
|
||||
|
||||
|
||||
def _test_fetch(baseaddr: str) -> None:
|
||||
# pylint: disable=consider-using-with
|
||||
import urllib.request
|
||||
response = urllib.request.urlopen(urllib.request.Request(
|
||||
f'{baseaddr}/ping', None, {'User-Agent': _ba.app.user_agent_string}),
|
||||
timeout=10.0)
|
||||
response = urllib.request.urlopen(
|
||||
urllib.request.Request(f'{baseaddr}/ping', None,
|
||||
{'User-Agent': _ba.app.user_agent_string}),
|
||||
context=ba.app.net.sslcontext,
|
||||
timeout=10.0,
|
||||
)
|
||||
if response.getcode() != 200:
|
||||
raise RuntimeError(
|
||||
f'Got unexpected response code {response.getcode()}.')
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class PluginSettingsWindow(ba.Window):
|
|||
self._subcontainer = ba.columnwidget(parent=self._scrollwidget,
|
||||
selection_loops_to_parent=True)
|
||||
|
||||
if ba.app.meta.metascan is None:
|
||||
if ba.app.meta.scanresults is None:
|
||||
ba.screenmessage('Still scanning plugins; please try again.',
|
||||
color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window):
|
|||
from ba.osmusic import OSMusicPlayer
|
||||
from bastd.ui.fileselector import FileSelectorWindow
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
base_path = _ba.android_get_external_storage_path()
|
||||
base_path = _ba.android_get_external_files_dir()
|
||||
ba.app.ui.set_main_menu_window(
|
||||
FileSelectorWindow(
|
||||
base_path,
|
||||
|
|
@ -173,7 +173,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window):
|
|||
def _on_music_folder_press(self) -> None:
|
||||
from bastd.ui.fileselector import FileSelectorWindow
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
base_path = _ba.android_get_external_storage_path()
|
||||
base_path = _ba.android_get_external_files_dir()
|
||||
ba.app.ui.set_main_menu_window(
|
||||
FileSelectorWindow(base_path,
|
||||
callback=self._music_folder_selector_cb,
|
||||
|
|
|
|||
|
|
@ -182,8 +182,9 @@ class TournamentEntryWindow(popup.PopupWindow):
|
|||
h_align='center',
|
||||
v_align='center',
|
||||
scale=0.6,
|
||||
text=ba.Lstr(resource='watchAVideoText',
|
||||
fallback_resource='watchAnAdText'),
|
||||
# Note: AdMob now requires rewarded ad usage
|
||||
# specifically says 'Ad' in it.
|
||||
text=ba.Lstr(resource='watchAnAdText'),
|
||||
maxwidth=95,
|
||||
color=(0, 1, 0))
|
||||
ad_plays_remaining_text = (
|
||||
|
|
|
|||
10
dist/ba_data/python/bastd/ui/trophies.py
vendored
10
dist/ba_data/python/bastd/ui/trophies.py
vendored
|
|
@ -112,7 +112,7 @@ class TrophiesWindow(popup.PopupWindow):
|
|||
sub_width: int,
|
||||
trophy_types: list[list[str]]) -> int:
|
||||
from ba.internal import get_trophy_string
|
||||
pts = 0
|
||||
total_pts = 0
|
||||
for i, trophy_type in enumerate(trophy_types):
|
||||
t_count = self._data['t' + trophy_type[0]]
|
||||
t_mult = self._data['t' + trophy_type[0] + 'm']
|
||||
|
|
@ -157,7 +157,7 @@ class TrophiesWindow(popup.PopupWindow):
|
|||
h_align='center',
|
||||
v_align='center')
|
||||
|
||||
pts = t_count * t_mult
|
||||
this_pts = t_count * t_mult
|
||||
ba.textwidget(parent=self._subcontainer,
|
||||
position=(sub_width * 0.88,
|
||||
sub_height - 20 - incr * i),
|
||||
|
|
@ -167,12 +167,12 @@ class TrophiesWindow(popup.PopupWindow):
|
|||
flatness=1.0,
|
||||
shadow=0.0,
|
||||
scale=0.5,
|
||||
text=eq_text.replace('${NUMBER}', str(pts)),
|
||||
text=eq_text.replace('${NUMBER}', str(this_pts)),
|
||||
size=(0, 0),
|
||||
h_align='center',
|
||||
v_align='center')
|
||||
pts += pts
|
||||
return pts
|
||||
total_pts += this_pts
|
||||
return total_pts
|
||||
|
||||
def _on_cancel_press(self) -> None:
|
||||
self._transition_out()
|
||||
|
|
|
|||
64
dist/ba_data/python/efro/error.py
vendored
64
dist/ba_data/python/efro/error.py
vendored
|
|
@ -4,6 +4,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import errno
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
|
@ -39,12 +40,13 @@ class CommunicationError(Exception):
|
|||
"""A communication related error has occurred.
|
||||
|
||||
This covers anything network-related going wrong in the sending
|
||||
of data or receiving of a response. This error does not imply
|
||||
of data or receiving of a response. Basically anything that is out
|
||||
of our control should get lumped in here. This error does not imply
|
||||
that data was not received on the other end; only that a full
|
||||
acknowledgement round trip was not completed.
|
||||
|
||||
These errors should be gracefully handled whenever possible, as
|
||||
occasional network outages are generally unavoidable.
|
||||
occasional network issues are unavoidable.
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -55,9 +57,9 @@ class RemoteError(Exception):
|
|||
occurs remotely. The error string can consist of a remote stack
|
||||
trace or a simple message depending on the context.
|
||||
|
||||
Communication systems should raise more specific error types when
|
||||
more introspection/control is needed; this is intended somewhat as
|
||||
a catch-all.
|
||||
Communication systems should raise more specific error types locally
|
||||
when more introspection/control is needed; this is intended somewhat
|
||||
as a catch-all.
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
@ -69,31 +71,43 @@ class IntegrityError(ValueError):
|
|||
"""Data has been tampered with or corrupted in some form."""
|
||||
|
||||
|
||||
def is_urllib_network_error(exc: BaseException) -> bool:
|
||||
"""Is the provided exception from urllib a network-related error?
|
||||
def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
|
||||
"""Is the provided exception from urllib a communication-related error?
|
||||
|
||||
Url, if provided can provide extra context for when to treat an error
|
||||
as such an error.
|
||||
|
||||
This should be passed an exception which resulted from opening or
|
||||
reading a urllib Request. It returns True for any errors that could
|
||||
conceivably arise due to unavailable/poor network connections,
|
||||
firewall/connectivity issues, etc. These issues can often be safely
|
||||
ignored or presented to the user as general 'network-unavailable'
|
||||
states.
|
||||
firewall/connectivity issues, or other issues out of our control.
|
||||
These errors can often be safely ignored or presented to the user
|
||||
as general 'network-unavailable' states.
|
||||
"""
|
||||
import urllib.error
|
||||
import http.client
|
||||
import errno
|
||||
import socket
|
||||
if isinstance(
|
||||
exc,
|
||||
(urllib.error.URLError, ConnectionError, http.client.IncompleteRead,
|
||||
http.client.BadStatusLine, socket.timeout)):
|
||||
if isinstance(exc, (urllib.error.URLError, ConnectionError,
|
||||
http.client.IncompleteRead, http.client.BadStatusLine,
|
||||
http.client.RemoteDisconnected, socket.timeout)):
|
||||
|
||||
# Special case: although an HTTPError is a subclass of URLError,
|
||||
# we don't return True for it. It means we have successfully
|
||||
# communicated with the server but what we are asking for is
|
||||
# not there/etc.
|
||||
# we don't consider it a communication error. It generally means we
|
||||
# have successfully communicated with the server but what we are asking
|
||||
# for is not there/etc.
|
||||
if isinstance(exc, urllib.error.HTTPError):
|
||||
|
||||
# Special sub-case: appspot.com hosting seems to give 403 errors
|
||||
# (forbidden) to some countries. I'm assuming for legal reasons?..
|
||||
# Let's consider that a communication error since its out of our
|
||||
# control so we don't fill up logs with it.
|
||||
if exc.code == 403 and url is not None and '.appspot.com' in url:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if isinstance(exc, OSError):
|
||||
if exc.errno == 10051: # Windows unreachable network error.
|
||||
return True
|
||||
|
|
@ -106,18 +120,17 @@ def is_urllib_network_error(exc: BaseException) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def is_udp_network_error(exc: BaseException) -> bool:
|
||||
"""Is the provided exception a network-related error?
|
||||
def is_udp_communication_error(exc: BaseException) -> bool:
|
||||
"""Should this udp-related exception be considered a communication error?
|
||||
|
||||
This should be passed an exception which resulted from creating and
|
||||
using a socket.SOCK_DGRAM type socket. It should return True for any
|
||||
errors that could conceivably arise due to unavailable/poor network
|
||||
connections, firewall/connectivity issues, etc. These issues can often
|
||||
conditions, firewall/connectivity issues, etc. These issues can often
|
||||
be safely ignored or presented to the user as general
|
||||
'network-unavailable' states.
|
||||
"""
|
||||
import errno
|
||||
if isinstance(exc, ConnectionRefusedError):
|
||||
if isinstance(exc, ConnectionRefusedError | TimeoutError):
|
||||
return True
|
||||
if isinstance(exc, OSError):
|
||||
if exc.errno == 10051: # Windows unreachable network error.
|
||||
|
|
@ -140,8 +153,8 @@ def is_udp_network_error(exc: BaseException) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def is_asyncio_streams_network_error(exc: BaseException) -> bool:
|
||||
"""Is the provided exception a network-related error?
|
||||
def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
|
||||
"""Should this streams error be considered a communication error?
|
||||
|
||||
This should be passed an exception which resulted from creating and
|
||||
using asyncio streams. It should return True for any errors that could
|
||||
|
|
@ -149,7 +162,6 @@ def is_asyncio_streams_network_error(exc: BaseException) -> bool:
|
|||
firewall/connectivity issues, etc. These issues can often be safely
|
||||
ignored or presented to the user as general 'connection-lost' events.
|
||||
"""
|
||||
import errno
|
||||
import ssl
|
||||
|
||||
if isinstance(exc, (
|
||||
|
|
|
|||
1
dist/ba_data/python/efro/message/_message.py
vendored
1
dist/ba_data/python/efro/message/_message.py
vendored
|
|
@ -56,6 +56,7 @@ class ErrorResponse(Response):
|
|||
OTHER = 0
|
||||
CLEAN = 1
|
||||
LOCAL = 2
|
||||
COMMUNICATION = 3
|
||||
|
||||
error_message: Annotated[str, IOAttrs('m')]
|
||||
error_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.OTHER
|
||||
|
|
|
|||
49
dist/ba_data/python/efro/message/_protocol.py
vendored
49
dist/ba_data/python/efro/message/_protocol.py
vendored
|
|
@ -25,28 +25,40 @@ class MessageProtocol:
|
|||
"""Wrangles a set of message types, formats, and response types.
|
||||
Both endpoints must be using a compatible Protocol for communication
|
||||
to succeed. To maintain Protocol compatibility between revisions,
|
||||
all message types must retain the same id, message attr storage names must
|
||||
not change, newly added attrs must have default values, etc.
|
||||
all message types must retain the same id, message attr storage
|
||||
names must not change, newly added attrs must have default values,
|
||||
etc.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
message_types: dict[int, type[Message]],
|
||||
response_types: dict[int, type[Response]],
|
||||
preserve_clean_errors: bool = True,
|
||||
log_remote_exceptions: bool = True,
|
||||
trusted_sender: bool = False) -> None:
|
||||
receiver_logs_exceptions: bool = True,
|
||||
receiver_returns_stack_traces: bool = False) -> None:
|
||||
"""Create a protocol with a given configuration.
|
||||
|
||||
Note that common response types are automatically registered
|
||||
with (unchanging negative ids) so they don't need to be passed
|
||||
explicitly (but can be if a different id is desired).
|
||||
|
||||
If 'preserve_clean_errors' is True, efro.error.CleanError errors
|
||||
on the remote end will result in the same error raised locally.
|
||||
All other Exception types come across as efro.error.RemoteError.
|
||||
If 'preserve_clean_errors' is True, efro.error.CleanError
|
||||
exceptions raised on the receiver end will result in a matching
|
||||
CleanError raised back on the sender. All other Exception types
|
||||
come across as efro.error.RemoteError.
|
||||
|
||||
If 'trusted_sender' is True, stringified remote stack traces will
|
||||
be included in the responses if errors occur.
|
||||
When 'receiver_logs_exceptions' is True, any uncaught Exceptions
|
||||
on the receiver end will be logged there via logging.exception()
|
||||
(in addition to the usual behavior of returning an ErrorResponse
|
||||
to the sender). This is good to leave enabled if your
|
||||
intention is to never return ErrorResponses. Looser setups
|
||||
making routine use of CleanErrors or whatnot may want to
|
||||
disable this, however.
|
||||
|
||||
If 'receiver_returns_stack_traces' is True, stringified stack
|
||||
traces will be returned to the sender for exceptions occurring
|
||||
on the receiver end. This can make debugging easier but should
|
||||
only be used when the client is trusted to see such info.
|
||||
"""
|
||||
self.message_types_by_id: dict[int, type[Message]] = {}
|
||||
self.message_ids_by_type: dict[type[Message], int] = {}
|
||||
|
|
@ -102,9 +114,9 @@ class MessageProtocol:
|
|||
assert is_ioprepped_dataclass(cls)
|
||||
assert issubclass(cls, Response)
|
||||
if cls not in self.response_ids_by_type:
|
||||
raise ValueError(f'Possible response type {cls}'
|
||||
f' needs to be included in response_types'
|
||||
f' for this protocol.')
|
||||
raise ValueError(
|
||||
f'Possible response type {cls} needs to be included'
|
||||
f' in response_types for this protocol.')
|
||||
|
||||
# Make sure all registered types have unique base names.
|
||||
# We can take advantage of this to generate cleaner looking
|
||||
|
|
@ -116,8 +128,8 @@ class MessageProtocol:
|
|||
' all types are required to have unique names.')
|
||||
|
||||
self.preserve_clean_errors = preserve_clean_errors
|
||||
self.log_remote_exceptions = log_remote_exceptions
|
||||
self.trusted_sender = trusted_sender
|
||||
self.receiver_logs_exceptions = receiver_logs_exceptions
|
||||
self.receiver_returns_stack_traces = receiver_returns_stack_traces
|
||||
|
||||
@staticmethod
|
||||
def encode_dict(obj: dict) -> str:
|
||||
|
|
@ -134,7 +146,9 @@ class MessageProtocol:
|
|||
|
||||
def error_to_response(self, exc: Exception) -> Response:
|
||||
"""Translate an error to a response."""
|
||||
if self.log_remote_exceptions:
|
||||
|
||||
# Log any errors we got during handling if so desired.
|
||||
if self.receiver_logs_exceptions:
|
||||
logging.exception('Error handling message.')
|
||||
|
||||
# If anything goes wrong, return a ErrorResponse instead.
|
||||
|
|
@ -142,8 +156,9 @@ class MessageProtocol:
|
|||
return ErrorResponse(error_message=str(exc),
|
||||
error_type=ErrorResponse.ErrorType.CLEAN)
|
||||
return ErrorResponse(
|
||||
error_message=(traceback.format_exc() if self.trusted_sender else
|
||||
'An unknown error has occurred.'),
|
||||
error_message=(traceback.format_exc()
|
||||
if self.receiver_returns_stack_traces else
|
||||
'An internal error has occurred.'),
|
||||
error_type=ErrorResponse.ErrorType.OTHER)
|
||||
|
||||
def _to_dict(self, message: Any, ids_by_type: dict[type, int],
|
||||
|
|
|
|||
52
dist/ba_data/python/efro/message/_sender.py
vendored
52
dist/ba_data/python/efro/message/_sender.py
vendored
|
|
@ -9,7 +9,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from efro.error import CleanError, RemoteError
|
||||
from efro.error import CleanError, RemoteError, CommunicationError
|
||||
from efro.message._message import EmptyResponse, ErrorResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -35,7 +35,7 @@ class MessageSender:
|
|||
def send_raw_message(self, message: str) -> str:
|
||||
# Actually send the message here.
|
||||
|
||||
# MyMessageSender class should provide overloads for send(), send_bg(),
|
||||
# MyMessageSender class should provide overloads for send(), send_async(),
|
||||
# etc. to ensure all sending happens with valid types.
|
||||
obj = MyClass()
|
||||
obj.msg.send(SomeMessageType())
|
||||
|
|
@ -54,7 +54,12 @@ class MessageSender:
|
|||
def send_method(
|
||||
self, call: Callable[[Any, str],
|
||||
str]) -> Callable[[Any, str], str]:
|
||||
"""Function decorator for setting raw send method."""
|
||||
"""Function decorator for setting raw send method.
|
||||
|
||||
Send methods take strings and should return strings.
|
||||
Any Exception raised during the send_method manifests as
|
||||
a CommunicationError for the message sender.
|
||||
"""
|
||||
assert self._send_raw_message_call is None
|
||||
self._send_raw_message_call = call
|
||||
return call
|
||||
|
|
@ -62,7 +67,12 @@ class MessageSender:
|
|||
def send_async_method(
|
||||
self, call: Callable[[Any, str], Awaitable[str]]
|
||||
) -> Callable[[Any, str], Awaitable[str]]:
|
||||
"""Function decorator for setting raw send-async method."""
|
||||
"""Function decorator for setting raw send-async method.
|
||||
|
||||
Send methods take strings and should return strings.
|
||||
Any Exception raised during the send_method manifests as
|
||||
a CommunicationError for the message sender.
|
||||
"""
|
||||
assert self._send_async_raw_message_call is None
|
||||
self._send_async_raw_message_call = call
|
||||
return call
|
||||
|
|
@ -123,7 +133,17 @@ class MessageSender:
|
|||
raise RuntimeError('send() is unimplemented for this type.')
|
||||
|
||||
msg_encoded = self._encode_message(bound_obj, message)
|
||||
response_encoded = self._send_raw_message_call(bound_obj, msg_encoded)
|
||||
try:
|
||||
response_encoded = self._send_raw_message_call(
|
||||
bound_obj, msg_encoded)
|
||||
except Exception as exc:
|
||||
# Any error in the raw send call gets recorded as either
|
||||
# a local or communication error.
|
||||
return ErrorResponse(
|
||||
error_message=f'Error in send async method: {exc}',
|
||||
error_type=(ErrorResponse.ErrorType.COMMUNICATION
|
||||
if isinstance(exc, CommunicationError) else
|
||||
ErrorResponse.ErrorType.LOCAL))
|
||||
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||
|
||||
async def send_split_part_1_async(self, bound_obj: Any,
|
||||
|
|
@ -139,9 +159,17 @@ class MessageSender:
|
|||
raise RuntimeError('send_async() is unimplemented for this type.')
|
||||
|
||||
msg_encoded = self._encode_message(bound_obj, message)
|
||||
response_encoded = await self._send_async_raw_message_call(
|
||||
bound_obj, msg_encoded)
|
||||
|
||||
try:
|
||||
response_encoded = await self._send_async_raw_message_call(
|
||||
bound_obj, msg_encoded)
|
||||
except Exception as exc:
|
||||
# Any error in the raw send call gets recorded as either
|
||||
# a local or communication error.
|
||||
return ErrorResponse(
|
||||
error_message=f'Error in send async method: {exc}',
|
||||
error_type=(ErrorResponse.ErrorType.COMMUNICATION
|
||||
if isinstance(exc, CommunicationError) else
|
||||
ErrorResponse.ErrorType.LOCAL))
|
||||
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||
|
||||
def send_split_part_2(self, message: Message,
|
||||
|
|
@ -183,8 +211,8 @@ class MessageSender:
|
|||
# If we got to this point, we successfully communicated
|
||||
# with the other end so errors represent protocol mismatches
|
||||
# or other invalid data. For now let's just log it but perhaps
|
||||
# we'd want to somehow embed it in the ErrorResponse to be raised
|
||||
# directly to the user later.
|
||||
# we'd want to somehow embed it in the ErrorResponse to be
|
||||
# available directly to the user later.
|
||||
logging.exception('Error decoding raw response')
|
||||
response = ErrorResponse(
|
||||
error_message=
|
||||
|
|
@ -208,6 +236,10 @@ class MessageSender:
|
|||
# Some error occurred. Raise a local Exception for it.
|
||||
if isinstance(raw_response, ErrorResponse):
|
||||
|
||||
if (raw_response.error_type is
|
||||
ErrorResponse.ErrorType.COMMUNICATION):
|
||||
raise CommunicationError(raw_response.error_message)
|
||||
|
||||
# If something went wrong on our end of the connection,
|
||||
# don't say it was a remote error.
|
||||
if raw_response.error_type is ErrorResponse.ErrorType.LOCAL:
|
||||
|
|
|
|||
5
dist/ba_data/python/efro/rpc.py
vendored
5
dist/ba_data/python/efro/rpc.py
vendored
|
|
@ -13,8 +13,9 @@ from dataclasses import dataclass
|
|||
from threading import current_thread
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
from efro.error import CommunicationError, is_asyncio_streams_network_error
|
||||
from efro.util import assert_never
|
||||
from efro.error import (CommunicationError,
|
||||
is_asyncio_streams_communication_error)
|
||||
from efro.dataclassio import (dataclass_to_json, dataclass_from_json,
|
||||
ioprepped, IOAttrs)
|
||||
|
||||
|
|
@ -596,7 +597,7 @@ class RPCEndpoint:
|
|||
if isinstance(exc, _KeepaliveTimeoutError):
|
||||
return True
|
||||
|
||||
return is_asyncio_streams_network_error(exc)
|
||||
return is_asyncio_streams_communication_error(exc)
|
||||
|
||||
def _check_env(self) -> None:
|
||||
# I was seeing that asyncio stuff wasn't working as expected if
|
||||
|
|
|
|||
4
dist/ba_data/python/efro/util.py
vendored
4
dist/ba_data/python/efro/util.py
vendored
|
|
@ -637,9 +637,11 @@ def compact_id(num: int) -> str:
|
|||
'abcdefghijklmnopqrstuvwxyz')
|
||||
|
||||
|
||||
# NOTE: Even though this is available as part of typing_extensions, keeping
|
||||
# it in here for now so we don't require typing_extensions as a dependency.
|
||||
# Once 3.11 rolls around we can kill this and use typing.assert_never.
|
||||
def assert_never(value: NoReturn) -> NoReturn:
|
||||
"""Trick for checking exhaustive handling of Enums, etc.
|
||||
|
||||
See https://github.com/python/typing/issues/735
|
||||
"""
|
||||
assert False, f'Unhandled value: {value} ({type(value).__name__})'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue