diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index fcb5af3b..5a433830 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -1021,6 +1021,7 @@
nntplib
nodepos
nodpi
+ nofiles
noinspect
noninfringement
nonmultipart
diff --git a/Makefile b/Makefile
index 80509e66..46696f7f 100644
--- a/Makefile
+++ b/Makefile
@@ -41,9 +41,9 @@ help:
# Prerequisites that should be in place before running most any other build;
# things like tool config files, etc.
-PREREQS = .dir-locals.el .mypy.ini .pycheckers \
- .pylintrc .style.yapf .clang-format \
- .projectile .editorconfig .cache/checkenv
+PREREQS = .cache/checkenv .dir-locals.el \
+ .mypy.ini .pycheckers .pylintrc .style.yapf .clang-format \
+ .projectile .editorconfig
prereqs: ${PREREQS}
@@ -264,43 +264,43 @@ formatmakefile: prereqs
# These are useful, but can take significantly longer and/or be a bit flaky.
check: updatecheck
- @$(MAKE) -j3 cpplintcode pylintscripts mypyscripts
+ @$(MAKE) -j3 cpplint pylint mypy
@echo ALL CHECKS PASSED!
check2: updatecheck
- @$(MAKE) -j4 cpplintcode pylintscripts mypyscripts pycharmscripts
+ @$(MAKE) -j4 cpplint pylint mypy pycharmscripts
@echo ALL CHECKS PASSED!
checkfast: updatecheck
- @$(MAKE) -j3 cpplintcode pylintscriptsfast mypyscripts
+ @$(MAKE) -j3 cpplint pylintfast mypy
@echo ALL CHECKS PASSED!
checkfast2: updatecheck
- @$(MAKE) -j4 cpplintcode pylintscriptsfast mypyscripts pycharmscripts
+ @$(MAKE) -j4 cpplint pylintfast mypy pycharmscripts
@echo ALL CHECKS PASSED!
checkfull: updatecheck
- @$(MAKE) -j3 cpplintcodefull pylintscriptsfull mypyscriptsfull
+ @$(MAKE) -j3 cpplintfull pylintfull mypyfull
@echo ALL CHECKS PASSED!
checkfull2: updatecheck
- @$(MAKE) -j4 cpplintcodefull pylintscriptsfull mypyscriptsfull pycharmscriptsfull
+ @$(MAKE) -j4 cpplintfull pylintfull mypyfull pycharmscriptsfull
@echo ALL CHECKS PASSED!
-cpplintcode: prereqs
- @tools/snippets cpplintcode
+cpplint: prereqs
+ @tools/snippets cpplint
-cpplintcodefull: prereqs
- @tools/snippets cpplintcode -full
+cpplintfull: prereqs
+ @tools/snippets cpplint -full
-pylintscripts: prereqs
- @tools/snippets pylintscripts
+pylint: prereqs
+ @tools/snippets pylint
-pylintscriptsfull: prereqs
- @tools/snippets pylintscripts -full
+pylintfull: prereqs
+ @tools/snippets pylint -full
-mypyscripts: prereqs
- @tools/snippets mypyscripts
+mypy: prereqs
+ @tools/snippets mypy
-mypyscriptsfull: prereqs
- @tools/snippets mypyscripts -full
+mypyfull: prereqs
+ @tools/snippets mypy -full
pycharmscripts: prereqs
@tools/snippets pycharmscripts
@@ -311,14 +311,14 @@ pycharmscriptsfull: prereqs
# 'Fast' script checking using dependency recursion limits.
# This can require much less re-checking but may miss problems in rare cases.
# Its not a bad idea to run a non-fast check every so often or before pushing.
-pylintscriptsfast: prereqs
- @tools/snippets pylintscripts -fast
+pylintfast: prereqs
+ @tools/snippets pylint -fast
# Tell make which of these targets don't represent files.
.PHONY: format formatfull formatcode formatcodefull formatscripts \
formatscriptsfull check check2 checkfast checkfast2 checkfull checkfull2 \
- cpplintcode cpplintcodefull pylintscripts pylintscriptsfull mypyscripts \
- mypyscriptsfull pycharmscripts pycharmscriptsfull
+ cpplint cpplintfull pylint pylintfull mypy \
+ mypyfull pycharmscripts pycharmscriptsfull
################################################################################
@@ -338,24 +338,24 @@ updatecheck: prereqs
# Run an update and check together; handy while iterating.
# (slightly more efficient than running update/check separately).
updatethencheck: update
- @$(MAKE) -j3 cpplintcode pylintscripts mypyscripts
+ @$(MAKE) -j3 cpplint pylint mypy
@echo ALL CHECKS PASSED!
updatethencheck2: update
- @$(MAKE) -j4 cpplintcode pylintscripts mypyscripts pycharmscripts
+ @$(MAKE) -j4 cpplint pylint mypy pycharmscripts
@echo ALL CHECKS PASSED!
updatethencheckfast: update
- @$(MAKE) -j3 cpplintcode pylintscriptsfast mypyscripts
+ @$(MAKE) -j3 cpplint pylintfast mypy
@echo ALL CHECKS PASSED!
updatethencheckfast2: update
- @$(MAKE) -j4 cpplintcode pylintscriptsfast mypyscripts pycharmscripts
+ @$(MAKE) -j4 cpplint pylintfast mypy pycharmscripts
@echo ALL CHECKS PASSED!
updatethencheckfull: update
- @$(MAKE) -j3 cpplintcodefull pylintscriptsfull mypyscriptsfull
+ @$(MAKE) -j3 cpplintfull pylintfull mypyfull
@echo ALL CHECKS PASSED!
updatethencheckfull2: update
- @$(MAKE) -j4 cpplintcodefull pylintscriptsfull mypyscriptsfull pycharmscriptsfull
+ @$(MAKE) -j4 cpplintfull pylintfull mypyfull pycharmscriptsfull
@echo ALL CHECKS PASSED!
# Run a format, an update, and then a check.
diff --git a/README.md b/README.md
index e883627b..84a7130b 100644
--- a/README.md
+++ b/README.md
@@ -23,11 +23,11 @@ The Ballistica project is the foundation for the next generation of [BombSquad](
* Migrate modding documentation to this repo's wiki, allowing other modders to add their own bits of wisdom
### Frequently Asked Questions
-* **Q: What's with the new name? Is BombSquad getting renamed?**
+* **Q: What's with this new name? Is BombSquad getting renamed?**
* A: No, BombSquad is still BombSquad. 'Ballistica' is simply the new name for the engine/app-framework. This way it can be used for other game/app projects without causing confusion (though that is mostly theoretical at this point). As a modder, the biggest change you will notice is a lot of 'ba' prefixes in the APIs as opposed to 'bs'. You may also see the word 'BallisticaCore' show up various places, which in actual releases gets replaced by 'BombSquad'.
* **Q: Does this mean BombSquad is open source?**
-* A: Yes and no. All code contained in this repo is MIT licensed and free for use anywhere. This includes game scripts, pipeline tools, etc. In the future I hope to expand this to include at least some binary sources. Anything not included here, however, even if automatically downloaded by build scripts, is still copyrighted and cannot be redistributed without explicit consent. This includes assets and game binaries. So in a nutshell: create and share mods to your heart's content, but please don't distribute your own complete copy of the game. Please email support@froemling.net if you have any questions.
+* A: Yes and no. All code contained in this repo is MIT licensed and free for use anywhere. This includes game scripts, pipeline tools, etc. In the future I hope to expand this to include at least some binary sources. Anything not included here, however, even if automatically downloaded by build scripts, is still proprietary and cannot be redistributed without explicit consent. This includes assets and game binaries. So in a nutshell: create and share mods to your heart's content, but please don't distribute your own complete copy of the game. Please email support@froemling.net if you have any questions.
* **Q: Will my existing BombSquad 1.4.x mods still work?**
* A: No. All mods will need to be explicitly updated to work with the new ballistica apis in 1.5+. This may or may not be a significant amount of work depending on the mod. I would highly suggest tinkering around with some of the new features in 1.5 such as type-safe Python and dynamic assets before attempting to port any old mods, as some things are done significantly differently now. You may also want to consider simply sticking with 1.4 builds for a while longer, especially for server duties, since they will remain fully compatible with clients running 1.5. The new ballistica apis may still be in significant flux for at least a while until the dust settles, but they will be worth switching to eventually, I promise!
diff --git a/assets/src/data/scripts/_ba.py b/assets/src/data/scripts/_ba.py
index 5413ab2d..8770f7c0 100644
--- a/assets/src/data/scripts/_ba.py
+++ b/assets/src/data/scripts/_ba.py
@@ -34,7 +34,7 @@ NOTE: This file was autogenerated by gendummymodule; do not edit by hand.
"""
# (hash we can use to see if this file is out of date)
-# SOURCES_HASH=171913046369526584263358640346000279030
+# SOURCES_HASH=120456495529909322688635129785556212748
# I'm sorry Pylint. I know this file saddens you. Be strong.
# pylint: disable=useless-suppression
@@ -3483,10 +3483,20 @@ def set_ui_input_device(input_device: Optional[ba.InputDevice]) -> None:
def show_ad(purpose: str,
- on_completion_call: Callable[[], None] = None,
- pass_actually_showed: bool = False) -> None:
- """show_ad(purpose: str, on_completion_call: Callable[[], None] = None,
- pass_actually_showed: bool = False) -> None
+ on_completion_call: Callable[[], None] = None) -> None:
+ """show_ad(purpose: str, on_completion_call: Callable[[], None] = None)
+ -> None
+
+ (internal)
+ """
+ return None
+
+
+def show_ad_2(purpose: str,
+ on_completion_call: Callable[[bool], None] = None) -> None:
+ """show_ad_2(purpose: str,
+ on_completion_call: Callable[[bool], None] = None)
+ -> None
(internal)
"""
diff --git a/assets/src/data/scripts/ba/_achievement.py b/assets/src/data/scripts/ba/_achievement.py
index bb39fff9..6f79b645 100644
--- a/assets/src/data/scripts/ba/_achievement.py
+++ b/assets/src/data/scripts/ba/_achievement.py
@@ -667,7 +667,7 @@ class Achievement:
from ba import _gameutils
from bastd.actor.text import Text
from bastd.actor.image import Image
- from ba._general import Call, WeakCall
+ from ba._general import WeakCall
from ba._lang import Lstr
from ba._messages import DieMessage
from ba._enums import TimeType, SpecialChar
@@ -692,7 +692,8 @@ class Achievement:
_ba.playsound(_ba.getsound('achievement'), host_only=True)
else:
_ba.timer(
- 0.5, Call(_ba.playsound, _ba.getsound('ding'), host_only=True))
+ 0.5,
+ lambda: _ba.playsound(_ba.getsound('ding'), host_only=True))
in_time = 0.300
out_time = 3.5
diff --git a/assets/src/data/scripts/ba/_app.py b/assets/src/data/scripts/ba/_app.py
index 442c1ee3..420462c6 100644
--- a/assets/src/data/scripts/ba/_app.py
+++ b/assets/src/data/scripts/ba/_app.py
@@ -247,27 +247,9 @@ class App:
import urllib.request
try:
val = urllib.request.urlopen('https://example.com').read()
- print("HTTPS SUCCESS", len(val))
+ print("HTTPS TEST SUCCESS", len(val))
except Exception as exc:
- print("GOT EXC", exc)
-
- try:
- import sqlite3
- print("GOT SQLITE", sqlite3)
- except Exception as exc:
- print("EXC IMPORTING SQLITE", exc)
-
- try:
- import csv
- print("GOT CSV", csv)
- except Exception as exc:
- print("EXC IMPORTING CSV", exc)
-
- try:
- import lzma
- print("GOT LZMA", lzma)
- except Exception as exc:
- print("EXC IMPORTING LZMA", exc)
+ print("HTTPS TEST FAIL:", exc)
# Config.
self.config_file_healthy = False
@@ -436,7 +418,7 @@ class App:
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from ba import _apputils
- from ba._general import Call
+ # from ba._general import Call
from ba import _appconfig
from ba import ui as bsui
from ba import _achievement
@@ -493,31 +475,23 @@ class App:
self.small_ui = True
self.med_ui = False
with _ba.Context('ui'):
- _ba.pushcall(
- Call(_ba.screenmessage,
- 'FORCING SMALL UI FOR TESTING',
- color=(1, 0, 1),
- log=True))
+ _ba.pushcall(lambda: _ba.screenmessage(
+ 'FORCING SMALL UI FOR TESTING', color=(1, 0, 1), log=True))
# noinspection PyUnreachableCode
if 0: # force-test medium UI
self.small_ui = False
self.med_ui = True
with _ba.Context('ui'):
- _ba.pushcall(
- Call(_ba.screenmessage,
- 'FORCING MEDIUM UI FOR TESTING',
- color=(1, 0, 1),
- log=True))
+ _ba.pushcall(lambda: _ba.screenmessage(
+ 'FORCING MEDIUM UI FOR TESTING', color=(1, 0, 1
+ ), log=True))
# noinspection PyUnreachableCode
if 0: # force-test large UI
self.small_ui = False
self.med_ui = False
with _ba.Context('ui'):
- _ba.pushcall(
- Call(_ba.screenmessage,
- 'FORCING LARGE UI FOR TESTING',
- color=(1, 0, 1),
- log=True))
+ _ba.pushcall(lambda: _ba.screenmessage(
+ 'FORCING LARGE UI FOR TESTING', color=(1, 0, 1), log=True))
# pylint: enable=using-constant-test
# If there's a leftover log file, attempt to upload
@@ -572,16 +546,17 @@ class App:
server_addr = _ba.get_master_server_address()
if 'localhost' in server_addr:
_ba.timer(2.0,
- Call(_ba.screenmessage,
- "Note: using local server", (1, 1, 0),
- log=True),
+ lambda: _ba.screenmessage("Note: using local server",
+ (1, 1, 0),
+ log=True),
timetype=TimeType.REAL)
elif 'test' in server_addr:
- _ba.timer(2.0,
- Call(_ba.screenmessage,
- "Note: using test server-module", (1, 1, 0),
- log=True),
- timetype=TimeType.REAL)
+ _ba.timer(
+ 2.0,
+ lambda: _ba.screenmessage("Note: using test server-module",
+ (1, 1, 0),
+ log=True),
+ timetype=TimeType.REAL)
cfg['launchCount'] = launch_count
cfg.commit()
@@ -786,7 +761,6 @@ class App:
def do_remove_in_game_ads_message(self) -> None:
"""(internal)"""
from ba._lang import Lstr
- from ba._general import Call
from ba._enums import TimeType
# Print this message once every 10 minutes at most.
@@ -797,15 +771,12 @@ class App:
with _ba.Context('ui'):
_ba.timer(
1.0,
- Call(_ba.screenmessage,
- Lstr(
- resource='removeInGameAdsText',
- subs=[
- ('${PRO}',
- Lstr(resource='store.bombSquadProNameText')),
- ('${APP_NAME}', Lstr(resource='titleText'))
- ]),
- color=(1, 1, 0)),
+ lambda: _ba.screenmessage(Lstr(
+ resource='removeInGameAdsText',
+ subs=[('${PRO}',
+ Lstr(resource='store.bombSquadProNameText')),
+ ('${APP_NAME}', Lstr(resource='titleText'))]),
+ color=(1, 1, 0)),
timetype=TimeType.REAL)
def shutdown(self) -> None:
diff --git a/assets/src/data/scripts/ba/_apputils.py b/assets/src/data/scripts/ba/_apputils.py
index 1ae82d0e..77e5d109 100644
--- a/assets/src/data/scripts/ba/_apputils.py
+++ b/assets/src/data/scripts/ba/_apputils.py
@@ -279,23 +279,29 @@ def print_corrupt_file_error() -> None:
from ba._lang import get_resource
from ba._general import Call
from ba._enums import TimeType
- _ba.timer(2.0,
- Call(_ba.screenmessage,
- get_resource('internal.corruptFileText').replace(
- '${EMAIL}', 'support@froemling.net'),
- color=(1, 0, 0)),
- timetype=TimeType.REAL)
+ _ba.timer(
+ 2.0,
+ lambda: _ba.screenmessage(get_resource('internal.corruptFileText').
+ replace('${EMAIL}', 'support@froemling.net'),
+ color=(1, 0, 0)),
+ timetype=TimeType.REAL)
_ba.timer(2.0,
Call(_ba.playsound, _ba.getsound('error')),
timetype=TimeType.REAL)
def show_ad(purpose: str,
- on_completion_call: Callable[[], Any] = None,
- pass_actually_showed: bool = False) -> None:
+ on_completion_call: Callable[[], Any] = None) -> None:
"""(internal)"""
_ba.app.last_ad_purpose = purpose
- _ba.show_ad(purpose, on_completion_call, pass_actually_showed)
+ _ba.show_ad(purpose, on_completion_call)
+
+
+def show_ad_2(purpose: str,
+ on_completion_call: Callable[[bool], Any] = None) -> None:
+ """(internal)"""
+ _ba.app.last_ad_purpose = purpose
+ _ba.show_ad_2(purpose, on_completion_call)
def call_after_ad(call: Callable[[], Any]) -> None:
diff --git a/assets/src/data/scripts/ba/_dep.py b/assets/src/data/scripts/ba/_dep.py
index 4d6ae53d..07d96fd3 100644
--- a/assets/src/data/scripts/ba/_dep.py
+++ b/assets/src/data/scripts/ba/_dep.py
@@ -19,7 +19,6 @@
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to object/asset dependencies."""
-# pylint: disable=redefined-builtin
from __future__ import annotations
@@ -74,8 +73,7 @@ class Dependency(Generic[T]):
# return different things based on self's type and avoid the need for
# the fake dep classes below.
# See https://github.com/python/mypy/issues/5320
- # noinspection PyShadowingBuiltins
- def __get__(self, obj: Any, type: Any = None) -> Any:
+ def __get__(self, obj: Any, cls: Any = None) -> Any:
if obj is None:
raise TypeError("Dependency must be accessed through an instance.")
@@ -114,15 +112,13 @@ else:
class _InstanceDep(Dependency[TI]):
"""Fake stub we use to tell the type system we provide a type."""
- # noinspection PyShadowingBuiltins
- def __get__(self, obj: Any, type: Any = None) -> Type[TI]:
+ def __get__(self, obj: Any, cls: Any = None) -> Type[TI]:
return cast(Type[TI], None)
class _StaticDep(Dependency[TS]):
"""Fake stub we use to tell the type system we provide an instance."""
- # noinspection PyShadowingBuiltins
- def __get__(self, obj: Any, type: Any = None) -> TS:
+ def __get__(self, obj: Any, cls: Any = None) -> TS:
return cast(TS, None)
# pylint: disable=invalid-name
@@ -169,7 +165,7 @@ class BoundDepComponent:
class DepComponent:
- """Base class for all classes that can act as dependencies.
+ """Base class for all classes that can act as or use dependencies.
category: Dependency Classes
"""
@@ -373,7 +369,7 @@ class InstancedDepComponent(DepComponent):
class StaticDepComponent(DepComponent):
- """Base for DepComponents intended to be instanced once and shared."""
+ """Base for DepComponents intended to be instantiated once and shared."""
@classmethod
def dep_get_payload(cls, depdata: DepData) -> Any:
diff --git a/assets/src/data/scripts/ba/_general.py b/assets/src/data/scripts/ba/_general.py
index 279c531e..aee69116 100644
--- a/assets/src/data/scripts/ba/_general.py
+++ b/assets/src/data/scripts/ba/_general.py
@@ -30,6 +30,7 @@ import _ba
if TYPE_CHECKING:
from typing import Any, Type
+ from bafoundation import executils
T = TypeVar('T')
@@ -121,7 +122,7 @@ def get_type_name(cls: Type) -> str:
return cls.__module__ + '.' + cls.__name__
-class WeakCall:
+class _WeakCall:
"""Wrap a callable and arguments into a single callable object.
Category: General Utility Classes
@@ -159,11 +160,12 @@ class WeakCall:
Instantiate a WeakCall; pass a callable as the first
arg, followed by any number of arguments or keywords.
- # example: wrap a method call with some positional and keyword args:
+ # example: wrap a method call with some positional and
+ # keyword args:
myweakcall = ba.WeakCall(myobj.dostuff, argval1, namedarg=argval2)
# Now we have a single callable to run that whole mess.
- # This is the same as calling myobj.dostuff(argval1, namedarg=argval2)
+ # The same as calling myobj.dostuff(argval1, namedarg=argval2)
# (provided my_obj still exists; this will do nothing otherwise)
myweakcall()
"""
@@ -191,16 +193,18 @@ class WeakCall:
str(self._args) + ' _keywds=' + str(self._keywds) + '>')
-class Call:
+class _Call:
"""Wraps a callable and arguments into a single callable object.
Category: General Utility Classes
- The callable is strong-referenced so it won't die until this object does.
+ The callable is strong-referenced so it won't die until this
+ object does.
+
Note that a bound method (ex: myobj.dosomething) contains a reference
- to 'self' (myobj in that case), so you will be keeping that object alive
- too. Use ba.WeakCall if you want to pass a method to callback without
- keeping its object alive.
+ to 'self' (myobj in that case), so you will be keeping that object
+ alive too. Use ba.WeakCall if you want to pass a method to callback
+ without keeping its object alive.
"""
def __init__(self, *args: Any, **keywds: Any):
@@ -208,11 +212,11 @@ class Call:
Instantiate a Call; pass a callable as the first
arg, followed by any number of arguments or keywords.
- # example: wrap a method call with 1 positional and 1 keyword arg
+ # Example: wrap a method call with 1 positional and 1 keyword arg.
mycall = ba.Call(myobj.dostuff, argval1, namedarg=argval2)
- # now we have a single callable to run that whole mess
- # this is the same as calling myobj.dostuff(argval1, namedarg=argval2)
+ # Now we have a single callable to run that whole mess.
+ # ..the same as calling myobj.dostuff(argval1, namedarg=argval2)
mycall()
"""
self._call = args[0]
@@ -227,6 +231,16 @@ class Call:
str(self._args) + ' _keywds=' + str(self._keywds) + '>')
+if TYPE_CHECKING:
+ WeakCall = executils.Call
+ Call = executils.Call
+else:
+ WeakCall = _WeakCall
+ WeakCall.__name__ = 'WeakCall'
+ Call = _Call
+ Call.__name__ = 'Call'
+
+
class WeakMethod:
"""A weak-referenced bound method.
diff --git a/assets/src/data/scripts/ba/_music.py b/assets/src/data/scripts/ba/_music.py
index 6434fad0..191a1781 100644
--- a/assets/src/data/scripts/ba/_music.py
+++ b/assets/src/data/scripts/ba/_music.py
@@ -30,7 +30,7 @@ from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
- from typing import Callable, Any, List, Optional, Dict
+ from typing import Callable, Any, List, Optional, Dict, Union
class MusicPlayer:
@@ -141,7 +141,9 @@ class InternalMusicPlayer(MusicPlayer):
class _PickFolderSongThread(threading.Thread):
- def __init__(self, path: str, callback: Callable):
+ def __init__(self, path: str,
+ callback: Callable[[Union[str, List[str]], Optional[str]],
+ None]):
super().__init__()
self._callback = callback
self._path = path
@@ -166,7 +168,7 @@ class InternalMusicPlayer(MusicPlayer):
raise Exception(
_lang.Lstr(resource='internal.noMusicFilesInFolderText'
).evaluate())
- _ba.pushcall(Call(self._callback, result=all_files),
+ _ba.pushcall(Call(self._callback, all_files, None),
from_other_thread=True)
except Exception as exc:
from ba import _error
@@ -175,9 +177,7 @@ class InternalMusicPlayer(MusicPlayer):
err_str = str(exc)
except Exception:
err_str = ''
- _ba.pushcall(Call(self._callback,
- result=self._path,
- error=err_str),
+ _ba.pushcall(Call(self._callback, self._path, err_str),
from_other_thread=True)
def on_play(self, entry: Any) -> None:
@@ -195,14 +195,19 @@ class InternalMusicPlayer(MusicPlayer):
self._actually_playing = False
self._PickFolderSongThread(name, self._on_play_folder_cb).start()
- def _on_play_folder_cb(self, result: str,
+ def _on_play_folder_cb(self,
+ result: Union[str, List[str]],
error: Optional[str] = None) -> None:
from ba import _lang
if error is not None:
rstr = (_lang.Lstr(
resource='internal.errorPlayingMusicText').evaluate())
- err_str = (rstr.replace('${MUSIC}', os.path.basename(result)) +
- '; ' + str(error))
+ if isinstance(result, str):
+ err_str = (rstr.replace('${MUSIC}', os.path.basename(result)) +
+ '; ' + str(error))
+ else:
+ err_str = (rstr.replace('${MUSIC}', '') + '; ' +
+ str(error))
_ba.screenmessage(err_str, color=(1, 0, 0))
return
@@ -332,7 +337,8 @@ class ITunesThread(threading.Thread):
self._commands.append(['GET_PLAYLISTS', callback])
self._commands_available.set()
- def _handle_get_playlists_command(self, target: str) -> None:
+ def _handle_get_playlists_command(self, target: Callable[[List[str]], None]
+ ) -> None:
from ba._general import Call
try:
playlists = _ba.itunes_get_playlists()
diff --git a/assets/src/data/scripts/ba/_stats.py b/assets/src/data/scripts/ba/_stats.py
index 8f6d8749..3e17e183 100644
--- a/assets/src/data/scripts/ba/_stats.py
+++ b/assets/src/data/scripts/ba/_stats.py
@@ -32,7 +32,7 @@ import _ba
if TYPE_CHECKING:
import ba
from weakref import ReferenceType
- from typing import Any, Dict, Optional, Sequence, Union
+ from typing import Any, Dict, Optional, Sequence, Union, Tuple
@dataclass
@@ -210,9 +210,9 @@ class PlayerRecord:
delay = 1000
sound = stats.orchestrahitsound4
- def _apply(name2: str, score2: int, showpoints2: bool,
- color2: Sequence[float], scale2: float,
- sound2: ba.Sound) -> None:
+ def _apply(name2: Lstr, score2: int, showpoints2: bool,
+ color2: Tuple[float, float, float, float], scale2: float,
+ sound2: Optional[ba.Sound]) -> None:
from bastd.actor.popuptext import PopupText
# Only award this if they're still alive and we can get
@@ -238,7 +238,8 @@ class PlayerRecord:
color=color2,
scale=scale2,
position=our_pos).autoretain()
- _ba.playsound(sound2)
+ if sound2:
+ _ba.playsound(sound2)
self.score += score2
self.accumscore += score2
diff --git a/assets/src/data/scripts/ba/internal.py b/assets/src/data/scripts/ba/internal.py
index a782625f..ec461d37 100644
--- a/assets/src/data/scripts/ba/internal.py
+++ b/assets/src/data/scripts/ba/internal.py
@@ -45,7 +45,7 @@ from ba._achievement import (get_achievement, set_completed_achievements,
display_achievement_banner,
get_achievements_for_coop_level)
from ba._apputils import (is_browser_likely_available, get_remote_app_name,
- should_submit_debug_info, show_ad)
+ should_submit_debug_info, show_ad, show_ad_2)
from ba._benchmark import (run_gpu_benchmark, run_cpu_benchmark,
run_media_reload_benchmark, run_stress_test)
from ba._campaign import get_campaign
diff --git a/assets/src/data/scripts/bafoundation/executils.py b/assets/src/data/scripts/bafoundation/executils.py
index 89fe294c..7116b69a 100644
--- a/assets/src/data/scripts/bafoundation/executils.py
+++ b/assets/src/data/scripts/bafoundation/executils.py
@@ -25,20 +25,20 @@ from __future__ import annotations
from typing import TYPE_CHECKING, TypeVar, Generic, Callable, cast
if TYPE_CHECKING:
- from typing import Any
+ from typing import Any, overload
-T = TypeVar('T', bound=Callable)
+CT = TypeVar('CT', bound=Callable)
-class _CallbackCall(Generic[T]):
+class _CallbackCall(Generic[CT]):
"""Descriptor for exposing a call with a type defined by a TypeVar."""
- def __get__(self, obj: Any, type_in: Any = None) -> T:
- return cast(T, None)
+ def __get__(self, obj: Any, type_in: Any = None) -> CT:
+ return cast(CT, None)
-class CallbackSet(Generic[T]):
- """Wrangles callbacks for a particular event."""
+class CallbackSet(Generic[CT]):
+ """Wrangles callbacks for a particular event in a type-safe manner."""
# In the type-checker's eyes, our 'run' attr is a CallbackCall which
# returns a callable with the type we were created with. This lets us
@@ -47,7 +47,7 @@ class CallbackSet(Generic[T]):
# At runtime, run() simply passes its args verbatim to its registered
# callbacks; no types are checked.
if TYPE_CHECKING:
- run: _CallbackCall[T] = _CallbackCall()
+ run: _CallbackCall[CT] = _CallbackCall()
else:
def run(self, *args, **keywds):
@@ -60,6 +60,221 @@ class CallbackSet(Generic[T]):
def __del__(self) -> None:
print("~CallbackSet()")
- def add(self, call: T) -> None:
+ def add(self, call: CT) -> None:
"""Add a callback to be run."""
print("Would add call", call)
+
+
+# Define Call() which can be used in type-checking call-wrappers that behave
+# similarly to functools.partial (in that they take a callable and some
+# positional arguments to be passed to it)
+
+# We define several different _CallXArg classes corresponding to different
+# argument counts and define Call() as an overloaded function which returns
+# one of them based on how many args are passed.
+
+# To use this, simply assign your call type to this Call for type checking:
+# example:
+# class _MyCallWrapper:
+#
+# if TYPE_CHECKING:
+# MyCallWrapper = bafoundation.executils.Call
+# else:
+# MyCallWrapper = _MyCallWrapper
+
+# Note that this setup currently only works with positional arguments; if you
+# would like to pass args via keyword you can wrap a lambda or local function
+# which takes keyword args and converts to a call containing keywords.
+
+if TYPE_CHECKING:
+ In1T = TypeVar('In1T')
+ In2T = TypeVar('In2T')
+ In3T = TypeVar('In3T')
+ In4T = TypeVar('In4T')
+ In5T = TypeVar('In5T')
+ In6T = TypeVar('In6T')
+ In7T = TypeVar('In7T')
+ OutT = TypeVar('OutT')
+
+ class _CallNoArgs(Generic[OutT]):
+ """Single argument variant of call wrapper."""
+
+ def __init__(self, _call: Callable[[], OutT]):
+ ...
+
+ def __call__(self) -> OutT:
+ ...
+
+ class _Call1Arg(Generic[In1T, OutT]):
+ """Single argument variant of call wrapper."""
+
+ def __init__(self, _call: Callable[[In1T], OutT]):
+ ...
+
+ def __call__(self, _arg1: In1T) -> OutT:
+ ...
+
+ class _Call2Args(Generic[In1T, In2T, OutT]):
+ """Two argument variant of call wrapper"""
+
+ def __init__(self, _call: Callable[[In1T, In2T], OutT]):
+ ...
+
+ def __call__(self, _arg1: In1T, _arg2: In2T) -> OutT:
+ ...
+
+ class _Call3Args(Generic[In1T, In2T, In3T, OutT]):
+ """Three argument variant of call wrapper"""
+
+ def __init__(self, _call: Callable[[In1T, In2T, In3T], OutT]):
+ ...
+
+ def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T) -> OutT:
+ ...
+
+ class _Call4Args(Generic[In1T, In2T, In3T, In4T, OutT]):
+ """Four argument variant of call wrapper"""
+
+ def __init__(self, _call: Callable[[In1T, In2T, In3T, In4T], OutT]):
+ ...
+
+ def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T,
+ _arg4: In4T) -> OutT:
+ ...
+
+ class _Call5Args(Generic[In1T, In2T, In3T, In4T, In5T, OutT]):
+ """Five argument variant of call wrapper"""
+
+ def __init__(self,
+ _call: Callable[[In1T, In2T, In3T, In4T, In5T], OutT]):
+ ...
+
+ def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T, _arg4: In4T,
+ _arg5: In5T) -> OutT:
+ ...
+
+ class _Call6Args(Generic[In1T, In2T, In3T, In4T, In5T, In6T, OutT]):
+ """Six argument variant of call wrapper"""
+
+ def __init__(
+ self,
+ _call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T], OutT]):
+ ...
+
+ def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T, _arg4: In4T,
+ _arg5: In5T, _arg6: In6T) -> OutT:
+ ...
+
+ class _Call7Args(Generic[In1T, In2T, In3T, In4T, In5T, In6T, In7T, OutT]):
+ """Seven argument variant of call wrapper"""
+
+ def __init__(
+ self,
+ _call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T, In7T],
+ OutT]):
+ ...
+
+ def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T, _arg4: In4T,
+ _arg5: In5T, _arg6: In6T, _arg7: In7T) -> OutT:
+ ...
+
+ # No arg call; no args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[], OutT]) -> _CallNoArgs[OutT]:
+ ...
+
+ # 1 arg call; 1 arg bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T], OutT], arg1: In1T) -> _CallNoArgs[OutT]:
+ ...
+
+ # 1 arg call; no args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T], OutT]) -> _Call1Arg[In1T, OutT]:
+ ...
+
+ # 2 arg call; 2 args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T], OutT], arg1: In1T,
+ arg2: In2T) -> _CallNoArgs[OutT]:
+ ...
+
+ # 2 arg call; 1 arg bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T], OutT],
+ arg1: In1T) -> _Call1Arg[In2T, OutT]:
+ ...
+
+ # 2 arg call; no args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T], OutT]) -> _CallNoArgs[OutT]:
+ ...
+
+ # 3 arg call; 3 args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T, In3T], OutT], arg1: In1T, arg2: In2T,
+ arg3: In3T) -> _CallNoArgs[OutT]:
+ ...
+
+ # 3 arg call; 2 args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T, In3T], OutT], arg1: In1T,
+ arg2: In2T) -> _Call1Arg[In3T, OutT]:
+ ...
+
+ # 3 arg call; 1 arg bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T, In3T], OutT],
+ arg1: In1T) -> _Call2Args[In2T, In3T, OutT]:
+ ...
+
+ # 3 arg call; no args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T, In3T], OutT]
+ ) -> _Call3Args[In1T, In2T, In3T, OutT]:
+ ...
+
+ # 4 arg call; 4 args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T, In3T, In4T], OutT], arg1: In1T,
+ arg2: In2T, arg3: In3T, arg4: In4T) -> _CallNoArgs[OutT]:
+ ...
+
+ # 5 arg call; 5 args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T, In3T, In4T, In5T], OutT], arg1: In1T,
+ arg2: In2T, arg3: In3T, arg4: In4T,
+ arg5: In5T) -> _CallNoArgs[OutT]:
+ ...
+
+ # 6 arg call; 6 args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T], OutT],
+ arg1: In1T, arg2: In2T, arg3: In3T, arg4: In4T, arg5: In5T,
+ arg6: In6T) -> _CallNoArgs[OutT]:
+ ...
+
+ # 7 arg call; 7 args bundled.
+ # noinspection PyPep8Naming
+ @overload
+ def Call(call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T, In7T], OutT],
+ arg1: In1T, arg2: In2T, arg3: In3T, arg4: In4T, arg5: In5T,
+ arg6: In6T, arg7: In7T) -> _CallNoArgs[OutT]:
+ ...
+
+ # noinspection PyPep8Naming
+ def Call(*_args: Any, **_keywds: Any) -> Any:
+ ...
diff --git a/assets/src/data/scripts/bastd/actor/spazbot.py b/assets/src/data/scripts/bastd/actor/spazbot.py
index 956cb89f..4b2f7c4a 100644
--- a/assets/src/data/scripts/bastd/actor/spazbot.py
+++ b/assets/src/data/scripts/bastd/actor/spazbot.py
@@ -920,7 +920,7 @@ class BotSet:
self._spawning_count += 1
def _spawn_bot(self, bot_type: Type[SpazBot], pos: Sequence[float],
- on_spawn_call: Callable[[SpazBot], Any]) -> None:
+ on_spawn_call: Optional[Callable[[SpazBot], Any]]) -> None:
spaz = bot_type()
ba.playsound(self._spawn_sound, position=pos)
assert spaz.node
diff --git a/assets/src/data/scripts/bastd/game/kingofthehill.py b/assets/src/data/scripts/bastd/game/kingofthehill.py
index b7420e2f..550e4fce 100644
--- a/assets/src/data/scripts/bastd/game/kingofthehill.py
+++ b/assets/src/data/scripts/bastd/game/kingofthehill.py
@@ -118,9 +118,10 @@ class KingOfTheHillGame(ba.TeamGameActivity):
actions=(("modify_part_collision", "collide",
True), ("modify_part_collision", "physical", False),
("call", "at_connect",
- ba.Call(self._handle_player_flag_region_collide, 1)),
+ ba.Call(self._handle_player_flag_region_collide, True)),
("call", "at_disconnect",
- ba.Call(self._handle_player_flag_region_collide, 0))))
+ ba.Call(self._handle_player_flag_region_collide,
+ False))))
def get_instance_description(self) -> Union[str, Sequence]:
return ('Secure the flag for ${ARG1} seconds.',
diff --git a/assets/src/data/scripts/bastd/game/ninjafight.py b/assets/src/data/scripts/bastd/game/ninjafight.py
index 48496ec5..1a47fcc4 100644
--- a/assets/src/data/scripts/bastd/game/ninjafight.py
+++ b/assets/src/data/scripts/bastd/game/ninjafight.py
@@ -104,46 +104,28 @@ class NinjaFightGame(ba.TeamGameActivity):
# Spawn some baddies.
ba.timer(
- 1.0,
- ba.Call(self._bots.spawn_bot,
- spazbot.ChargerBot,
- pos=(3, 3, -2),
- spawn_time=3.0))
+ 1.0, lambda: self._bots.spawn_bot(
+ spazbot.ChargerBot, pos=(3, 3, -2), spawn_time=3.0))
ba.timer(
- 2.0,
- ba.Call(self._bots.spawn_bot,
- spazbot.ChargerBot,
- pos=(-3, 3, -2),
- spawn_time=3.0))
+ 2.0, lambda: self._bots.spawn_bot(
+ spazbot.ChargerBot, pos=(-3, 3, -2), spawn_time=3.0))
ba.timer(
- 3.0,
- ba.Call(self._bots.spawn_bot,
- spazbot.ChargerBot,
- pos=(5, 3, -2),
- spawn_time=3.0))
+ 3.0, lambda: self._bots.spawn_bot(
+ spazbot.ChargerBot, pos=(5, 3, -2), spawn_time=3.0))
ba.timer(
- 4.0,
- ba.Call(self._bots.spawn_bot,
- spazbot.ChargerBot,
- pos=(-5, 3, -2),
- spawn_time=3.0))
+ 4.0, lambda: self._bots.spawn_bot(
+ spazbot.ChargerBot, pos=(-5, 3, -2), spawn_time=3.0))
# Add some extras for multiplayer or pro mode.
assert self.initial_player_info is not None
if len(self.initial_player_info) > 2 or is_pro:
ba.timer(
- 5.0,
- ba.Call(self._bots.spawn_bot,
- spazbot.ChargerBot,
- pos=(0, 3, -5),
- spawn_time=3.0))
+ 5.0, lambda: self._bots.spawn_bot(
+ spazbot.ChargerBot, pos=(0, 3, -5), spawn_time=3.0))
if len(self.initial_player_info) > 3 or is_pro:
ba.timer(
- 6.0,
- ba.Call(self._bots.spawn_bot,
- spazbot.ChargerBot,
- pos=(0, 3, 1),
- spawn_time=3.0))
+ 6.0, lambda: self._bots.spawn_bot(
+ spazbot.ChargerBot, pos=(0, 3, 1), spawn_time=3.0))
# Called for each spawning player.
def spawn_player(self, player: ba.Player) -> ba.Actor:
diff --git a/assets/src/data/scripts/bastd/game/thelaststand.py b/assets/src/data/scripts/bastd/game/thelaststand.py
index 5f806aef..e9d77447 100644
--- a/assets/src/data/scripts/bastd/game/thelaststand.py
+++ b/assets/src/data/scripts/bastd/game/thelaststand.py
@@ -152,7 +152,7 @@ class TheLastStandGame(ba.CoopGameActivity):
def _drop_powerups(self,
standard_points: bool = False,
- force_first: bool = None) -> None:
+ force_first: str = None) -> None:
"""Generic powerup drop."""
from bastd.actor import powerupbox
if standard_points:
diff --git a/assets/src/data/scripts/bastd/ui/account/__init__.py b/assets/src/data/scripts/bastd/ui/account/__init__.py
index e93604b2..84237015 100644
--- a/assets/src/data/scripts/bastd/ui/account/__init__.py
+++ b/assets/src/data/scripts/bastd/ui/account/__init__.py
@@ -40,9 +40,8 @@ def show_sign_in_prompt(account_type: str = None) -> None:
else:
confirm.ConfirmWindow(
ba.Lstr(resource='notSignedInErrorText'),
- ba.Call(settings.AccountSettingsWindow,
- modal=True,
- close_once_signed_in=True),
+ lambda: settings.AccountSettingsWindow(modal=True,
+ close_once_signed_in=True),
ok_text=ba.Lstr(resource='accountSettingsWindow.signInText'),
width=460,
height=130)
diff --git a/assets/src/data/scripts/bastd/ui/account/settings.py b/assets/src/data/scripts/bastd/ui/account/settings.py
index 8e13914a..3867952d 100644
--- a/assets/src/data/scripts/bastd/ui/account/settings.py
+++ b/assets/src/data/scripts/bastd/ui/account/settings.py
@@ -774,11 +774,11 @@ class AccountSettingsWindow(ba.OldWindow):
autoselect=True,
size=(button_width, 60),
label=ba.Lstr(resource=self._r + '.resetProgressText'),
- on_activate_call=ba.Call(confirm.ConfirmWindow,
- text=confirm_text,
- width=500,
- height=200,
- action=self._reset_progress))
+ on_activate_call=lambda: confirm.ConfirmWindow(
+ text=confirm_text,
+ width=500,
+ height=200,
+ action=self._reset_progress))
if first_selectable is None:
first_selectable = btn
if ba.app.toolbars:
diff --git a/assets/src/data/scripts/bastd/ui/confirm.py b/assets/src/data/scripts/bastd/ui/confirm.py
index 25937cdd..c364c0b9 100644
--- a/assets/src/data/scripts/bastd/ui/confirm.py
+++ b/assets/src/data/scripts/bastd/ui/confirm.py
@@ -147,7 +147,8 @@ class QuitWindow:
origin_widget: ba.Widget = None):
app = ba.app
self._back = back
- # if there's already one of us up somewhere, kill it
+
+ # If there's already one of us up somewhere, kill it.
if app.quit_window is not None:
app.quit_window.delete()
app.quit_window = None
@@ -164,9 +165,10 @@ class QuitWindow:
def _fade_and_quit(self) -> None:
_ba.fade_screen(False,
time=0.2,
- endcall=ba.Call(ba.quit, soft=True, back=self._back))
+ endcall=lambda: ba.quit(soft=True, back=self._back))
_ba.lock_all_input()
- # unlock and fade back in shortly.. just in case something goes wrong
+
+ # Unlock and fade back in shortly.. just in case something goes wrong
# (or on android where quit just backs out of our activity and
# we may come back)
ba.timer(0.3, _ba.unlock_all_input, timetype=ba.TimeType.REAL)
diff --git a/assets/src/data/scripts/bastd/ui/coop/browser.py b/assets/src/data/scripts/bastd/ui/coop/browser.py
index 333a401e..7ef92fe2 100644
--- a/assets/src/data/scripts/bastd/ui/coop/browser.py
+++ b/assets/src/data/scripts/bastd/ui/coop/browser.py
@@ -797,15 +797,15 @@ class CoopBrowserWindow(ba.OldWindow):
row_v_show_buffer = 100
v -= 198
- h_scroll = ba.hscrollwidget(parent=w_parent,
- size=(self._scroll_width - 10, 205),
- position=(-5, v),
- simple_culling_h=70,
- highlight=False,
- border_opacity=0.0,
- color=(0.45, 0.4, 0.5),
- on_select_call=ba.Call(
- self._on_row_selected, 'campaign'))
+ h_scroll = ba.hscrollwidget(
+ parent=w_parent,
+ size=(self._scroll_width - 10, 205),
+ position=(-5, v),
+ simple_culling_h=70,
+ highlight=False,
+ border_opacity=0.0,
+ color=(0.45, 0.4, 0.5),
+ on_select_call=lambda: self._on_row_selected('campaign'))
self._campaign_h_scroll = h_scroll
ba.widget(edit=h_scroll,
show_buffer_top=row_v_show_buffer,
@@ -1021,7 +1021,7 @@ class CoopBrowserWindow(ba.OldWindow):
# event queue.. we need to push ours too so we're enabled *after* them.
ba.pushcall(self._enable_selectable_callback)
- def _on_row_selected(self, row: int) -> None:
+ def _on_row_selected(self, row: str) -> None:
if self._do_selection_callbacks:
if self._selected_row != row:
self._selected_row = row
@@ -1039,16 +1039,14 @@ class CoopBrowserWindow(ba.OldWindow):
'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=ba.Call(
- self.run,
- None,
- tournament_button=data))
+ 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,
@@ -1353,7 +1351,7 @@ class CoopBrowserWindow(ba.OldWindow):
origin_widget=self._league_rank_button.get_button()).
get_root_widget())
- def _switch_to_score(self, show_tab: str = 'extras') -> None:
+ def _switch_to_score(self, show_tab: Optional[str] = 'extras') -> None:
# pylint: disable=cyclic-import
from bastd.ui import account
from bastd.ui.store import browser
@@ -1403,7 +1401,9 @@ class CoopBrowserWindow(ba.OldWindow):
"""Return whether our tourney data is up to date."""
return self._tourney_data_up_to_date
- def run(self, game: str, tournament_button: Dict[str, Any] = None) -> None:
+ def run(self,
+ game: Optional[str],
+ tournament_button: Dict[str, Any] = None) -> None:
"""Run the provided game."""
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
@@ -1515,6 +1515,7 @@ class CoopBrowserWindow(ba.OldWindow):
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')
diff --git a/assets/src/data/scripts/bastd/ui/fileselector.py b/assets/src/data/scripts/bastd/ui/fileselector.py
index 9b8b2f18..ff9ac7e7 100644
--- a/assets/src/data/scripts/bastd/ui/fileselector.py
+++ b/assets/src/data/scripts/bastd/ui/fileselector.py
@@ -108,7 +108,7 @@ class FileSelectorWindow(ba.OldWindow):
self._folder_color = (1.1, 0.8, 0.2)
self._file_tex = ba.gettexture('file')
self._file_color = (1, 1, 1)
- self._use_folder_button = None
+ self._use_folder_button: Optional[ba.Widget] = None
self._folder_center = self._width * 0.5 + 15
self._folder_icon = ba.imagewidget(parent=self._root_widget,
size=(40, 40),
@@ -189,7 +189,7 @@ class FileSelectorWindow(ba.OldWindow):
class _RefreshThread(threading.Thread):
def __init__(self, path: str,
- callback: Callable[[List[str], str], Any]):
+ callback: Callable[[List[str], Optional[str]], Any]):
super().__init__()
self._callback = callback
self._path = path
@@ -200,17 +200,19 @@ class FileSelectorWindow(ba.OldWindow):
files = os.listdir(self._path)
duration = time.time() - starttime
min_time = 0.1
- # make sure this takes at least 1/10 second so the user
- # has time to see the selection highlight
+
+ # Make sure this takes at least 1/10 second so the user
+ # has time to see the selection highlight.
if duration < min_time:
time.sleep(min_time - duration)
- ba.pushcall(ba.Call(self._callback, file_names=files),
+ ba.pushcall(ba.Call(self._callback, files, None),
from_other_thread=True)
except Exception as exc:
- # ignore permission-denied
+ # Ignore permission-denied.
if 'Errno 13' not in str(exc):
ba.print_exception()
- ba.pushcall(ba.Call(self._callback, error=str(exc)),
+ nofiles: List[str] = []
+ ba.pushcall(ba.Call(self._callback, nofiles, str(exc)),
from_other_thread=True)
def _set_path(self, path: str, add_to_recent: bool = True) -> None:
@@ -219,7 +221,7 @@ class FileSelectorWindow(ba.OldWindow):
self._recent_paths.append(path)
self._RefreshThread(path, self._refresh).start()
- def _refresh(self, file_names: List[str], error: str) -> None:
+ def _refresh(self, file_names: List[str], error: Optional[str]) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
@@ -357,6 +359,7 @@ class FileSelectorWindow(ba.OldWindow):
if num == 0:
ba.widget(edit=cnt, up_widget=self._back_button)
is_valid_file_path = self._is_valid_file_path(entry)
+ assert self._path is not None
is_dir = os.path.isdir(self._path + '/' + entry)
if is_dir:
ba.imagewidget(parent=cnt,
diff --git a/assets/src/data/scripts/bastd/ui/gather.py b/assets/src/data/scripts/bastd/ui/gather.py
index 84c3feb9..8b8b4b80 100644
--- a/assets/src/data/scripts/bastd/ui/gather.py
+++ b/assets/src/data/scripts/bastd/ui/gather.py
@@ -218,17 +218,15 @@ class GatherWindow(ba.OldWindow):
# that they will get disconnected.. otherwise just go ahead..
google_player_count = (_ba.get_google_play_party_client_count())
if google_player_count > 0:
- confirm.ConfirmWindow(ba.Lstr(
- resource=self._r + '.googlePlayReInviteText',
- subs=[('${COUNT}', str(google_player_count))]),
- ba.Call(ba.timer,
- 0.2,
- _ba.invite_players,
- timetype='real'),
- width=500,
- height=150,
- ok_text=ba.Lstr(resource=self._r +
- '.googlePlayInviteText'))
+ confirm.ConfirmWindow(
+ ba.Lstr(resource=self._r + '.googlePlayReInviteText',
+ subs=[('${COUNT}', str(google_player_count))]),
+ lambda: ba.timer(
+ 0.2, _ba.invite_players, timetype=ba.TimeType.REAL),
+ width=500,
+ height=150,
+ ok_text=ba.Lstr(resource=self._r +
+ '.googlePlayInviteText'))
else:
ba.timer(0.1, _ba.invite_players, timetype=ba.TimeType.REAL)
@@ -411,19 +409,19 @@ class GatherWindow(ba.OldWindow):
h_align='center',
v_align='center')
v -= 180
- ba.buttonwidget(parent=cnt,
- label=ba.Lstr(resource=self._r +
- '.googlePlaySeeInvitesText'),
- color=(0.5, 0.5, 0.6),
- textcolor=(0.75, 0.7, 0.8),
- autoselect=True,
- position=(c_width * 0.5 - b_width2 * 0.5, v),
- size=(b_width2, 60),
- on_activate_call=ba.Call(
- ba.timer,
- 0.1,
- self._on_google_play_show_invites_press,
- timetype='real'))
+ ba.buttonwidget(
+ parent=cnt,
+ label=ba.Lstr(resource=self._r + '.googlePlaySeeInvitesText'),
+ color=(0.5, 0.5, 0.6),
+ textcolor=(0.75, 0.7, 0.8),
+ autoselect=True,
+ position=(c_width * 0.5 - b_width2 * 0.5, v),
+ size=(b_width2, 60),
+ on_activate_call=lambda: ba.timer(
+ 0.1,
+ self._on_google_play_show_invites_press,
+ timetype=ba.TimeType.REAL),
+ )
elif tab == 'internet':
c_width = self._scroll_width
@@ -448,9 +446,8 @@ class GatherWindow(ba.OldWindow):
click_activate=True,
selectable=True,
autoselect=True,
- on_activate_call=ba.Call(self._set_internet_tab,
- 'join',
- playsound=True),
+ on_activate_call=lambda: self._set_internet_tab(
+ 'join', playsound=True),
text=ba.Lstr(resource=self._r +
'.joinPublicPartyDescriptionText'))
ba.widget(edit=txt, up_widget=self._tab_buttons[tab])
@@ -466,9 +463,8 @@ class GatherWindow(ba.OldWindow):
click_activate=True,
selectable=True,
autoselect=True,
- on_activate_call=ba.Call(self._set_internet_tab,
- 'host',
- playsound=True),
+ on_activate_call=lambda: self._set_internet_tab(
+ 'host', playsound=True),
text=ba.Lstr(resource=self._r +
'.hostPublicPartyDescriptionText'))
ba.widget(edit=txt,
@@ -824,7 +820,8 @@ class GatherWindow(ba.OldWindow):
class HostAddrFetchThread(threading.Thread):
"""Thread to fetch an addr."""
- def __init__(self, name: str, call: Callable[[str], Any]):
+ def __init__(self, name: str,
+ call: Callable[[Optional[str]], Any]):
super().__init__()
self._name = name
self._call = call
@@ -839,7 +836,7 @@ class GatherWindow(ba.OldWindow):
ba.pushcall(ba.Call(self._call, None),
from_other_thread=True)
- def do_it_2(addr2: str) -> None:
+ def do_it_2(addr2: Optional[str]) -> None:
if addr2 is None:
ba.screenmessage(ba.Lstr(
resource='internal.unableToResolveHostText'),
@@ -1681,10 +1678,9 @@ class GatherWindow(ba.OldWindow):
class PingThread(threading.Thread):
"""Thread for sending out pings."""
- def __init__(
- self, address: str, port: int,
- call: Callable[[str, int, Optional[int]], Any]
- ):
+ def __init__(self, address: str, port: int,
+ call: Callable[[str, int, Optional[int]],
+ Optional[int]]):
super().__init__()
# need utf8 here to avoid an error on our minimum
# bundled python
@@ -1743,7 +1739,8 @@ class GatherWindow(ba.OldWindow):
PingThread(party['address'], party['port'],
ba.WeakCall(self._ping_callback)).start()
- def _ping_callback(self, address: str, port: int, result: int) -> None:
+ def _ping_callback(self, address: str, port: Optional[int],
+ result: Optional[int]) -> None:
# Look for a widget corresponding to this target; if we find one,
# update our list.
party = self._public_parties.get(address + '_' + str(port))
diff --git a/assets/src/data/scripts/bastd/ui/getcurrency.py b/assets/src/data/scripts/bastd/ui/getcurrency.py
index bcf3e4d5..844acddc 100644
--- a/assets/src/data/scripts/bastd/ui/getcurrency.py
+++ b/assets/src/data/scripts/bastd/ui/getcurrency.py
@@ -609,7 +609,7 @@ def show_get_tickets_prompt() -> None:
confirm.ConfirmWindow(
ba.Lstr(translate=('serverResponses',
'You don\'t have enough tickets for this!')),
- ba.Call(GetCurrencyWindow, modal=True),
+ lambda: GetCurrencyWindow(modal=True),
ok_text=ba.Lstr(resource='getTicketsWindow.titleText'),
width=460,
height=130)
diff --git a/assets/src/data/scripts/bastd/ui/kiosk.py b/assets/src/data/scripts/bastd/ui/kiosk.py
index a0fa5f96..7ed11fe0 100644
--- a/assets/src/data/scripts/bastd/ui/kiosk.py
+++ b/assets/src/data/scripts/bastd/ui/kiosk.py
@@ -39,12 +39,16 @@ class KioskWindow(ba.OldWindow):
from bastd.ui import confirm
self._width = 720.0
self._height = 340.0
- super().__init__(root_widget=ba.containerwidget(
- size=(self._width, self._height),
- transition=transition,
- on_cancel_call=ba.Call(confirm.QuitWindow, swish=True, back=True),
- background=False,
- stack_offset=(0, -130)))
+
+ def _do_cancel() -> None:
+ confirm.QuitWindow(swish=True, back=True)
+
+ super().__init__(
+ root_widget=ba.containerwidget(size=(self._width, self._height),
+ transition=transition,
+ on_cancel_call=_do_cancel,
+ background=False,
+ stack_offset=(0, -130)))
self._r = 'kioskWindow'
diff --git a/assets/src/data/scripts/bastd/ui/league/rankbutton.py b/assets/src/data/scripts/bastd/ui/league/rankbutton.py
index d06d89aa..2145a18b 100644
--- a/assets/src/data/scripts/bastd/ui/league/rankbutton.py
+++ b/assets/src/data/scripts/bastd/ui/league/rankbutton.py
@@ -207,7 +207,9 @@ class LeagueRankButton:
if widget:
widget.delete()
- ba.timer(2.0, ba.Call(safe_delete, diff_text, timetype='real'))
+ ba.timer(2.0,
+ ba.Call(safe_delete, diff_text),
+ timetype=ba.TimeType.REAL)
status_text: Union[str, ba.Lstr]
if self._rank is not None:
assert self._smooth_rank is not None
diff --git a/assets/src/data/scripts/bastd/ui/mainmenu.py b/assets/src/data/scripts/bastd/ui/mainmenu.py
index 57e34da7..199d25eb 100644
--- a/assets/src/data/scripts/bastd/ui/mainmenu.py
+++ b/assets/src/data/scripts/bastd/ui/mainmenu.py
@@ -320,10 +320,12 @@ class MainMenuWindow(ba.OldWindow):
# we want back presses to quit our activity.
if (not self._in_game and not self._have_quit_button
and ba.app.platform == 'android'):
+
+ def _do_quit() -> None:
+ confirm.QuitWindow(swish=True, back=True)
+
ba.containerwidget(edit=self._root_widget,
- on_cancel_call=ba.Call(confirm.QuitWindow,
- swish=True,
- back=True))
+ on_cancel_call=_do_quit)
# Add speed-up/slow-down buttons for replays.
# (ideally this should be part of a fading-out playback bar like most
diff --git a/assets/src/data/scripts/bastd/ui/partyqueue.py b/assets/src/data/scripts/bastd/ui/partyqueue.py
index d6b3891d..c0c24b48 100644
--- a/assets/src/data/scripts/bastd/ui/partyqueue.py
+++ b/assets/src/data/scripts/bastd/ui/partyqueue.py
@@ -123,7 +123,7 @@ class PartyQueueWindow(ba.OldWindow):
# need to push a deferred call to kill these as necessary instead.
# (should bulletproof internal widget code to give a clean error
# in this case)
- def kill_widgets(widgets: Sequence[ba.Widget]) -> None:
+ def kill_widgets(widgets: Sequence[Optional[ba.Widget]]) -> None:
for widget in widgets:
if widget:
widget.delete()
diff --git a/assets/src/data/scripts/bastd/ui/playlist/addgame.py b/assets/src/data/scripts/bastd/ui/playlist/addgame.py
index 334d0be3..885cc32a 100644
--- a/assets/src/data/scripts/bastd/ui/playlist/addgame.py
+++ b/assets/src/data/scripts/bastd/ui/playlist/addgame.py
@@ -152,22 +152,26 @@ class PlaylistAddGameWindow(ba.OldWindow):
gametypes.sort(key=lambda g: g.get_display_string().evaluate())
for i, gametype in enumerate(gametypes):
- txt = ba.textwidget(
- parent=self._column,
- position=(0, 0),
- size=(self._width - 88, 24),
- text=gametype.get_display_string(),
- h_align="left",
- v_align="center",
- color=(0.8, 0.8, 0.8, 1.0),
- maxwidth=self._scroll_width * 0.8,
- on_select_call=ba.Call(self._set_selected_game_type, gametype),
- always_highlight=True,
- selectable=True,
- on_activate_call=ba.Call(ba.timer,
- 0.1,
- self._select_button.activate,
- timetype='real'))
+
+ def _doit() -> None:
+ if self._select_button:
+ ba.timer(0.1,
+ self._select_button.activate,
+ timetype=ba.TimeType.REAL)
+
+ txt = ba.textwidget(parent=self._column,
+ position=(0, 0),
+ size=(self._width - 88, 24),
+ text=gametype.get_display_string(),
+ h_align="left",
+ v_align="center",
+ color=(0.8, 0.8, 0.8, 1.0),
+ maxwidth=self._scroll_width * 0.8,
+ on_select_call=ba.Call(
+ self._set_selected_game_type, gametype),
+ always_highlight=True,
+ selectable=True,
+ on_activate_call=_doit)
if i == 0:
ba.widget(edit=txt, up_widget=self._back_button)
diff --git a/assets/src/data/scripts/bastd/ui/playlist/browser.py b/assets/src/data/scripts/bastd/ui/playlist/browser.py
index a6a65c4d..3ad4a07c 100644
--- a/assets/src/data/scripts/bastd/ui/playlist/browser.py
+++ b/assets/src/data/scripts/bastd/ui/playlist/browser.py
@@ -350,6 +350,7 @@ class PlaylistBrowserWindow(ba.OldWindow):
for y in range(rows):
for x in range(columns):
name = items[index][0]
+ assert name is not None
pos = (x * (button_width + 2 * button_buffer_h) +
button_buffer_h + 8 + h_offs, self._sub_height - 47 -
(y + 1) * (button_height + 2 * button_buffer_v))
diff --git a/assets/src/data/scripts/bastd/ui/playoptions.py b/assets/src/data/scripts/bastd/ui/playoptions.py
index c60ff459..3e358462 100644
--- a/assets/src/data/scripts/bastd/ui/playoptions.py
+++ b/assets/src/data/scripts/bastd/ui/playoptions.py
@@ -225,9 +225,8 @@ class PlayOptionsWindow(popup.PopupWindow):
position=(h, v),
texture=ba.gettexture(tex_name if owned else 'empty'),
model_opaque=model_opaque if owned else None,
- on_activate_call=ba.Call(ba.screenmessage,
- desc,
- color=desc_color),
+ on_activate_call=ba.Call(ba.screenmessage, desc,
+ desc_color),
label='',
color=(1, 1, 1),
autoselect=True,
diff --git a/assets/src/data/scripts/bastd/ui/tabs.py b/assets/src/data/scripts/bastd/ui/tabs.py
index 470835ee..71e045ff 100644
--- a/assets/src/data/scripts/bastd/ui/tabs.py
+++ b/assets/src/data/scripts/bastd/ui/tabs.py
@@ -27,7 +27,7 @@ from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
- from typing import Any, Callable, Dict, Tuple, List, Sequence
+ from typing import Any, Callable, Dict, Tuple, List, Sequence, Optional
def create_tab_buttons(parent_widget: ba.Widget,
@@ -50,9 +50,11 @@ def create_tab_buttons(parent_widget: ba.Widget,
h = pos[0]
for _i, tab in enumerate(tabs):
- def _tick_and_call(call: Callable[[Any], Any], arg: Any) -> None:
+ def _tick_and_call(call: Optional[Callable[[Any], Any]],
+ arg: Any) -> None:
ba.playsound(ba.getsound('click01'))
- call(arg)
+ if call is not None:
+ call(arg)
pos = (h + tab_spacing * 0.5, tab_pos_v)
size = (tab_button_width - tab_spacing, 50.0)
diff --git a/assets/src/data/scripts/bastd/ui/tournamententry.py b/assets/src/data/scripts/bastd/ui/tournamententry.py
index fe3e39bd..931be250 100644
--- a/assets/src/data/scripts/bastd/ui/tournamententry.py
+++ b/assets/src/data/scripts/bastd/ui/tournamententry.py
@@ -541,7 +541,7 @@ class TournamentEntryWindow(popup.PopupWindow):
self._launch()
def _on_pay_with_ad_press(self) -> None:
- from ba.internal import show_ad
+ from ba.internal import show_ad_2
# If we're already entering, ignore.
if self._entering:
@@ -563,9 +563,8 @@ class TournamentEntryWindow(popup.PopupWindow):
cur_time = ba.time(ba.TimeType.REAL)
if cur_time - self._last_ad_press_time > 5.0:
self._last_ad_press_time = cur_time
- show_ad('tournament_entry',
- on_completion_call=ba.WeakCall(self._on_ad_complete),
- pass_actually_showed=True)
+ show_ad_2('tournament_entry',
+ on_completion_call=ba.WeakCall(self._on_ad_complete))
def _on_ad_complete(self, actually_showed: bool) -> None:
diff --git a/assets/src/data/scripts/bastd/ui/watch.py b/assets/src/data/scripts/bastd/ui/watch.py
index fd467f33..78527204 100644
--- a/assets/src/data/scripts/bastd/ui/watch.py
+++ b/assets/src/data/scripts/bastd/ui/watch.py
@@ -333,15 +333,15 @@ class WatchWindow(ba.OldWindow):
autoselect=True,
maxwidth=c_width * 0.7,
max_chars=200)
- cbtn = ba.buttonwidget(parent=cnt,
- label=ba.Lstr(resource='cancelText'),
- on_activate_call=ba.Call(
- ba.containerwidget,
- edit=cnt,
- transition='out_scale'),
- size=(180, 60),
- position=(30, 30),
- autoselect=True)
+ cbtn = ba.buttonwidget(
+ parent=cnt,
+ label=ba.Lstr(resource='cancelText'),
+ on_activate_call=ba.Call(
+ lambda c: ba.containerwidget(edit=c, transition='out_scale'),
+ cnt),
+ size=(180, 60),
+ position=(30, 30),
+ autoselect=True)
okb = ba.buttonwidget(parent=cnt,
label=ba.Lstr(resource=self._r + '.renameText'),
size=(180, 60),
diff --git a/tools/efrotools/code.py b/tools/efrotools/code.py
index 95eddec4..ca5927a4 100644
--- a/tools/efrotools/code.py
+++ b/tools/efrotools/code.py
@@ -85,7 +85,7 @@ def formatcode(projroot: Path, full: bool) -> None:
flush=True)
-def cpplintcode(projroot: Path, full: bool) -> None:
+def cpplint(projroot: Path, full: bool) -> None:
"""Run lint-checking on all code deemed lint-able."""
from concurrent.futures import ThreadPoolExecutor
from efrotools import get_config
@@ -230,7 +230,7 @@ def get_script_filenames(projroot: Path) -> List[str]:
return sorted(list(f for f in filenames if 'flycheck_' not in f))
-def pylintscripts(projroot: Path, full: bool, fast: bool) -> None:
+def pylint(projroot: Path, full: bool, fast: bool) -> None:
"""Run lint-checking on all scripts deemed lint-able."""
from efrotools import get_files_hash
pylintrc = Path(projroot, '.pylintrc')
@@ -516,7 +516,7 @@ def runmypy(filenames: List[str], full: bool = False,
subprocess.run(args, check=check)
-def mypyscripts(projroot: Path, full: bool) -> None:
+def mypy(projroot: Path, full: bool) -> None:
"""Run mypy on all of our scripts."""
import time
filenames = get_script_filenames(projroot)
diff --git a/tools/efrotools/pybuild.py b/tools/efrotools/pybuild.py
index 6e861209..ef555569 100644
--- a/tools/efrotools/pybuild.py
+++ b/tools/efrotools/pybuild.py
@@ -137,9 +137,9 @@ def build_apple(arch: str, debug: bool = False) -> None:
txt = efrotools.replace_one(
txt, '&& PATH=$(PROJECT_DIR)/$(PYTHON_DIR-macOS)/dist/bin:$(PATH) .',
'&& PATH="$(PROJECT_DIR)/$(PYTHON_DIR-macOS)/dist/bin:$(PATH)" .')
- txt = efrotools.replace_one(
- txt, '&& PATH=$(PROJECT_DIR)/$(PYTHON_DIR-macOS)/dist/bin:$(PATH) m',
- '&& PATH="$(PROJECT_DIR)/$(PYTHON_DIR-macOS)/dist/bin:$(PATH)" m')
+ # txt = efrotools.replace_one(
+ # txt, '&& PATH=$(PROJECT_DIR)/$(PYTHON_DIR-macOS)/dist/bin:$(PATH) m',
+ # '&& PATH="$(PROJECT_DIR)/$(PYTHON_DIR-macOS)/dist/bin:$(PATH)" m')
# Remove makefile dependencies so we don't build the
# libs we're not using.
diff --git a/tools/efrotools/snippets.py b/tools/efrotools/snippets.py
index 388a2232..6c9c10d6 100644
--- a/tools/efrotools/snippets.py
+++ b/tools/efrotools/snippets.py
@@ -226,11 +226,11 @@ def formatmakefile() -> None:
outfile.write(formatted)
-def cpplintcode() -> None:
+def cpplint() -> None:
"""Run lint-checking on all code deemed lint-able."""
from efrotools import code
full = '-full' in sys.argv
- code.cpplintcode(PROJROOT, full)
+ code.cpplint(PROJROOT, full)
def scriptfiles() -> None:
@@ -247,19 +247,19 @@ def scriptfiles() -> None:
print(' '.join(paths))
-def pylintscripts() -> None:
+def pylint() -> None:
"""Run pylint checks on our scripts."""
from efrotools import code
full = ('-full' in sys.argv)
fast = ('-fast' in sys.argv)
- code.pylintscripts(PROJROOT, full, fast)
+ code.pylint(PROJROOT, full, fast)
-def mypyscripts() -> None:
+def mypy() -> None:
"""Run mypy checks on our scripts."""
from efrotools import code
full = ('-full' in sys.argv)
- code.mypyscripts(PROJROOT, full)
+ code.mypy(PROJROOT, full)
def pycharmscripts() -> None:
diff --git a/tools/snippets b/tools/snippets
index 3e9bb549..594d11b0 100755
--- a/tools/snippets
+++ b/tools/snippets
@@ -43,10 +43,9 @@ import efrotools
# noinspection PyUnresolvedReferences
from efrotools.snippets import ( # pylint: disable=unused-import
PROJROOT, CleanError, snippets_main, formatcode, formatscripts,
- formatmakefile, cpplintcode, pylintscripts, mypyscripts,
- tool_config_install, sync, sync_all, scriptfiles, pycharmscripts,
- clioncode, androidstudiocode, makefile_target_list, spelling, spelling_all,
- compile_python_files)
+ formatmakefile, cpplint, pylint, mypy, tool_config_install, sync, sync_all,
+ scriptfiles, pycharmscripts, clioncode, androidstudiocode,
+ makefile_target_list, spelling, spelling_all, compile_python_files)
if TYPE_CHECKING:
from typing import Optional, List, Sequence