From 2d5c2dda9ad848048b23747be457277135930f75 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Thu, 15 Oct 2020 22:08:06 -0700 Subject: [PATCH] Cleaned up some subsystems such as ba.app.lang and ba.app.ui --- .efrocachemap | 40 +- .idea/dictionaries/ericf.xml | 3 + CHANGELOG.md | 6 +- assets/.asset_manifest_public.json | 4 +- assets/Makefile | 4 +- assets/src/ba_data/python/ba/__init__.py | 6 +- assets/src/ba_data/python/ba/_account.py | 11 +- assets/src/ba_data/python/ba/_achievement.py | 20 +- .../src/ba_data/python/ba/_activitytypes.py | 6 +- assets/src/ba_data/python/ba/_app.py | 110 +--- assets/src/ba_data/python/ba/_apputils.py | 18 +- assets/src/ba_data/python/ba/_benchmark.py | 9 +- assets/src/ba_data/python/ba/_collision.py | 10 +- assets/src/ba_data/python/ba/_coopgame.py | 2 +- assets/src/ba_data/python/ba/_coopsession.py | 2 +- assets/src/ba_data/python/ba/_gameactivity.py | 2 +- assets/src/ba_data/python/ba/_gameresults.py | 2 +- assets/src/ba_data/python/ba/_gameutils.py | 2 +- assets/src/ba_data/python/ba/_hooks.py | 36 +- assets/src/ba_data/python/ba/_lang.py | 464 --------------- assets/src/ba_data/python/ba/_language.py | 562 ++++++++++++++++++ assets/src/ba_data/python/ba/_level.py | 4 +- assets/src/ba_data/python/ba/_lobby.py | 2 +- assets/src/ba_data/python/ba/_map.py | 4 +- assets/src/ba_data/python/ba/_meta.py | 2 +- .../ba_data/python/ba/_multiteamsession.py | 2 +- assets/src/ba_data/python/ba/_music.py | 2 + assets/src/ba_data/python/ba/_player.py | 2 +- assets/src/ba_data/python/ba/_servermode.py | 2 +- assets/src/ba_data/python/ba/_session.py | 2 +- assets/src/ba_data/python/ba/_stats.py | 6 +- assets/src/ba_data/python/ba/_store.py | 13 +- assets/src/ba_data/python/ba/_team.py | 2 +- assets/src/ba_data/python/ba/_ui.py | 9 +- assets/src/ba_data/python/ba/deprecated.py | 5 - assets/src/ba_data/python/ba/macmusicapp.py | 7 +- assets/src/ba_data/python/ba/modutils.py | 6 +- assets/src/ba_data/python/ba/osmusic.py | 10 +- .../python/bastd/activity/dualteamscore.py | 3 +- .../python/bastd/activity/multiteamvictory.py | 4 +- assets/src/ba_data/python/bastd/mainmenu.py | 4 +- assets/src/ba_data/python/bastd/ui/gather.py | 2 +- assets/src/ba_data/python/bastd/ui/helpui.py | 24 +- .../python/bastd/ui/league/rankwindow.py | 4 +- .../python/bastd/ui/settings/advanced.py | 23 +- .../python/bastd/ui/soundtrack/edit.py | 4 +- .../src/ba_data/python/bastd/ui/trophies.py | 35 +- .../.idea/dictionaries/ericf.xml | 3 + docs/ba_module.md | 298 ++++++++-- src/ballistica/ballistica.cc | 4 +- src/generated_src/ballistica/binding.py | 8 +- 51 files changed, 1027 insertions(+), 788 deletions(-) delete mode 100644 assets/src/ba_data/python/ba/_lang.py create mode 100644 assets/src/ba_data/python/ba/_language.py diff --git a/.efrocachemap b/.efrocachemap index 99c8b895..1f3eee67 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3932,24 +3932,24 @@ "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e", "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", - "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/a3/b4/d47b1f9ca27dc994d225efc9b652", - "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a1/20/f75ee36d80a99dbfe1ff79db2093", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/94/91/af4a3be510e2570651fbb8d4c297", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cd/6c/73657b342cb1666dd3ac734a93c8", - "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/a6/9d/8830abe356b106005b793952f60c", - "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/93/e7/063a5a038904ef0641d405a5e66d", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/69/e8/77839309e3301d62bbf77624d810", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/95/4e/9e5dbd0b19acddc2cd056eca123c", - "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/0c/bb/3990f398178b924de0b493db14d8", - "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/85/14/604f8855cfa461797c5e3c5b5aab", - "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/e1/83/9d2ffd1a9f149a18c005be57db29", - "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/f6/63/9aaf6704f8dcb6e32808d09989a4", - "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/15/54/bfba7d740c7221a5d46e8e21c756", - "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4b/1f/ca36bea671a5b88a7e2ccf2e4c4a", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/57/2d/e4b9a67cb21131cdcdfb8287f9e7", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/59/b6/6ffc20f2c0253180496d2dae968c", - "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cd/4f/d760d9fce637b61efeed648063cc", - "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7c/5a/d63a634b3886c9cf1b3697d24b75", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/46/80/98efbaeed954d2b008a9bfb77e12", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/89/24/6aae1e666373c46b409e44d7cdf7" + "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/68/70/ecdec08c0236f5bebbf298c9f4cd", + "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/85/7f/53fb1e3f1414b4865315d815b17a", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9e/ae/586229f233660b6b49ca655b7c1b", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e5/a5/c6b90e3629a8041a68827aafec3f", + "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d3/66/82b2cab9b1438c30cfd33eefba5e", + "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/52/37/253765404b79740cd4a7ac5fe325", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c7/69/ecdbcee9df40225475391beb4cb2", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/34/51/cb528ea6eb5ac853794922a0cc34", + "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d4/b8/d3a690970ca379d805432bde8533", + "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/4d/16/b9014f5d983481731b54f7045dd9", + "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/1e/11/d3c478923937f376eecb25561ae9", + "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/d6/b4/b7854676ec826e42aa3ecb394b49", + "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7a/76/bf185c1ea65f3c25cfa63666511b", + "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/49/30/5acd0d56b736a3729e7689cd4e18", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f7/e8/197874ac6d756c341628ea4bb518", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/50/6f/140a3d77288e2355605c9d0c942a", + "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1c/ca/62d29425e0ad0b17c145ecbc97fe", + "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c0/fa/52a38fc153714bff3bcda077ed75", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/75/5b/e84edb24f5df313bb6ebafa91b19", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0f/2e/af28c0026c0379e5441e789e9721" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index e69f197f..d0296a83 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -850,6 +850,7 @@ getplayer getpt getremote + getres getscanresults getscoreconfig getsession @@ -1015,6 +1016,7 @@ intex intp introspectable + iobj ipaddress ipos iprof @@ -1359,6 +1361,7 @@ nline nlines nntplib + noassets nodeactor nodepos nodpi diff --git a/CHANGELOG.md b/CHANGELOG.md index 41addb3c..f73dd01d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -### 1.5.26 (20178) +### 1.5.27 (20218) +- Language functionality has been consolidated into a LanguageSubsystem object at ba.app.lang +- ba.get_valid_languages() is now an attr: ba.app.lang.available_languages + +### 1.5.26 (20217) - Simplified licensing header on python scripts. - General project refactoring in order to open source most of the C++ layer. diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json index 19bd2d97..79e43d36 100644 --- a/assets/.asset_manifest_public.json +++ b/assets/.asset_manifest_public.json @@ -30,7 +30,7 @@ "ba_data/python/ba/__pycache__/_hooks.cpython-38.opt-1.pyc", "ba_data/python/ba/__pycache__/_input.cpython-38.opt-1.pyc", "ba_data/python/ba/__pycache__/_keyboard.cpython-38.opt-1.pyc", - "ba_data/python/ba/__pycache__/_lang.cpython-38.opt-1.pyc", + "ba_data/python/ba/__pycache__/_language.cpython-38.opt-1.pyc", "ba_data/python/ba/__pycache__/_level.cpython-38.opt-1.pyc", "ba_data/python/ba/__pycache__/_lobby.cpython-38.opt-1.pyc", "ba_data/python/ba/__pycache__/_map.cpython-38.opt-1.pyc", @@ -91,7 +91,7 @@ "ba_data/python/ba/_hooks.py", "ba_data/python/ba/_input.py", "ba_data/python/ba/_keyboard.py", - "ba_data/python/ba/_lang.py", + "ba_data/python/ba/_language.py", "ba_data/python/ba/_level.py", "ba_data/python/ba/_lobby.py", "ba_data/python/ba/_map.py", diff --git a/assets/Makefile b/assets/Makefile index 784b3207..36d24841 100644 --- a/assets/Makefile +++ b/assets/Makefile @@ -161,7 +161,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \ build/ba_data/python/ba/_hooks.py \ build/ba_data/python/ba/_input.py \ build/ba_data/python/ba/_keyboard.py \ - build/ba_data/python/ba/_lang.py \ + build/ba_data/python/ba/_language.py \ build/ba_data/python/ba/_level.py \ build/ba_data/python/ba/_lobby.py \ build/ba_data/python/ba/_map.py \ @@ -399,7 +399,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ build/ba_data/python/ba/__pycache__/_hooks.cpython-38.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_input.cpython-38.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_keyboard.cpython-38.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_lang.cpython-38.opt-1.pyc \ + build/ba_data/python/ba/__pycache__/_language.cpython-38.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_level.cpython-38.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_lobby.cpython-38.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_map.cpython-38.opt-1.pyc \ diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index 67440e76..d141478e 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -42,9 +42,10 @@ from ba._gameactivity import GameActivity from ba._gameresults import GameResults from ba._settings import (Setting, IntSetting, FloatSetting, ChoiceSetting, BoolSetting, IntChoiceSetting, FloatChoiceSetting) -from ba._lang import Lstr, setlanguage, get_valid_languages +from ba._language import Lstr, LanguageSubsystem from ba._map import Map, getmaps from ba._session import Session +from ba._ui import UISubsystem from ba._servermode import ServerController from ba._score import ScoreType, ScoreConfig from ba._stats import PlayerScoredMessage, PlayerRecord, Stats @@ -70,7 +71,8 @@ from ba._messages import (UNHANDLED, OutOfBoundsMessage, DeathType, DieMessage, ShouldShatterMessage, ImpactDamageMessage, FreezeMessage, ThawMessage, HitMessage, CelebrateMessage) -from ba._music import setmusic, MusicPlayer, MusicType, MusicPlayMode +from ba._music import (setmusic, MusicPlayer, MusicType, MusicPlayMode, + MusicSubsystem) from ba._powerup import PowerupMessage, PowerupAcceptMessage from ba._multiteamsession import MultiTeamSession from ba.ui import Window, UIController, uicleanupcheck diff --git a/assets/src/ba_data/python/ba/_account.py b/assets/src/ba_data/python/ba/_account.py index ecdfbd6b..5fee72c0 100644 --- a/assets/src/ba_data/python/ba/_account.py +++ b/assets/src/ba_data/python/ba/_account.py @@ -18,7 +18,7 @@ def handle_account_gained_tickets(count: int) -> None: (internal) """ - from ba._lang import Lstr + from ba._language import Lstr _ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText', subs=[('${COUNT}', str(count))]), color=(0, 1, 0)) @@ -166,7 +166,7 @@ def have_pro_options() -> bool: def show_post_purchase_message() -> None: """(internal)""" - from ba._lang import Lstr + from ba._language import Lstr from ba._enums import TimeType app = _ba.app cur_time = _ba.time(TimeType.REAL) @@ -183,14 +183,15 @@ def show_post_purchase_message() -> None: def on_account_state_changed() -> None: """(internal)""" import time - from ba import _lang + from ba import _language app = _ba.app # Run any pending promo codes we had queued up while not signed in. if _ba.get_account_state() == 'signed_in' and app.pending_promo_codes: for code in app.pending_promo_codes: - _ba.screenmessage(_lang.Lstr(resource='submittingPromoCodeText'), - color=(0, 1, 0)) + _ba.screenmessage( + _language.Lstr(resource='submittingPromoCodeText'), + color=(0, 1, 0)) _ba.add_transaction({ 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, diff --git a/assets/src/ba_data/python/ba/_achievement.py b/assets/src/ba_data/python/ba/_achievement.py index 7a616476..039fbe81 100644 --- a/assets/src/ba_data/python/ba/_achievement.py +++ b/assets/src/ba_data/python/ba/_achievement.py @@ -269,7 +269,7 @@ class Achievement: @property def display_name(self) -> ba.Lstr: """Return a ba.Lstr for this Achievement's name.""" - from ba._lang import Lstr + from ba._language import Lstr name: Union[ba.Lstr, str] try: if self._level_name != '': @@ -289,16 +289,18 @@ class Achievement: @property def description(self) -> ba.Lstr: """Get a ba.Lstr for the Achievement's brief description.""" - from ba._lang import Lstr, get_resource - if 'description' in get_resource('achievements')[self._name]: + from ba._language import Lstr + if 'description' in _ba.app.lang.get_resource('achievements')[ + self._name]: return Lstr(resource='achievements.' + self._name + '.description') return Lstr(resource='achievements.' + self._name + '.descriptionFull') @property def description_complete(self) -> ba.Lstr: """Get a ba.Lstr for the Achievement's description when completed.""" - from ba._lang import Lstr, get_resource - if 'descriptionComplete' in get_resource('achievements')[self._name]: + from ba._language import Lstr + if 'descriptionComplete' in _ba.app.lang.get_resource('achievements')[ + self._name]: return Lstr(resource='achievements.' + self._name + '.descriptionComplete') return Lstr(resource='achievements.' + self._name + @@ -307,7 +309,7 @@ class Achievement: @property def description_full(self) -> ba.Lstr: """Get a ba.Lstr for the Achievement's full description.""" - from ba._lang import Lstr + from ba._language import Lstr return Lstr( resource='achievements.' + self._name + '.descriptionFull', @@ -318,7 +320,7 @@ class Achievement: @property def description_full_complete(self) -> ba.Lstr: """Get a ba.Lstr for the Achievement's full desc. when completed.""" - from ba._lang import Lstr + from ba._language import Lstr return Lstr( resource='achievements.' + self._name + '.descriptionFullComplete', subs=[('${LEVEL}', @@ -353,7 +355,7 @@ class Achievement: Shows the Achievement icon, name, and description. """ # pylint: disable=cyclic-import - from ba._lang import Lstr + from ba._language import Lstr from ba._enums import SpecialChar from ba._coopsession import CoopSession from bastd.actor.image import Image @@ -657,7 +659,7 @@ class Achievement: from bastd.actor.text import Text from bastd.actor.image import Image from ba._general import WeakCall - from ba._lang import Lstr + from ba._language import Lstr from ba._messages import DieMessage from ba._enums import TimeType, SpecialChar app = _ba.app diff --git a/assets/src/ba_data/python/ba/_activitytypes.py b/assets/src/ba_data/python/ba/_activitytypes.py index d739ba86..c877adb9 100644 --- a/assets/src/ba_data/python/ba/_activitytypes.py +++ b/assets/src/ba_data/python/ba/_activitytypes.py @@ -167,7 +167,7 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]): def on_begin(self) -> None: # pylint: disable=cyclic-import from bastd.actor.text import Text - from ba import _lang + from ba import _language super().on_begin() # Pop up a 'press any button to continue' statement after our @@ -176,9 +176,9 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]): if _ba.app.ui.uiscale is UIScale.LARGE: # FIXME: Need a better way to determine whether we've probably # got a keyboard. - sval = _lang.Lstr(resource='pressAnyKeyButtonText') + sval = _language.Lstr(resource='pressAnyKeyButtonText') else: - sval = _lang.Lstr(resource='pressAnyButtonText') + sval = _language.Lstr(resource='pressAnyButtonText') Text(self._custom_continue_message if self._custom_continue_message is not None else sval, diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py index d3a9936e..7c0de6d6 100644 --- a/assets/src/ba_data/python/ba/_app.py +++ b/assets/src/ba_data/python/ba/_app.py @@ -11,7 +11,7 @@ import _ba if TYPE_CHECKING: import ba - from ba import _lang, _meta + from ba import _language, _meta from bastd.actor import spazappearance from typing import Optional, Dict, Set, Any, Type, Tuple, Callable, List @@ -49,87 +49,6 @@ class App: assert isinstance(self._env['config_file_path'], str) return self._env['config_file_path'] - @property - def locale(self) -> str: - """Raw country/language code detected by the game (such as 'en_US'). - - Generally for language-specific code you should look at - ba.App.language, which is the language the game is using - (which may differ from locale if the user sets a language, etc.) - """ - assert isinstance(self._env['locale'], str) - return self._env['locale'] - - def can_display_language(self, language: str) -> bool: - """Tell whether we can display a particular language. - - (internal) - - On some platforms we don't have unicode rendering yet - which limits the languages we can draw. - """ - - # We don't yet support full unicode display on windows or linux :-(. - if (language in { - 'Chinese', 'ChineseTraditional', 'Persian', 'Korean', 'Arabic', - 'Hindi', 'Vietnamese' - } and not _ba.can_display_full_unicode()): - return False - return True - - def _get_default_language(self) -> str: - languages = { - 'de': 'German', - 'es': 'Spanish', - 'sk': 'Slovak', - 'it': 'Italian', - 'nl': 'Dutch', - 'da': 'Danish', - 'pt': 'Portuguese', - 'fr': 'French', - 'el': 'Greek', - 'ru': 'Russian', - 'pl': 'Polish', - 'sv': 'Swedish', - 'eo': 'Esperanto', - 'cs': 'Czech', - 'hr': 'Croatian', - 'hu': 'Hungarian', - 'be': 'Belarussian', - 'ro': 'Romanian', - 'ko': 'Korean', - 'fa': 'Persian', - 'ar': 'Arabic', - 'zh': 'Chinese', - 'tr': 'Turkish', - 'id': 'Indonesian', - 'sr': 'Serbian', - 'uk': 'Ukrainian', - 'vi': 'Vietnamese', - 'vec': 'Venetian', - 'hi': 'Hindi' - } - - # Special case for Chinese: map specific variations to traditional. - # (otherwise will map to 'Chinese' which is simplified) - if self.locale in ('zh_HANT', 'zh_TW'): - language = 'ChineseTraditional' - else: - language = languages.get(self.locale[:2], 'English') - if not self.can_display_language(language): - language = 'English' - return language - - @property - def language(self) -> str: - """The name of the language the game is running in. - - This can be selected explicitly by the user or may be set - automatically based on ba.App.locale or other factors. - """ - assert isinstance(self.config, dict) - return self.config.get('Lang', self.default_language) - @property def user_agent_string(self) -> str: """String containing various bits of info about OS/device/etc.""" @@ -254,7 +173,8 @@ class App: """ # pylint: disable=too-many-statements from ba._music import MusicSubsystem - from ba._ui import UI + from ba._language import LanguageSubsystem + from ba._ui import UISubsystem # Config. self.config_file_healthy = False @@ -283,7 +203,6 @@ class App: self.active_plugins: Dict[str, ba.Plugin] = {} # Misc. - self.default_language = self._get_default_language() self.metascan: Optional[_meta.ScanResults] = None self.tips: List[str] = [] self.stress_test_reset_timer: Optional[ba.Timer] = None @@ -326,8 +245,7 @@ class App: self.music = MusicSubsystem() # Language. - self.language_target: Optional[_lang.AttrDict] = None - self.language_merged: Optional[_lang.AttrDict] = None + self.lang = LanguageSubsystem() # Achievements. self.achievements: List[ba.Achievement] = [] @@ -358,7 +276,7 @@ class App: self.coop_session_args: Dict = {} # UI. - self.ui = UI() + self.ui = UISubsystem() self.value_test_defaults: dict = {} self.first_main_menu = True # FIXME: Move to mainmenu class. @@ -551,7 +469,7 @@ class App: activity: Optional[ba.Activity] = _ba.get_foreground_host_activity() if (activity is not None and activity.allow_pausing and not _ba.have_connected_clients()): - from ba import _gameutils, _lang + from ba import _gameutils, _language from ba._nodeactor import NodeActor # FIXME: Shouldn't be touching scene stuff here; @@ -567,10 +485,14 @@ class App: _ba.newnode( 'text', attrs={ - 'text': _lang.Lstr(resource='pausedByHostText'), - 'client_only': True, - 'flatness': 1.0, - 'h_align': 'center' + 'text': + _language.Lstr(resource='pausedByHostText'), + 'client_only': + True, + 'flatness': + 1.0, + 'h_align': + 'center' })) def resume(self) -> None: @@ -705,7 +627,7 @@ class App: def do_remove_in_game_ads_message(self) -> None: """(internal)""" - from ba._lang import Lstr + from ba._language import Lstr from ba._enums import TimeType # Print this message once every 10 minutes at most. @@ -730,7 +652,7 @@ class App: def handle_deep_link(self, url: str) -> None: """Handle a deep link URL.""" - from ba._lang import Lstr + from ba._language import Lstr from ba._enums import TimeType appname = _ba.appname() if url.startswith(f'{appname}://code/'): diff --git a/assets/src/ba_data/python/ba/_apputils.py b/assets/src/ba_data/python/ba/_apputils.py index ea2ba64d..10f4ca15 100644 --- a/assets/src/ba_data/python/ba/_apputils.py +++ b/assets/src/ba_data/python/ba/_apputils.py @@ -40,8 +40,8 @@ def is_browser_likely_available() -> bool: def get_remote_app_name() -> ba.Lstr: """(internal)""" - from ba import _lang - return _lang.Lstr(resource='remote_app.app_name') + from ba import _language + return _language.Lstr(resource='remote_app.app_name') def should_submit_debug_info() -> bool: @@ -213,15 +213,15 @@ def print_live_object_warnings(when: Any, def print_corrupt_file_error() -> None: """Print an error if a corrupt file is found.""" - from ba._lang import get_resource from ba._general import Call from ba._enums import TimeType - _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, + lambda: _ba.screenmessage( + _ba.app.lang.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) diff --git a/assets/src/ba_data/python/ba/_benchmark.py b/assets/src/ba_data/python/ba/_benchmark.py index c5b0618d..070b022d 100644 --- a/assets/src/ba_data/python/ba/_benchmark.py +++ b/assets/src/ba_data/python/ba/_benchmark.py @@ -151,13 +151,14 @@ def run_media_reload_benchmark() -> None: def delay_add(start_time: float) -> None: def doit(start_time_2: float) -> None: - from ba import _lang _ba.screenmessage( - _lang.get_resource('debugWindow.totalReloadTimeText').replace( - '${TIME}', str(_ba.time(TimeType.REAL) - start_time_2))) + _ba.app.lang.get_resource( + 'debugWindow.totalReloadTimeText').replace( + '${TIME}', + str(_ba.time(TimeType.REAL) - start_time_2))) _ba.print_load_info() if _ba.app.config.resolve('Texture Quality') != 'High': - _ba.screenmessage(_lang.get_resource( + _ba.screenmessage(_ba.app.lang.get_resource( 'debugWindow.reloadBenchmarkBestResultsText'), color=(1, 1, 0)) diff --git a/assets/src/ba_data/python/ba/_collision.py b/assets/src/ba_data/python/ba/_collision.py index 1959c741..e9db1e8e 100644 --- a/assets/src/ba_data/python/ba/_collision.py +++ b/assets/src/ba_data/python/ba/_collision.py @@ -14,7 +14,10 @@ if TYPE_CHECKING: class Collision: - """A class providing info about occurring collisions.""" + """A class providing info about occurring collisions. + + Category: Gameplay Classes + """ @property def position(self) -> ba.Vec3: @@ -62,5 +65,8 @@ _collision = Collision() def getcollision() -> Collision: - """Return the in-progress collision.""" + """Return the in-progress collision. + + Category: Gameplay Functions + """ return _collision diff --git a/assets/src/ba_data/python/ba/_coopgame.py b/assets/src/ba_data/python/ba/_coopgame.py index f1a516f3..adc64ad4 100644 --- a/assets/src/ba_data/python/ba/_coopgame.py +++ b/assets/src/ba_data/python/ba/_coopgame.py @@ -139,7 +139,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): def _show_remaining_achievements(self) -> None: # pylint: disable=cyclic-import from ba._achievement import get_achievements_for_coop_level - from ba._lang import Lstr + from ba._language import Lstr from bastd.actor.text import Text ts_h_offs = 30 v_offs = -200 diff --git a/assets/src/ba_data/python/ba/_coopsession.py b/assets/src/ba_data/python/ba/_coopsession.py index 5e351854..6273f867 100644 --- a/assets/src/ba_data/python/ba/_coopsession.py +++ b/assets/src/ba_data/python/ba/_coopsession.py @@ -229,7 +229,7 @@ class CoopSession(Session): # pylint: disable=too-many-statements # pylint: disable=cyclic-import from ba._activitytypes import JoinActivity, TransitionActivity - from ba._lang import Lstr + from ba._language import Lstr from ba._general import WeakCall from ba._coopgame import CoopGameActivity from ba._gameresults import GameResults diff --git a/assets/src/ba_data/python/ba/_gameactivity.py b/assets/src/ba_data/python/ba/_gameactivity.py index b4da7b35..3ada842d 100644 --- a/assets/src/ba_data/python/ba/_gameactivity.py +++ b/assets/src/ba_data/python/ba/_gameactivity.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, TypeVar from ba._activity import Activity from ba._score import ScoreConfig -from ba._lang import Lstr +from ba._language import Lstr from ba._messages import PlayerDiedMessage, StandMessage from ba._error import NotFoundError, print_error, print_exception from ba._general import Call, WeakCall diff --git a/assets/src/ba_data/python/ba/_gameresults.py b/assets/src/ba_data/python/ba/_gameresults.py index 563c9527..9885b30f 100644 --- a/assets/src/ba_data/python/ba/_gameresults.py +++ b/assets/src/ba_data/python/ba/_gameresults.py @@ -106,7 +106,7 @@ class GameResults: (properly formatted for the score type.) """ from ba._gameutils import timestring - from ba._lang import Lstr + from ba._language import Lstr from ba._enums import TimeFormat from ba._score import ScoreType if not self._game_set: diff --git a/assets/src/ba_data/python/ba/_gameutils.py b/assets/src/ba_data/python/ba/_gameutils.py index cda5d083..89e73542 100644 --- a/assets/src/ba_data/python/ba/_gameutils.py +++ b/assets/src/ba_data/python/ba/_gameutils.py @@ -267,7 +267,7 @@ def timestring(timeval: float, use a 'timedisplay' Node and attribute connections. """ - from ba._lang import Lstr + from ba._language import Lstr # Temp sanity check while we transition from milliseconds to seconds # based time values. diff --git a/assets/src/ba_data/python/ba/_hooks.py b/assets/src/ba_data/python/ba/_hooks.py index 1365329a..c1e10c8f 100644 --- a/assets/src/ba_data/python/ba/_hooks.py +++ b/assets/src/ba_data/python/ba/_hooks.py @@ -40,31 +40,31 @@ def set_config_fullscreen_off() -> None: def not_signed_in_screen_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.screenmessage(Lstr(resource='notSignedInErrorText')) def connecting_to_party_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.screenmessage(Lstr(resource='internal.connectingToPartyText'), color=(1, 1, 1)) def rejecting_invite_already_in_party_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.screenmessage( Lstr(resource='internal.rejectingInviteAlreadyInPartyText'), color=(1, 0.5, 0)) def connection_failed_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.screenmessage(Lstr(resource='internal.connectionFailedText'), color=(1, 0.5, 0)) def temporarily_unavailable_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage( Lstr(resource='getTicketsWindow.unavailableTemporarilyText'), @@ -72,20 +72,20 @@ def temporarily_unavailable_message() -> None: def in_progress_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='getTicketsWindow.inProgressText'), color=(1, 0, 0)) def error_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) def purchase_not_valid_error() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='store.purchaseNotValidError', subs=[('${EMAIL}', 'support@froemling.net')]), @@ -93,28 +93,28 @@ def purchase_not_valid_error() -> None: def purchase_already_in_progress_error() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='store.purchaseAlreadyInProgressText'), color=(1, 0, 0)) def gear_vr_controller_warning() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='usesExternalControllerText'), color=(1, 0, 0)) def orientation_reset_cb_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.screenmessage( Lstr(resource='internal.vrOrientationResetCardboardText'), color=(0, 1, 0)) def orientation_reset_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.screenmessage(Lstr(resource='internal.vrOrientationResetText'), color=(0, 1, 0)) @@ -133,8 +133,8 @@ def launch_main_menu_session() -> None: def language_test_toggle() -> None: - from ba._lang import setlanguage - setlanguage('Gibberish' if _ba.app.language == 'English' else 'English') + _ba.app.lang.setlanguage('Gibberish' if _ba.app.lang.language == + 'English' else 'English') def award_in_control_achievement() -> None: @@ -156,7 +156,7 @@ def launch_coop_game(name: str) -> None: def purchases_restored_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.screenmessage(Lstr(resource='getTicketsWindow.purchasesRestoredText'), color=(0, 1, 0)) @@ -168,7 +168,7 @@ def dismiss_wii_remotes_window() -> None: def unavailable_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.screenmessage(Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0)) @@ -185,7 +185,7 @@ def set_last_ad_network(sval: str) -> None: def no_game_circle_message() -> None: - from ba._lang import Lstr + from ba._language import Lstr _ba.screenmessage(Lstr(resource='noGameCircleText'), color=(1, 0, 0)) @@ -250,7 +250,7 @@ def read_config() -> None: def ui_remote_press() -> None: """Handle a press by a remote device that is only usable for nav.""" - from ba._lang import Lstr + from ba._language import Lstr # Can be called without a context; need a context for getsound. with _ba.Context('ui'): diff --git a/assets/src/ba_data/python/ba/_lang.py b/assets/src/ba_data/python/ba/_lang.py deleted file mode 100644 index 2e98ea26..00000000 --- a/assets/src/ba_data/python/ba/_lang.py +++ /dev/null @@ -1,464 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Language related functionality.""" -from __future__ import annotations - -import json -import os -from typing import TYPE_CHECKING, overload - -import _ba - -if TYPE_CHECKING: - import ba - from typing import Any, Dict, List, Optional, Tuple, Union, Sequence - - -class Lstr: - """Used to define strings in a language-independent way. - - category: General Utility Classes - - These should be used whenever possible in place of hard-coded strings - so that in-game or UI elements show up correctly on all clients in their - currently-active language. - - To see available resource keys, look at any of the bs_language_*.py files - in the game or the translations pages at bombsquadgame.com/translate. - - # EXAMPLE 1: specify a string from a resource path - mynode.text = ba.Lstr(resource='audioSettingsWindow.titleText') - - # EXAMPLE 2: specify a translated string via a category and english value; - # if a translated value is available, it will be used; otherwise the - # english value will be. To see available translation categories, look - # under the 'translations' resource section. - mynode.text = ba.Lstr(translate=('gameDescriptions', 'Defeat all enemies')) - - # EXAMPLE 3: specify a raw value and some substitutions. Substitutions can - # be used with resource and translate modes as well. - mynode.text = ba.Lstr(value='${A} / ${B}', - subs=[('${A}', str(score)), ('${B}', str(total))]) - - # EXAMPLE 4: Lstrs can be nested. This example would display the resource - # at res_a but replace ${NAME} with the value of the resource at res_b - mytextnode.text = ba.Lstr(resource='res_a', - subs=[('${NAME}', ba.Lstr(resource='res_b'))]) - """ - - # pylint: disable=redefined-outer-name, dangerous-default-value - # noinspection PyDefaultArgument - @overload - def __init__(self, - *, - resource: str, - fallback_resource: str = '', - fallback_value: str = '', - subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None: - """Create an Lstr from a string resource.""" - ... - - # noinspection PyShadowingNames,PyDefaultArgument - @overload - def __init__(self, - *, - translate: Tuple[str, str], - subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None: - """Create an Lstr by translating a string in a category.""" - ... - - # noinspection PyDefaultArgument - @overload - def __init__(self, - *, - value: str, - subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None: - """Create an Lstr from a raw string value.""" - ... - - # pylint: enable=redefined-outer-name, dangerous-default-value - - def __init__(self, *args: Any, **keywds: Any) -> None: - """Instantiate a Lstr. - - Pass a value for either 'resource', 'translate', - or 'value'. (see Lstr help for examples). - 'subs' can be a sequence of 2-member sequences consisting of values - and replacements. - 'fallback_resource' can be a resource key that will be used if the - main one is not present for - the current language in place of falling back to the english value - ('resource' mode only). - 'fallback_value' can be a literal string that will be used if neither - the resource nor the fallback resource is found ('resource' mode only). - """ - # pylint: disable=too-many-branches - if args: - raise TypeError('Lstr accepts only keyword arguments') - - # Basically just store the exact args they passed. - # However if they passed any Lstr values for subs, - # replace them with that Lstr's dict. - self.args = keywds - our_type = type(self) - - if isinstance(self.args.get('value'), our_type): - raise TypeError("'value' must be a regular string; not an Lstr") - - if 'subs' in self.args: - subs_new = [] - for key, value in keywds['subs']: - if isinstance(value, our_type): - subs_new.append((key, value.args)) - else: - subs_new.append((key, value)) - self.args['subs'] = subs_new - - # As of protocol 31 we support compact key names - # ('t' instead of 'translate', etc). Convert as needed. - if 'translate' in keywds: - keywds['t'] = keywds['translate'] - del keywds['translate'] - if 'resource' in keywds: - keywds['r'] = keywds['resource'] - del keywds['resource'] - if 'value' in keywds: - keywds['v'] = keywds['value'] - del keywds['value'] - if 'fallback' in keywds: - from ba import _error - _error.print_error( - 'deprecated "fallback" arg passed to Lstr(); use ' - 'either "fallback_resource" or "fallback_value"', - once=True) - keywds['f'] = keywds['fallback'] - del keywds['fallback'] - if 'fallback_resource' in keywds: - keywds['f'] = keywds['fallback_resource'] - del keywds['fallback_resource'] - if 'subs' in keywds: - keywds['s'] = keywds['subs'] - del keywds['subs'] - if 'fallback_value' in keywds: - keywds['fv'] = keywds['fallback_value'] - del keywds['fallback_value'] - - def evaluate(self) -> str: - """Evaluate the Lstr and returns a flat string in the current language. - - You should avoid doing this as much as possible and instead pass - and store Lstr values. - """ - return _ba.evaluate_lstr(self._get_json()) - - def is_flat_value(self) -> bool: - """Return whether the Lstr is a 'flat' value. - - This is defined as a simple string value incorporating no translations, - resources, or substitutions. In this case it may be reasonable to - replace it with a raw string value, perform string manipulation on it, - etc. - """ - return bool('v' in self.args and not self.args.get('s', [])) - - def _get_json(self) -> str: - try: - return json.dumps(self.args, separators=(',', ':')) - except Exception: - from ba import _error - _error.print_exception('_get_json failed for', self.args) - return 'JSON_ERR' - - def __str__(self) -> str: - return '' - - def __repr__(self) -> str: - return '' - - @staticmethod - def from_json(json_string: str) -> ba.Lstr: - """Given a json string, returns a ba.Lstr. Does no data validation.""" - lstr = Lstr(value='') - lstr.args = json.loads(json_string) - return lstr - - -def setlanguage(language: Optional[str], - print_change: bool = True, - store_to_config: bool = True) -> None: - """Set the active language used for the game. - - category: General Utility Functions - - Pass None to use OS default language. - """ - # pylint: disable=too-many-locals - # pylint: disable=too-many-statements - # pylint: disable=too-many-branches - cfg = _ba.app.config - cur_language = cfg.get('Lang', None) - - # Store this in the config if its changing. - if language != cur_language and store_to_config: - if language is None: - if 'Lang' in cfg: - del cfg['Lang'] # Clear it out for default. - else: - cfg['Lang'] = language - cfg.commit() - switched = True - else: - switched = False - - with open('ba_data/data/languages/english.json') as infile: - lenglishvalues = json.loads(infile.read()) - - # None implies default. - if language is None: - language = _ba.app.default_language - try: - if language == 'English': - lmodvalues = None - else: - lmodfile = 'ba_data/data/languages/' + language.lower() + '.json' - with open(lmodfile) as infile: - lmodvalues = json.loads(infile.read()) - except Exception: - from ba import _error - _error.print_exception('Exception importing language:', language) - _ba.screenmessage("Error setting language to '" + language + - "'; see log for details", - color=(1, 0, 0)) - switched = False - lmodvalues = None - - # Create an attrdict of *just* our target language. - _ba.app.language_target = AttrDict() - langtarget = _ba.app.language_target - assert langtarget is not None - _add_to_attr_dict(langtarget, - lmodvalues if lmodvalues is not None else lenglishvalues) - - # Create an attrdict of our target language overlaid on our base (english). - languages = [lenglishvalues] - if lmodvalues is not None: - languages.append(lmodvalues) - lfull = AttrDict() - for lmod in languages: - _add_to_attr_dict(lfull, lmod) - _ba.app.language_merged = lfull - - # Pass some keys/values in for low level code to use; - # start with everything in their 'internal' section. - internal_vals = [ - v for v in list(lfull['internal'].items()) if isinstance(v[1], str) - ] - - # Cherry-pick various other values to include. - # (should probably get rid of the 'internal' section - # and do everything this way) - for value in [ - 'replayNameDefaultText', 'replayWriteErrorText', - 'replayVersionErrorText', 'replayReadErrorText' - ]: - internal_vals.append((value, lfull[value])) - internal_vals.append( - ('axisText', lfull['configGamepadWindow']['axisText'])) - internal_vals.append(('buttonText', lfull['buttonText'])) - lmerged = _ba.app.language_merged - assert lmerged is not None - random_names = [ - n.strip() for n in lmerged['randomPlayerNamesText'].split(',') - ] - random_names = [n for n in random_names if n != ''] - _ba.set_internal_language_keys(internal_vals, random_names) - if switched and print_change: - _ba.screenmessage(Lstr(resource='languageSetText', - subs=[('${LANGUAGE}', - Lstr(translate=('languages', language))) - ]), - color=(0, 1, 0)) - - -def _add_to_attr_dict(dst: AttrDict, src: Dict) -> None: - for key, value in list(src.items()): - if isinstance(value, dict): - try: - dst_dict = dst[key] - except Exception: - dst_dict = dst[key] = AttrDict() - if not isinstance(dst_dict, AttrDict): - raise RuntimeError("language key '" + key + - "' is defined both as a dict and value") - _add_to_attr_dict(dst_dict, value) - else: - if not isinstance(value, (float, int, bool, str, str, type(None))): - raise TypeError("invalid value type for res '" + key + "': " + - str(type(value))) - dst[key] = value - - -class AttrDict(dict): - """A dict that can be accessed with dot notation. - - (so foo.bar is equivalent to foo['bar']) - """ - - def __getattr__(self, attr: str) -> Any: - val = self[attr] - assert not isinstance(val, bytes) - return val - - def __setattr__(self, attr: str, value: Any) -> None: - raise Exception() - - -def get_resource(resource: str, - fallback_resource: str = None, - fallback_value: Any = None) -> Any: - """Return a translation resource by name.""" - try: - # If we have no language set, go ahead and set it. - if _ba.app.language_merged is None: - language = _ba.app.language - try: - setlanguage(language, - print_change=False, - store_to_config=False) - except Exception: - from ba import _error - _error.print_exception('exception setting language to', - language) - - # Try english as a fallback. - if language != 'English': - print('Resorting to fallback language (English)') - try: - setlanguage('English', - print_change=False, - store_to_config=False) - except Exception: - _error.print_exception( - 'error setting language to english fallback') - - # If they provided a fallback_resource value, try the - # target-language-only dict first and then fall back to trying the - # fallback_resource value in the merged dict. - if fallback_resource is not None: - try: - values = _ba.app.language_target - splits = resource.split('.') - dicts = splits[:-1] - key = splits[-1] - for dct in dicts: - assert values is not None - values = values[dct] - assert values is not None - val = values[key] - return val - except Exception: - # FIXME: Shouldn't we try the fallback resource in the merged - # dict AFTER we try the main resource in the merged dict? - try: - values = _ba.app.language_merged - splits = fallback_resource.split('.') - dicts = splits[:-1] - key = splits[-1] - for dct in dicts: - assert values is not None - values = values[dct] - assert values is not None - val = values[key] - return val - - except Exception: - # If we got nothing for fallback_resource, default to the - # normal code which checks or primary value in the merge - # dict; there's a chance we can get an english value for - # it (which we weren't looking for the first time through). - pass - - values = _ba.app.language_merged - splits = resource.split('.') - dicts = splits[:-1] - key = splits[-1] - for dct in dicts: - assert values is not None - values = values[dct] - assert values is not None - val = values[key] - return val - - except Exception: - # Ok, looks like we couldn't find our main or fallback resource - # anywhere. Now if we've been given a fallback value, return it; - # otherwise fail. - from ba import _error - if fallback_value is not None: - return fallback_value - raise _error.NotFoundError( - f"Resource not found: '{resource}'") from None - - -def translate(category: str, - strval: str, - raise_exceptions: bool = False, - print_errors: bool = False) -> str: - """Translate a value (or return the value if no translation available) - - Generally you should use ba.Lstr which handles dynamic translation, - as opposed to this which returns a flat string. - """ - try: - translated = get_resource('translations')[category][strval] - except Exception as exc: - if raise_exceptions: - raise - if print_errors: - print(('Translate error: category=\'' + category + '\' name=\'' + - strval + '\' exc=' + str(exc) + '')) - translated = None - translated_out: str - if translated is None: - translated_out = strval - else: - translated_out = translated - assert isinstance(translated_out, str) - return translated_out - - -def get_valid_languages() -> List[str]: - """Return a list containing names of all available languages. - - category: General Utility Functions - - Languages that may be present but are not displayable on the running - version of the game are ignored. - """ - langs = set() - app = _ba.app - try: - names = os.listdir('ba_data/data/languages') - names = [n.replace('.json', '').capitalize() for n in names] - - # FIXME: our simple capitalization fails on multi-word names; - # should handle this in a better way... - for i, name in enumerate(names): - if name == 'Chinesetraditional': - names[i] = 'ChineseTraditional' - except Exception: - from ba import _error - _error.print_exception() - names = [] - for name in names: - if app.can_display_language(name): - langs.add(name) - return sorted(name for name in names if app.can_display_language(name)) - - -def is_custom_unicode_char(char: str) -> bool: - """Return whether a char is in the custom unicode range we use.""" - assert isinstance(char, str) - if len(char) != 1: - raise ValueError('Invalid Input; must be length 1') - return 0xE000 <= ord(char) <= 0xF8FF diff --git a/assets/src/ba_data/python/ba/_language.py b/assets/src/ba_data/python/ba/_language.py new file mode 100644 index 00000000..98f0da56 --- /dev/null +++ b/assets/src/ba_data/python/ba/_language.py @@ -0,0 +1,562 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Language related functionality.""" +from __future__ import annotations + +import json +import os +from typing import TYPE_CHECKING, overload + +import _ba + +if TYPE_CHECKING: + import ba + from typing import Any, Dict, List, Optional, Tuple, Union, Sequence + + +class LanguageSubsystem: + """Wraps up language related app functionality. + + Category: App Classes + + To use this class, access the single instance of it at 'ba.app.lang'. + """ + + def __init__(self) -> None: + self.language_target: Optional[AttrDict] = None + self.language_merged: Optional[AttrDict] = None + self.default_language = self._get_default_language() + + def _can_display_language(self, language: str) -> bool: + """Tell whether we can display a particular language. + + On some platforms we don't have unicode rendering yet + which limits the languages we can draw. + """ + + # We don't yet support full unicode display on windows or linux :-(. + if (language in { + 'Chinese', 'ChineseTraditional', 'Persian', 'Korean', 'Arabic', + 'Hindi', 'Vietnamese' + } and not _ba.can_display_full_unicode()): + return False + return True + + @property + def locale(self) -> str: + """Raw country/language code detected by the game (such as 'en_US'). + + Generally for language-specific code you should look at + ba.App.language, which is the language the game is using + (which may differ from locale if the user sets a language, etc.) + """ + env = _ba.env() + assert isinstance(env['locale'], str) + return env['locale'] + + def _get_default_language(self) -> str: + languages = { + 'de': 'German', + 'es': 'Spanish', + 'sk': 'Slovak', + 'it': 'Italian', + 'nl': 'Dutch', + 'da': 'Danish', + 'pt': 'Portuguese', + 'fr': 'French', + 'el': 'Greek', + 'ru': 'Russian', + 'pl': 'Polish', + 'sv': 'Swedish', + 'eo': 'Esperanto', + 'cs': 'Czech', + 'hr': 'Croatian', + 'hu': 'Hungarian', + 'be': 'Belarussian', + 'ro': 'Romanian', + 'ko': 'Korean', + 'fa': 'Persian', + 'ar': 'Arabic', + 'zh': 'Chinese', + 'tr': 'Turkish', + 'id': 'Indonesian', + 'sr': 'Serbian', + 'uk': 'Ukrainian', + 'vi': 'Vietnamese', + 'vec': 'Venetian', + 'hi': 'Hindi' + } + + # Special case for Chinese: map specific variations to traditional. + # (otherwise will map to 'Chinese' which is simplified) + if self.locale in ('zh_HANT', 'zh_TW'): + language = 'ChineseTraditional' + else: + language = languages.get(self.locale[:2], 'English') + if not self._can_display_language(language): + language = 'English' + return language + + @property + def language(self) -> str: + """The name of the language the game is running in. + + This can be selected explicitly by the user or may be set + automatically based on ba.App.locale or other factors. + """ + assert isinstance(_ba.app.config, dict) + return _ba.app.config.get('Lang', self.default_language) + + @property + def available_languages(self) -> List[str]: + """A list of all available languages. + + Note that languages that may be present in game assets but which + are not displayable on the running version of the game are not + included here. + """ + langs = set() + try: + names = os.listdir('ba_data/data/languages') + names = [n.replace('.json', '').capitalize() for n in names] + + # FIXME: our simple capitalization fails on multi-word names; + # should handle this in a better way... + for i, name in enumerate(names): + if name == 'Chinesetraditional': + names[i] = 'ChineseTraditional' + except Exception: + from ba import _error + _error.print_exception() + names = [] + for name in names: + if self._can_display_language(name): + langs.add(name) + return sorted(name for name in names + if self._can_display_language(name)) + + def setlanguage(self, + language: Optional[str], + print_change: bool = True, + store_to_config: bool = True) -> None: + """Set the active language used for the game. + + Pass None to use OS default language. + """ + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + cfg = _ba.app.config + cur_language = cfg.get('Lang', None) + + # Store this in the config if its changing. + if language != cur_language and store_to_config: + if language is None: + if 'Lang' in cfg: + del cfg['Lang'] # Clear it out for default. + else: + cfg['Lang'] = language + cfg.commit() + switched = True + else: + switched = False + + with open('ba_data/data/languages/english.json') as infile: + lenglishvalues = json.loads(infile.read()) + + # None implies default. + if language is None: + language = self.default_language + try: + if language == 'English': + lmodvalues = None + else: + lmodfile = 'ba_data/data/languages/' + language.lower( + ) + '.json' + with open(lmodfile) as infile: + lmodvalues = json.loads(infile.read()) + except Exception: + from ba import _error + _error.print_exception('Exception importing language:', language) + _ba.screenmessage("Error setting language to '" + language + + "'; see log for details", + color=(1, 0, 0)) + switched = False + lmodvalues = None + + # Create an attrdict of *just* our target language. + self.language_target = AttrDict() + langtarget = self.language_target + assert langtarget is not None + _add_to_attr_dict( + langtarget, + lmodvalues if lmodvalues is not None else lenglishvalues) + + # Create an attrdict of our target language overlaid + # on our base (english). + languages = [lenglishvalues] + if lmodvalues is not None: + languages.append(lmodvalues) + lfull = AttrDict() + for lmod in languages: + _add_to_attr_dict(lfull, lmod) + self.language_merged = lfull + + # Pass some keys/values in for low level code to use; + # start with everything in their 'internal' section. + internal_vals = [ + v for v in list(lfull['internal'].items()) + if isinstance(v[1], str) + ] + + # Cherry-pick various other values to include. + # (should probably get rid of the 'internal' section + # and do everything this way) + for value in [ + 'replayNameDefaultText', 'replayWriteErrorText', + 'replayVersionErrorText', 'replayReadErrorText' + ]: + internal_vals.append((value, lfull[value])) + internal_vals.append( + ('axisText', lfull['configGamepadWindow']['axisText'])) + internal_vals.append(('buttonText', lfull['buttonText'])) + lmerged = self.language_merged + assert lmerged is not None + random_names = [ + n.strip() for n in lmerged['randomPlayerNamesText'].split(',') + ] + random_names = [n for n in random_names if n != ''] + _ba.set_internal_language_keys(internal_vals, random_names) + if switched and print_change: + _ba.screenmessage(Lstr(resource='languageSetText', + subs=[('${LANGUAGE}', + Lstr(translate=('languages', + language)))]), + color=(0, 1, 0)) + + def get_resource(self, + resource: str, + fallback_resource: str = None, + fallback_value: Any = None) -> Any: + """Return a translation resource by name. + + DEPRECATED; use ba.Lstr functionality for these purposes. + """ + try: + # If we have no language set, go ahead and set it. + if self.language_merged is None: + language = self.language + try: + self.setlanguage(language, + print_change=False, + store_to_config=False) + except Exception: + from ba import _error + _error.print_exception('exception setting language to', + language) + + # Try english as a fallback. + if language != 'English': + print('Resorting to fallback language (English)') + try: + self.setlanguage('English', + print_change=False, + store_to_config=False) + except Exception: + _error.print_exception( + 'error setting language to english fallback') + + # If they provided a fallback_resource value, try the + # target-language-only dict first and then fall back to trying the + # fallback_resource value in the merged dict. + if fallback_resource is not None: + try: + values = self.language_target + splits = resource.split('.') + dicts = splits[:-1] + key = splits[-1] + for dct in dicts: + assert values is not None + values = values[dct] + assert values is not None + val = values[key] + return val + except Exception: + # FIXME: Shouldn't we try the fallback resource in the + # merged dict AFTER we try the main resource in the + # merged dict? + try: + values = self.language_merged + splits = fallback_resource.split('.') + dicts = splits[:-1] + key = splits[-1] + for dct in dicts: + assert values is not None + values = values[dct] + assert values is not None + val = values[key] + return val + + except Exception: + # If we got nothing for fallback_resource, default + # to the normal code which checks or primary + # value in the merge dict; there's a chance we can + # get an english value for it (which we weren't + # looking for the first time through). + pass + + values = self.language_merged + splits = resource.split('.') + dicts = splits[:-1] + key = splits[-1] + for dct in dicts: + assert values is not None + values = values[dct] + assert values is not None + val = values[key] + return val + + except Exception: + # Ok, looks like we couldn't find our main or fallback resource + # anywhere. Now if we've been given a fallback value, return it; + # otherwise fail. + from ba import _error + if fallback_value is not None: + return fallback_value + raise _error.NotFoundError( + f"Resource not found: '{resource}'") from None + + def translate(self, + category: str, + strval: str, + raise_exceptions: bool = False, + print_errors: bool = False) -> str: + """Translate a value (or return the value if no translation available) + + DEPRECATED; use ba.Lstr functionality for these purposes. + """ + try: + translated = self.get_resource('translations')[category][strval] + except Exception as exc: + if raise_exceptions: + raise + if print_errors: + print(('Translate error: category=\'' + category + + '\' name=\'' + strval + '\' exc=' + str(exc) + '')) + translated = None + translated_out: str + if translated is None: + translated_out = strval + else: + translated_out = translated + assert isinstance(translated_out, str) + return translated_out + + def is_custom_unicode_char(self, char: str) -> bool: + """Return whether a char is in the custom unicode range we use.""" + assert isinstance(char, str) + if len(char) != 1: + raise ValueError('Invalid Input; must be length 1') + return 0xE000 <= ord(char) <= 0xF8FF + + +class Lstr: + """Used to define strings in a language-independent way. + + category: General Utility Classes + + These should be used whenever possible in place of hard-coded strings + so that in-game or UI elements show up correctly on all clients in their + currently-active language. + + To see available resource keys, look at any of the bs_language_*.py files + in the game or the translations pages at bombsquadgame.com/translate. + + # EXAMPLE 1: specify a string from a resource path + mynode.text = ba.Lstr(resource='audioSettingsWindow.titleText') + + # EXAMPLE 2: specify a translated string via a category and english value; + # if a translated value is available, it will be used; otherwise the + # english value will be. To see available translation categories, look + # under the 'translations' resource section. + mynode.text = ba.Lstr(translate=('gameDescriptions', 'Defeat all enemies')) + + # EXAMPLE 3: specify a raw value and some substitutions. Substitutions can + # be used with resource and translate modes as well. + mynode.text = ba.Lstr(value='${A} / ${B}', + subs=[('${A}', str(score)), ('${B}', str(total))]) + + # EXAMPLE 4: Lstrs can be nested. This example would display the resource + # at res_a but replace ${NAME} with the value of the resource at res_b + mytextnode.text = ba.Lstr(resource='res_a', + subs=[('${NAME}', ba.Lstr(resource='res_b'))]) + """ + + # pylint: disable=dangerous-default-value + # noinspection PyDefaultArgument + @overload + def __init__(self, + *, + resource: str, + fallback_resource: str = '', + fallback_value: str = '', + subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None: + """Create an Lstr from a string resource.""" + ... + + # noinspection PyShadowingNames,PyDefaultArgument + @overload + def __init__(self, + *, + translate: Tuple[str, str], + subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None: + """Create an Lstr by translating a string in a category.""" + ... + + # noinspection PyDefaultArgument + @overload + def __init__(self, + *, + value: str, + subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None: + """Create an Lstr from a raw string value.""" + ... + + # pylint: enable=redefined-outer-name, dangerous-default-value + + def __init__(self, *args: Any, **keywds: Any) -> None: + """Instantiate a Lstr. + + Pass a value for either 'resource', 'translate', + or 'value'. (see Lstr help for examples). + 'subs' can be a sequence of 2-member sequences consisting of values + and replacements. + 'fallback_resource' can be a resource key that will be used if the + main one is not present for + the current language in place of falling back to the english value + ('resource' mode only). + 'fallback_value' can be a literal string that will be used if neither + the resource nor the fallback resource is found ('resource' mode only). + """ + # pylint: disable=too-many-branches + if args: + raise TypeError('Lstr accepts only keyword arguments') + + # Basically just store the exact args they passed. + # However if they passed any Lstr values for subs, + # replace them with that Lstr's dict. + self.args = keywds + our_type = type(self) + + if isinstance(self.args.get('value'), our_type): + raise TypeError("'value' must be a regular string; not an Lstr") + + if 'subs' in self.args: + subs_new = [] + for key, value in keywds['subs']: + if isinstance(value, our_type): + subs_new.append((key, value.args)) + else: + subs_new.append((key, value)) + self.args['subs'] = subs_new + + # As of protocol 31 we support compact key names + # ('t' instead of 'translate', etc). Convert as needed. + if 'translate' in keywds: + keywds['t'] = keywds['translate'] + del keywds['translate'] + if 'resource' in keywds: + keywds['r'] = keywds['resource'] + del keywds['resource'] + if 'value' in keywds: + keywds['v'] = keywds['value'] + del keywds['value'] + if 'fallback' in keywds: + from ba import _error + _error.print_error( + 'deprecated "fallback" arg passed to Lstr(); use ' + 'either "fallback_resource" or "fallback_value"', + once=True) + keywds['f'] = keywds['fallback'] + del keywds['fallback'] + if 'fallback_resource' in keywds: + keywds['f'] = keywds['fallback_resource'] + del keywds['fallback_resource'] + if 'subs' in keywds: + keywds['s'] = keywds['subs'] + del keywds['subs'] + if 'fallback_value' in keywds: + keywds['fv'] = keywds['fallback_value'] + del keywds['fallback_value'] + + def evaluate(self) -> str: + """Evaluate the Lstr and returns a flat string in the current language. + + You should avoid doing this as much as possible and instead pass + and store Lstr values. + """ + return _ba.evaluate_lstr(self._get_json()) + + def is_flat_value(self) -> bool: + """Return whether the Lstr is a 'flat' value. + + This is defined as a simple string value incorporating no translations, + resources, or substitutions. In this case it may be reasonable to + replace it with a raw string value, perform string manipulation on it, + etc. + """ + return bool('v' in self.args and not self.args.get('s', [])) + + def _get_json(self) -> str: + try: + return json.dumps(self.args, separators=(',', ':')) + except Exception: + from ba import _error + _error.print_exception('_get_json failed for', self.args) + return 'JSON_ERR' + + def __str__(self) -> str: + return '' + + def __repr__(self) -> str: + return '' + + @staticmethod + def from_json(json_string: str) -> ba.Lstr: + """Given a json string, returns a ba.Lstr. Does no data validation.""" + lstr = Lstr(value='') + lstr.args = json.loads(json_string) + return lstr + + +def _add_to_attr_dict(dst: AttrDict, src: Dict) -> None: + for key, value in list(src.items()): + if isinstance(value, dict): + try: + dst_dict = dst[key] + except Exception: + dst_dict = dst[key] = AttrDict() + if not isinstance(dst_dict, AttrDict): + raise RuntimeError("language key '" + key + + "' is defined both as a dict and value") + _add_to_attr_dict(dst_dict, value) + else: + if not isinstance(value, (float, int, bool, str, str, type(None))): + raise TypeError("invalid value type for res '" + key + "': " + + str(type(value))) + dst[key] = value + + +class AttrDict(dict): + """A dict that can be accessed with dot notation. + + (so foo.bar is equivalent to foo['bar']) + """ + + def __getattr__(self, attr: str) -> Any: + val = self[attr] + assert not isinstance(val, bytes) + return val + + def __setattr__(self, attr: str, value: Any) -> None: + raise Exception() diff --git a/assets/src/ba_data/python/ba/_level.py b/assets/src/ba_data/python/ba/_level.py index b9bd1675..c45f7156 100644 --- a/assets/src/ba_data/python/ba/_level.py +++ b/assets/src/ba_data/python/ba/_level.py @@ -62,8 +62,8 @@ class Level: @property def displayname(self) -> ba.Lstr: """The localized name for this Level.""" - from ba import _lang - return _lang.Lstr( + from ba import _language + return _language.Lstr( translate=('coopLevelNames', self._displayname if self._displayname is not None else self._name), subs=[('${GAME}', diff --git a/assets/src/ba_data/python/ba/_lobby.py b/assets/src/ba_data/python/ba/_lobby.py index d96206c6..338d5121 100644 --- a/assets/src/ba_data/python/ba/_lobby.py +++ b/assets/src/ba_data/python/ba/_lobby.py @@ -11,7 +11,7 @@ from typing import TYPE_CHECKING import _ba from ba._error import print_exception, print_error, NotFoundError from ba._gameutils import animate, animate_array -from ba._lang import Lstr +from ba._language import Lstr from ba._enums import SpecialChar, InputType from ba._profile import get_player_profile_colors diff --git a/assets/src/ba_data/python/ba/_map.py b/assets/src/ba_data/python/ba/_map.py index bae3805b..3bd4c55d 100644 --- a/assets/src/ba_data/python/ba/_map.py +++ b/assets/src/ba_data/python/ba/_map.py @@ -48,8 +48,8 @@ def get_map_display_string(name: str) -> ba.Lstr: Category: Asset Functions """ - from ba import _lang - return _lang.Lstr(translate=('mapsNames', name)) + from ba import _language + return _language.Lstr(translate=('mapsNames', name)) def getmaps(playtype: str) -> List[str]: diff --git a/assets/src/ba_data/python/ba/_meta.py b/assets/src/ba_data/python/ba/_meta.py index 40c50187..7b314d68 100644 --- a/assets/src/ba_data/python/ba/_meta.py +++ b/assets/src/ba_data/python/ba/_meta.py @@ -48,7 +48,7 @@ def start_scan() -> None: def handle_scan_results(results: ScanResults) -> None: """Called in the game thread with results of a completed scan.""" - from ba._lang import Lstr + from ba._language import Lstr from ba._plugin import PotentialPlugin # Warnings generally only get printed locally for users' benefit diff --git a/assets/src/ba_data/python/ba/_multiteamsession.py b/assets/src/ba_data/python/ba/_multiteamsession.py index 7884faa7..910b72e1 100644 --- a/assets/src/ba_data/python/ba/_multiteamsession.py +++ b/assets/src/ba_data/python/ba/_multiteamsession.py @@ -238,7 +238,7 @@ class MultiTeamSession(Session): from ba._math import normalized_color from ba._general import Call from ba._gameutils import cameraflash - from ba._lang import Lstr + from ba._language import Lstr from ba._freeforallsession import FreeForAllSession from ba._messages import CelebrateMessage _ba.timer(delay, Call(_ba.playsound, _ba.getsound('boxingBell'))) diff --git a/assets/src/ba_data/python/ba/_music.py b/assets/src/ba_data/python/ba/_music.py index b60684a6..fda0630f 100644 --- a/assets/src/ba_data/python/ba/_music.py +++ b/assets/src/ba_data/python/ba/_music.py @@ -120,6 +120,8 @@ class MusicSubsystem: """Subsystem for music playback in the app. Category: App Classes + + To use this class, access the single instance of it at 'ba.app.music'. """ def __init__(self) -> None: diff --git a/assets/src/ba_data/python/ba/_player.py b/assets/src/ba_data/python/ba/_player.py index e3fada9f..f6ed2f1f 100644 --- a/assets/src/ba_data/python/ba/_player.py +++ b/assets/src/ba_data/python/ba/_player.py @@ -291,7 +291,7 @@ class EmptyPlayer(Player['ba.EmptyTeam']): defining a ba.Activity that does not need custom types of its own. Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, - so if you want to define your own class for one of them you must do so + so if you want to define your own class for one of them you should do so for both. """ diff --git a/assets/src/ba_data/python/ba/_servermode.py b/assets/src/ba_data/python/ba/_servermode.py index 58798e9a..75e11b61 100644 --- a/assets/src/ba_data/python/ba/_servermode.py +++ b/assets/src/ba_data/python/ba/_servermode.py @@ -166,7 +166,7 @@ class ServerController: return False def _execute_shutdown(self) -> None: - from ba._lang import Lstr + from ba._language import Lstr if self._executing_shutdown: return self._executing_shutdown = True diff --git a/assets/src/ba_data/python/ba/_session.py b/assets/src/ba_data/python/ba/_session.py index 39cca9a7..72b82c6f 100644 --- a/assets/src/ba_data/python/ba/_session.py +++ b/assets/src/ba_data/python/ba/_session.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING import _ba from ba._error import print_error, print_exception, NodeNotFoundError -from ba._lang import Lstr +from ba._language import Lstr from ba._player import Player if TYPE_CHECKING: diff --git a/assets/src/ba_data/python/ba/_stats.py b/assets/src/ba_data/python/ba/_stats.py index 47a7ec79..bf3becb0 100644 --- a/assets/src/ba_data/python/ba/_stats.py +++ b/assets/src/ba_data/python/ba/_stats.py @@ -131,7 +131,7 @@ class PlayerRecord: """Submit a kill for this player entry.""" # FIXME Clean this up. # pylint: disable=too-many-statements - from ba._lang import Lstr + from ba._language import Lstr from ba._general import Call self._multi_kill_count += 1 stats = self._stats() @@ -341,7 +341,7 @@ class Stats: from bastd.actor.popuptext import PopupText from ba import _math from ba._gameactivity import GameActivity - from ba._lang import Lstr + from ba._language import Lstr del victim_player # Currently unused. name = player.getname() s_player = self._player_records[name] @@ -428,7 +428,7 @@ class Stats: killed: bool = False, killer: ba.Player = None) -> None: """Should be called when a player is killed.""" - from ba._lang import Lstr + from ba._language import Lstr name = player.getname() prec = self._player_records[name] prec.streak = 0 diff --git a/assets/src/ba_data/python/ba/_store.py b/assets/src/ba_data/python/ba/_store.py index 9bf0ec38..cd9f16ae 100644 --- a/assets/src/ba_data/python/ba/_store.py +++ b/assets/src/ba_data/python/ba/_store.py @@ -21,15 +21,16 @@ def get_store_item(item: str) -> Dict[str, Any]: def get_store_item_name_translated(item_name: str) -> ba.Lstr: """Return a ba.Lstr for a store item name.""" # pylint: disable=cyclic-import - from ba import _lang + from ba import _language from ba import _map item_info = get_store_item(item_name) if item_name.startswith('characters.'): - return _lang.Lstr(translate=('characterNames', item_info['character'])) + return _language.Lstr(translate=('characterNames', + item_info['character'])) if item_name in ['upgrades.pro', 'pro']: - return _lang.Lstr(resource='store.bombSquadProNameText', - subs=[('${APP_NAME}', - _lang.Lstr(resource='titleText'))]) + return _language.Lstr(resource='store.bombSquadProNameText', + subs=[('${APP_NAME}', + _language.Lstr(resource='titleText'))]) if item_name.startswith('maps.'): map_type: Type[ba.Map] = item_info['map_type'] return _map.get_map_display_string(map_type.name) @@ -37,7 +38,7 @@ def get_store_item_name_translated(item_name: str) -> ba.Lstr: gametype: Type[ba.GameActivity] = item_info['gametype'] return gametype.get_display_string() if item_name.startswith('icons.'): - return _lang.Lstr(resource='editProfileWindow.iconText') + return _language.Lstr(resource='editProfileWindow.iconText') raise ValueError('unrecognized item: ' + item_name) diff --git a/assets/src/ba_data/python/ba/_team.py b/assets/src/ba_data/python/ba/_team.py index e4f74704..6fa91d50 100644 --- a/assets/src/ba_data/python/ba/_team.py +++ b/assets/src/ba_data/python/ba/_team.py @@ -207,6 +207,6 @@ class EmptyTeam(Team['ba.EmptyPlayer']): defining a ba.Activity that does not need custom types of its own. Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, - so if you want to define your own class for one of them you must do so + so if you want to define your own class for one of them you should do so for both. """ diff --git a/assets/src/ba_data/python/ba/_ui.py b/assets/src/ba_data/python/ba/_ui.py index ac7dfaeb..0756ad4f 100644 --- a/assets/src/ba_data/python/ba/_ui.py +++ b/assets/src/ba_data/python/ba/_ui.py @@ -15,8 +15,13 @@ if TYPE_CHECKING: import ba -class UI: - """UI subsystem for the app.""" +class UISubsystem: + """Consolidated UI functionality for the app. + + Category: App Classes + + To use this class, access the single instance of it at 'ba.app.ui'. + """ def __init__(self) -> None: env = _ba.env() diff --git a/assets/src/ba_data/python/ba/deprecated.py b/assets/src/ba_data/python/ba/deprecated.py index 253c91f3..de3ff2aa 100644 --- a/assets/src/ba_data/python/ba/deprecated.py +++ b/assets/src/ba_data/python/ba/deprecated.py @@ -6,8 +6,3 @@ Classes or functions can be relocated here when they are deprecated. Any code using them should migrate to alternative methods, as deprecated items will eventually be fully removed. """ - -# pylint: disable=unused-import - -# The Lstr class should be used for all string resources. -from ba._lang import get_resource, translate diff --git a/assets/src/ba_data/python/ba/macmusicapp.py b/assets/src/ba_data/python/ba/macmusicapp.py index 68c6ebef..c8a64d75 100644 --- a/assets/src/ba_data/python/ba/macmusicapp.py +++ b/assets/src/ba_data/python/ba/macmusicapp.py @@ -68,7 +68,7 @@ class _MacMusicAppThread(threading.Thread): def run(self) -> None: """Run the Music.app thread.""" from ba._general import Call - from ba._lang import Lstr + from ba._language import Lstr from ba._enums import TimeType _ba.set_thread_name('BA_MacMusicAppThread') _ba.mac_music_app_init() @@ -212,7 +212,6 @@ class _MacMusicAppThread(threading.Thread): def _play_current_playlist(self) -> None: try: - from ba import _lang from ba._general import Call assert self._current_playlist is not None if _ba.mac_music_app_play_playlist(self._current_playlist): @@ -220,8 +219,8 @@ class _MacMusicAppThread(threading.Thread): else: _ba.pushcall(Call( _ba.screenmessage, - _lang.get_resource('playlistNotFoundText') + ': \'' + - self._current_playlist + '\'', (1, 0, 0)), + _ba.app.lang.get_resource('playlistNotFoundText') + + ': \'' + self._current_playlist + '\'', (1, 0, 0)), from_other_thread=True) except Exception: from ba import _error diff --git a/assets/src/ba_data/python/ba/modutils.py b/assets/src/ba_data/python/ba/modutils.py index 478d3df9..904711f8 100644 --- a/assets/src/ba_data/python/ba/modutils.py +++ b/assets/src/ba_data/python/ba/modutils.py @@ -17,7 +17,7 @@ def get_human_readable_user_scripts_path() -> str: This is NOT a valid filesystem path; may be something like "(SD Card)". """ - from ba import _lang + from ba import _language app = _ba.app path: Optional[str] = app.python_directory_user if path is None: @@ -32,14 +32,14 @@ def get_human_readable_user_scripts_path() -> str: if (ext_storage_path is not None and app.python_directory_user.startswith(ext_storage_path)): path = ('<' + - _lang.Lstr(resource='externalStorageText').evaluate() + + _language.Lstr(resource='externalStorageText').evaluate() + '>' + app.python_directory_user[len(ext_storage_path):]) return path def _request_storage_permission() -> bool: """If needed, requests storage permission from the user (& return true).""" - from ba._lang import Lstr + from ba._language import Lstr from ba._enums import Permission if not _ba.have_permission(Permission.STORAGE): _ba.playsound(_ba.getsound('error')) diff --git a/assets/src/ba_data/python/ba/osmusic.py b/assets/src/ba_data/python/ba/osmusic.py index aaceba92..5257e0aa 100644 --- a/assets/src/ba_data/python/ba/osmusic.py +++ b/assets/src/ba_data/python/ba/osmusic.py @@ -62,9 +62,9 @@ class OSMusicPlayer(MusicPlayer): def _on_play_folder_cb(self, result: Union[str, List[str]], error: Optional[str] = None) -> None: - from ba import _lang + from ba import _language if error is not None: - rstr = (_lang.Lstr( + rstr = (_language.Lstr( resource='internal.errorPlayingMusicText').evaluate()) if isinstance(result, str): err_str = (rstr.replace('${MUSIC}', os.path.basename(result)) + @@ -103,7 +103,7 @@ class _PickFolderSongThread(threading.Thread): self._path = path def run(self) -> None: - from ba import _lang + from ba import _language from ba._general import Call do_print_error = True try: @@ -119,8 +119,8 @@ class _PickFolderSongThread(threading.Thread): if not all_files: do_print_error = False raise RuntimeError( - _lang.Lstr(resource='internal.noMusicFilesInFolderText'). - evaluate()) + _language.Lstr(resource='internal.noMusicFilesInFolderText' + ).evaluate()) _ba.pushcall(Call(self._callback, all_files, None), from_other_thread=True) except Exception as exc: diff --git a/assets/src/ba_data/python/bastd/activity/dualteamscore.py b/assets/src/ba_data/python/bastd/activity/dualteamscore.py index 88438bd3..b031bb80 100644 --- a/assets/src/ba_data/python/bastd/activity/dualteamscore.py +++ b/assets/src/ba_data/python/bastd/activity/dualteamscore.py @@ -23,7 +23,6 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): assert isinstance(self._winner, ba.SessionTeam) def on_begin(self) -> None: - from ba.deprecated import get_resource ba.set_analytics_screen('Teams Score Screen') super().on_begin() @@ -37,7 +36,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): # 'First to 4'. session = self.session assert isinstance(session, ba.MultiTeamSession) - if get_resource('bestOfUseFirstToInstead'): + if ba.app.lang.get_resource('bestOfUseFirstToInstead'): best_txt = ba.Lstr(resource='firstToSeriesText', subs=[('${COUNT}', str(session.get_series_length() / 2 + 1)) diff --git a/assets/src/ba_data/python/bastd/activity/multiteamvictory.py b/assets/src/ba_data/python/bastd/activity/multiteamvictory.py index d8058806..9acc7846 100644 --- a/assets/src/ba_data/python/bastd/activity/multiteamvictory.py +++ b/assets/src/ba_data/python/bastd/activity/multiteamvictory.py @@ -33,7 +33,6 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): # pylint: disable=too-many-statements from bastd.actor.text import Text from bastd.actor.image import Image - from ba.deprecated import get_resource ba.set_analytics_screen('FreeForAll Series Victory Screen' if self. _is_ffa else 'Teams Series Victory Screen') if ba.app.ui.uiscale is ba.UIScale.LARGE: @@ -72,7 +71,8 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): tval = 6.4 t_incr = 0.12 - always_use_first_to = get_resource('bestOfUseFirstToInstead') + always_use_first_to = ba.app.lang.get_resource( + 'bestOfUseFirstToInstead') session = self.session if self._is_ffa: diff --git a/assets/src/ba_data/python/bastd/mainmenu.py b/assets/src/ba_data/python/bastd/mainmenu.py index 7a976816..44808147 100644 --- a/assets/src/ba_data/python/bastd/mainmenu.py +++ b/assets/src/ba_data/python/bastd/mainmenu.py @@ -492,7 +492,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): ba.getmodel('logoTransparent')) # If language has changed, recreate our logo text/graphics. - lang = app.language + lang = app.lang.language if lang != self._language: self._language = lang y = 20 @@ -511,7 +511,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # We draw higher in kiosk mode (make sure to test this # when making adjustments) for now we're hard-coded for # a few languages.. should maybe look into generalizing this?.. - if app.language == 'Chinese': + if app.lang.language == 'Chinese': base_x = -270.0 x = base_x - 20.0 spacing = 85.0 * base_scale diff --git a/assets/src/ba_data/python/bastd/ui/gather.py b/assets/src/ba_data/python/bastd/ui/gather.py index bb67b9a4..f3ccad20 100644 --- a/assets/src/ba_data/python/bastd/ui/gather.py +++ b/assets/src/ba_data/python/bastd/ui/gather.py @@ -1575,7 +1575,7 @@ class GatherWindow(ba.Window): { 'type': 'PUBLIC_PARTY_QUERY', 'proto': app.protocol_version, - 'lang': app.language + 'lang': app.lang.language }, callback=ba.WeakCall(self._on_public_party_query_result)) _ba.run_transactions() diff --git a/assets/src/ba_data/python/bastd/ui/helpui.py b/assets/src/ba_data/python/bastd/ui/helpui.py index 82b95d62..b49e51c8 100644 --- a/assets/src/ba_data/python/bastd/ui/helpui.py +++ b/assets/src/ba_data/python/bastd/ui/helpui.py @@ -22,7 +22,6 @@ class HelpWindow(ba.Window): # pylint: disable=too-many-statements # pylint: disable=too-many-locals from ba.internal import get_remote_app_name - from ba.deprecated import get_resource ba.set_analytics_screen('Help Window') # If they provided an origin-widget, scale up from that. @@ -38,6 +37,8 @@ class HelpWindow(ba.Window): self._r = 'helpWindow' + getres = ba.app.lang.get_resource + self._main_menu = main_menu uiscale = ba.app.ui.uiscale width = 950 if uiscale is ba.UIScale.SMALL else 750 @@ -111,8 +112,8 @@ class HelpWindow(ba.Window): label=ba.charstr(ba.SpecialChar.BACK)) self._sub_width = 660 - self._sub_height = 1590 + get_resource( - self._r + '.someDaysExtraSpace') + get_resource( + self._sub_height = 1590 + ba.app.lang.get_resource( + self._r + '.someDaysExtraSpace') + ba.app.lang.get_resource( self._r + '.orPunchingSomethingExtraSpace') self._subcontainer = ba.containerwidget(parent=self._scrollwidget, @@ -212,8 +213,7 @@ class HelpWindow(ba.Window): color=paragraph, v_align='center', flatness=1.0) - v -= (spacing * 25.0 + - get_resource(self._r + '.someDaysExtraSpace')) + v -= (spacing * 25.0 + getres(self._r + '.someDaysExtraSpace')) txt_scale = 0.66 txt = ba.Lstr(resource=self._r + '.orPunchingSomethingText').evaluate() @@ -228,7 +228,7 @@ class HelpWindow(ba.Window): v_align='center', flatness=1.0) v -= (spacing * 27.0 + - get_resource(self._r + '.orPunchingSomethingExtraSpace')) + getres(self._r + '.orPunchingSomethingExtraSpace')) txt_scale = 1.0 txt = ba.Lstr(resource=self._r + '.canHelpText', subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) @@ -387,7 +387,7 @@ class HelpWindow(ba.Window): texture=ba.gettexture('buttonPunch'), color=(1, 0.7, 0.3)) - txt_scale = get_resource(self._r + '.punchInfoTextScale') + txt_scale = getres(self._r + '.punchInfoTextScale') txt = ba.Lstr(resource=self._r + '.punchInfoText').evaluate() ba.textwidget(parent=self._subcontainer, position=(h - sep - 185 + 70, v + 120), @@ -409,7 +409,7 @@ class HelpWindow(ba.Window): color=(1, 0.3, 0.3)) txt = ba.Lstr(resource=self._r + '.bombInfoText').evaluate() - txt_scale = get_resource(self._r + '.bombInfoTextScale') + txt_scale = getres(self._r + '.bombInfoTextScale') ba.textwidget(parent=self._subcontainer, position=(h + sep + 50 + 60, v - 35), size=(0, 0), @@ -431,7 +431,7 @@ class HelpWindow(ba.Window): color=(0.5, 0.5, 1)) txtl = ba.Lstr(resource=self._r + '.pickUpInfoText') - txt_scale = get_resource(self._r + '.pickUpInfoTextScale') + txt_scale = getres(self._r + '.pickUpInfoTextScale') ba.textwidget(parent=self._subcontainer, position=(h + 60 + 120, v + sep + 50), size=(0, 0), @@ -452,7 +452,7 @@ class HelpWindow(ba.Window): color=(0.4, 1, 0.4)) txt = ba.Lstr(resource=self._r + '.jumpInfoText').evaluate() - txt_scale = get_resource(self._r + '.jumpInfoTextScale') + txt_scale = getres(self._r + '.jumpInfoTextScale') ba.textwidget(parent=self._subcontainer, position=(h - 250 + 75, v - sep - 15 + 30), size=(0, 0), @@ -464,7 +464,7 @@ class HelpWindow(ba.Window): v_align='top') txt = ba.Lstr(resource=self._r + '.runInfoText').evaluate() - txt_scale = get_resource(self._r + '.runInfoTextScale') + txt_scale = getres(self._r + '.runInfoTextScale') ba.textwidget(parent=self._subcontainer, position=(h, v - sep - 100), size=(0, 0), @@ -503,7 +503,7 @@ class HelpWindow(ba.Window): texture=logo_tex) v -= spacing * 50.0 - txt_scale = get_resource(self._r + '.powerupsSubtitleTextScale') + txt_scale = getres(self._r + '.powerupsSubtitleTextScale') txt = ba.Lstr(resource=self._r + '.powerupsSubtitleText').evaluate() ba.textwidget(parent=self._subcontainer, position=(h, v), diff --git a/assets/src/ba_data/python/bastd/ui/league/rankwindow.py b/assets/src/ba_data/python/bastd/ui/league/rankwindow.py index a998c71c..43df0935 100644 --- a/assets/src/ba_data/python/bastd/ui/league/rankwindow.py +++ b/assets/src/ba_data/python/bastd/ui/league/rankwindow.py @@ -22,9 +22,7 @@ class LeagueRankWindow(ba.Window): transition: str = 'in_right', modal: bool = False, origin_widget: ba.Widget = None): - # pylint: disable=too-many-statements from ba.internal import get_cached_league_rank_data - from ba.deprecated import get_resource ba.set_analytics_screen('League Rank Window') self._league_rank_data: Optional[Dict[str, Any]] = None @@ -46,7 +44,7 @@ class LeagueRankWindow(ba.Window): self._height = (657 if uiscale is ba.UIScale.SMALL else 710 if uiscale is ba.UIScale.MEDIUM else 800) self._r = 'coopSelectWindow' - self._rdict = get_resource(self._r) + self._rdict = ba.app.lang.get_resource(self._r) top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 self._league_url_arg = '' diff --git a/assets/src/ba_data/python/bastd/ui/settings/advanced.py b/assets/src/ba_data/python/bastd/ui/settings/advanced.py index 062c96c2..dbac1553 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/advanced.py +++ b/assets/src/ba_data/python/bastd/ui/settings/advanced.py @@ -164,10 +164,10 @@ class AdvancedSettingsWindow(ba.Window): def _update_lang_status(self) -> None: if self._complete_langs_list is not None: - up_to_date = (ba.app.language in self._complete_langs_list) + up_to_date = (ba.app.lang.language in self._complete_langs_list) ba.textwidget( edit=self._lang_status_text, - text='' if ba.app.language == 'Test' else ba.Lstr( + text='' if ba.app.lang.language == 'Test' else ba.Lstr( resource=self._r + '.translationNoUpdateNeededText') if up_to_date else ba.Lstr(resource=self._r + '.translationUpdateNeededText'), @@ -189,6 +189,8 @@ class AdvancedSettingsWindow(ba.Window): from bastd.ui.config import ConfigCheckBox from ba.modutils import show_user_scripts + available_languages = ba.app.lang.available_languages + # Don't rebuild if the menu is open or if our language and # language-list hasn't changed. # NOTE - although we now support widgets updating their own @@ -196,12 +198,11 @@ class AdvancedSettingsWindow(ba.Window): # menu based on the language so still need this. ...however we could # make this more limited to it only rebuilds that one menu instead # of everything. - if self._menu_open or ( - self._prev_lang == _ba.app.config.get('Lang', None) - and self._prev_lang_list == ba.get_valid_languages()): + if self._menu_open or (self._prev_lang == _ba.app.config.get( + 'Lang', None) and self._prev_lang_list == available_languages): return self._prev_lang = _ba.app.config.get('Lang', None) - self._prev_lang_list = ba.get_valid_languages() + self._prev_lang_list = available_languages # Clear out our sub-container. children = self._subcontainer.get_children() @@ -248,7 +249,7 @@ class AdvancedSettingsWindow(ba.Window): h_align='right', v_align='center') - languages = ba.get_valid_languages() + languages = _ba.app.lang.available_languages cur_lang = _ba.app.config.get('Lang', None) if cur_lang is None: cur_lang = 'Auto' @@ -289,9 +290,9 @@ class AdvancedSettingsWindow(ba.Window): button_size=(250, 60), choices_display=([ ba.Lstr(value=(ba.Lstr(resource='autoText').evaluate() + ' (' + - ba.Lstr(translate=( - 'languages', - ba.app.default_language)).evaluate() + ')')) + ba.Lstr(translate=('languages', + ba.app.lang.default_language + )).evaluate() + ')')) ] + [ba.Lstr(value=langs_full[l]) for l in languages]), current_choice=cur_lang) @@ -689,7 +690,7 @@ class AdvancedSettingsWindow(ba.Window): self._menu_open = False def _on_menu_choice(self, choice: str) -> None: - ba.setlanguage(None if choice == 'Auto' else choice) + ba.app.lang.setlanguage(None if choice == 'Auto' else choice) self._save_state() ba.timer(0.1, ba.WeakCall(self._rebuild), timetype=ba.TimeType.REAL) diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/edit.py b/assets/src/ba_data/python/bastd/ui/soundtrack/edit.py index 238d3716..e264d115 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/edit.py +++ b/assets/src/ba_data/python/bastd/ui/soundtrack/edit.py @@ -157,7 +157,6 @@ class SoundtrackEditWindow(ba.Window): ba.widget(edit=cancel_button, down_widget=self._text_field) def _refresh(self) -> None: - from ba.deprecated import get_resource for widget in self._col.get_children(): widget.delete() @@ -183,8 +182,9 @@ class SoundtrackEditWindow(ba.Window): 'Scores', 'Victory', ] + # FIXME: We should probably convert this to use translations. - type_names_translated = get_resource('soundtrackTypeNames') + type_names_translated = ba.app.lang.get_resource('soundtrackTypeNames') prev_type_button: Optional[ba.Widget] = None prev_test_button: Optional[ba.Widget] = None diff --git a/assets/src/ba_data/python/bastd/ui/trophies.py b/assets/src/ba_data/python/bastd/ui/trophies.py index 647912e2..4763172c 100644 --- a/assets/src/ba_data/python/bastd/ui/trophies.py +++ b/assets/src/ba_data/python/bastd/ui/trophies.py @@ -20,7 +20,6 @@ class TrophiesWindow(popup.PopupWindow): position: Tuple[float, float], data: Dict[str, Any], scale: float = None): - from ba.deprecated import get_resource self._data = data uiscale = ba.app.ui.uiscale if scale is None: @@ -76,7 +75,9 @@ class TrophiesWindow(popup.PopupWindow): trophy_types = [['0a'], ['0b'], ['1'], ['2'], ['3'], ['4']] sub_height = 40 + len(trophy_types) * incr - eq_text = get_resource('coopSelectWindow.powerRankingPointsEqualsText') + eq_text = ba.Lstr( + resource='coopSelectWindow.powerRankingPointsEqualsText').evaluate( + ) self._subcontainer = ba.containerwidget(parent=self._scrollwidget, size=(sub_width, sub_height), @@ -84,25 +85,27 @@ class TrophiesWindow(popup.PopupWindow): total_pts = 0 - multi_txt = get_resource('coopSelectWindow.powerRankingPointsMultText') + multi_txt = ba.Lstr( + resource='coopSelectWindow.powerRankingPointsMultText').evaluate() total_pts += self._create_trophy_type_widgets(eq_text, incr, multi_txt, sub_height, sub_width, trophy_types) - ba.textwidget(parent=self._subcontainer, - position=(sub_width * 1.0, - sub_height - 20 - incr * len(trophy_types)), - maxwidth=sub_width * 0.5, - scale=0.7, - color=(0.7, 0.8, 1.0), - flatness=1.0, - shadow=0.0, - text=get_resource('coopSelectWindow.totalText') + ' ' + - eq_text.replace('${NUMBER}', str(total_pts)), - size=(0, 0), - h_align='right', - v_align='center') + ba.textwidget( + parent=self._subcontainer, + position=(sub_width * 1.0, + sub_height - 20 - incr * len(trophy_types)), + maxwidth=sub_width * 0.5, + scale=0.7, + color=(0.7, 0.8, 1.0), + flatness=1.0, + shadow=0.0, + text=ba.Lstr(resource='coopSelectWindow.totalText').evaluate() + + ' ' + eq_text.replace('${NUMBER}', str(total_pts)), + size=(0, 0), + h_align='right', + v_align='center') def _create_trophy_type_widgets(self, eq_text: str, incr: int, multi_txt: str, sub_height: int, diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index 4aaad6d6..db864bea 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -353,6 +353,7 @@ getpublicpartyenabled getpublicpartymaxsize getqrcodetexture + getres getsession getsound gettexture @@ -415,6 +416,7 @@ internalformat interuptions invote + iobj iserverget iserverput isinst @@ -552,6 +554,7 @@ nitpicky nlpos nmemb + noassets nodetype nofilename noglobs diff --git a/docs/ba_module.md b/docs/ba_module.md index c75fff49..a25ce291 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-10-15 for Ballistica version 1.5.26 build 20213

+

last updated on 2020-10-15 for Ballistica version 1.5.27 build 20218

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


@@ -20,6 +20,7 @@
  • ba.NodeActor
  • ba.Chooser
  • +
  • ba.Collision
  • ba.GameResults
  • ba.GameTip
  • ba.InputDevice
  • @@ -62,6 +63,7 @@
  • ba.emitfx()
  • ba.existing()
  • ba.getactivity()
  • +
  • ba.getcollision()
  • ba.getnodes()
  • ba.getsession()
  • ba.newnode()
  • @@ -84,7 +86,6 @@

    User Interface Classes

    -

    Misc Classes

    - -

    Misc Functions

    -

    Protocols

    • ba.Existable
    • @@ -810,7 +805,7 @@ likely result in errors.

      Attributes:

      -
      api_version, build_number, config, config_file_path, debug_build, language, locale, on_tv, platform, python_directory_app, python_directory_app_site, python_directory_user, subplatform, test_build, ui_bounds, user_agent_string, version, vr_mode
      +
      api_version, build_number, config, config_file_path, debug_build, on_tv, platform, python_directory_app, python_directory_app_site, python_directory_user, subplatform, test_build, ui_bounds, user_agent_string, version, vr_mode

      api_version

      int

      @@ -849,23 +844,6 @@ likely result in errors.

      builds due to compiler optimizations being disabled and extra checks being run.

      -
      -

      language

      -

      str

      -

      The name of the language the game is running in.

      - -

      This can be selected explicitly by the user or may be set - automatically based on ba.App.locale or other factors.

      - -
      -

      locale

      -

      str

      -

      Raw country/language code detected by the game (such as 'en_US').

      - -

      Generally for language-specific code you should look at - ba.App.language, which is the language the game is using - (which may differ from locale if the user sets a language, etc.)

      -

      on_tv

      bool

      @@ -1441,6 +1419,9 @@ mycall()

      A class providing info about occurring collisions.

      +

      Category: Gameplay Classes +

      +

      Attributes:

      opposingbody, opposingnode, position, sourcenode
      @@ -2119,7 +2100,7 @@ its time with lingering corpses, sound effects, etc.

      defining a ba.Activity that does not need custom types of its own.

      Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, - so if you want to define your own class for one of them you must do so + so if you want to define your own class for one of them you should do so for both.

      @@ -2182,7 +2163,7 @@ its time with lingering corpses, sound effects, etc.

      defining a ba.Activity that does not need custom types of its own.

      Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, - so if you want to define your own class for one of them you must do so + so if you want to define your own class for one of them you should do so for both.

      @@ -3077,6 +3058,85 @@ prefs, etc.

      Dict[str, Tuple[str, ...]]

      Extra chars like emojis.

      +
      +
      +
      +

      ba.LanguageSubsystem

      +

      <top level class> +

      +

      Wraps up language related app functionality.

      + +

      Category: App Classes

      + +

      To use this class, access the single instance of it at 'ba.app.lang'. +

      + +

      Attributes:

      +
      available_languages, language, locale
      +
      +

      available_languages

      +

      List[str]

      +

      A list of all available languages.

      + +

      Note that languages that may be present in game assets but which + are not displayable on the running version of the game are not + included here.

      + +
      +

      language

      +

      str

      +

      The name of the language the game is running in.

      + +

      This can be selected explicitly by the user or may be set + automatically based on ba.App.locale or other factors.

      + +
      +

      locale

      +

      str

      +

      Raw country/language code detected by the game (such as 'en_US').

      + +

      Generally for language-specific code you should look at + ba.App.language, which is the language the game is using + (which may differ from locale if the user sets a language, etc.)

      + +
      +
      +

      Methods:

      +
      <constructor>, get_resource(), is_custom_unicode_char(), setlanguage(), translate()
      +
      +

      <constructor>

      +

      ba.LanguageSubsystem()

      + +
      +

      get_resource()

      +

      get_resource(self, resource: str, fallback_resource: str = None, fallback_value: Any = None) -> Any

      + +

      Return a translation resource by name.

      + +

      DEPRECATED; use ba.Lstr functionality for these purposes.

      + +
      +

      is_custom_unicode_char()

      +

      is_custom_unicode_char(self, char: str) -> bool

      + +

      Return whether a char is in the custom unicode range we use.

      + +
      +

      setlanguage()

      +

      setlanguage(self, language: Optional[str], print_change: bool = True, store_to_config: bool = True) -> None

      + +

      Set the active language used for the game.

      + +

      Pass None to use OS default language.

      + +
      +

      translate()

      +

      translate(self, category: str, strval: str, raise_exceptions: bool = False, print_errors: bool = False) -> str

      + +

      Translate a value (or return the value if no translation available)

      + +

      DEPRECATED; use ba.Lstr functionality for these purposes.

      +

      @@ -3907,6 +3967,96 @@ signify that the default soundtrack should be used..

    • TEST

    +

    ba.MusicSubsystem

    +

    <top level class> +

    +

    Subsystem for music playback in the app.

    + +

    Category: App Classes

    + +

    To use this class, access the single instance of it at 'ba.app.music'. +

    + +

    Methods:

    +
    <constructor>, do_play_music(), get_music_player(), get_soundtrack_entry_name(), get_soundtrack_entry_type(), have_music_player(), music_volume_changed(), on_app_launch(), on_app_resume(), on_app_shutdown(), set_music_play_mode(), supports_soundtrack_entry_type()
    +
    +

    <constructor>

    +

    ba.MusicSubsystem()

    + +
    +

    do_play_music()

    +

    do_play_music(self, musictype: Union[MusicType, str, None], continuous: bool = False, mode: MusicPlayMode = <MusicPlayMode.REGULAR: regular>, testsoundtrack: Dict[str, Any] = None) -> None

    + +

    Plays the requested music type/mode.

    + +

    For most cases, setmusic() is the proper call to use, which itself +calls this. Certain cases, however, such as soundtrack testing, may +require calling this directly.

    + +
    +

    get_music_player()

    +

    get_music_player(self) -> MusicPlayer

    + +

    Returns the system music player, instantiating if necessary.

    + +
    +

    get_soundtrack_entry_name()

    +

    get_soundtrack_entry_name(self, entry: Any) -> str

    + +

    Given a soundtrack entry, returns its name.

    + +
    +

    get_soundtrack_entry_type()

    +

    get_soundtrack_entry_type(self, entry: Any) -> str

    + +

    Given a soundtrack entry, returns its type, taking into +account what is supported locally.

    + +
    +

    have_music_player()

    +

    have_music_player(self) -> bool

    + +

    Returns whether a music player is present.

    + +
    +

    music_volume_changed()

    +

    music_volume_changed(self, val: float) -> None

    + +

    Should be called when changing the music volume.

    + +
    +

    on_app_launch()

    +

    on_app_launch(self) -> None

    + +

    Should be called by app on_app_launch().

    + +
    +

    on_app_resume()

    +

    on_app_resume(self) -> None

    + +

    Should be run when the app resumes from a suspended state.

    + +
    +

    on_app_shutdown()

    +

    on_app_shutdown(self) -> None

    + +

    Should be called when the app is shutting down.

    + +
    +

    set_music_play_mode()

    +

    set_music_play_mode(self, mode: MusicPlayMode, force_restart: bool = False) -> None

    + +

    Sets music play mode; used for soundtrack testing/etc.

    + +
    +

    supports_soundtrack_entry_type()

    +

    supports_soundtrack_entry_type(self, entry_type: str) -> bool

    + +

    Return whether provided soundtrack entry type is supported here.

    + +
    +
    +

    ba.MusicType

    Inherits from: enum.Enum

    Types of music available to play in-game.

    @@ -5777,6 +5927,69 @@ self.t = ba.Timer(0.3, say_it, repeat=True)
  • SMALL

  • +

    ba.UISubsystem

    +

    <top level class> +

    +

    Consolidated UI functionality for the app.

    + +

    Category: App Classes

    + +

    To use this class, access the single instance of it at 'ba.app.ui'. +

    + +

    Attributes:

    +
    +

    uiscale

    +

    ba.UIScale

    +

    Current ui scale for the app.

    + +
    +
    +

    Methods:

    +
    <constructor>, clear_main_menu_window(), get_main_menu_location(), has_main_menu_window(), on_app_launch(), set_main_menu_location(), set_main_menu_window()
    +
    +

    <constructor>

    +

    ba.UISubsystem()

    + +
    +

    clear_main_menu_window()

    +

    clear_main_menu_window(self, transition: str = None) -> None

    + +

    Clear any existing 'main' window with the provided transition.

    + +
    +

    get_main_menu_location()

    +

    get_main_menu_location(self) -> Optional[str]

    + +

    Return the current named main menu location, if any.

    + +
    +

    has_main_menu_window()

    +

    has_main_menu_window(self) -> bool

    + +

    Return whether a main menu window is present.

    + +
    +

    on_app_launch()

    +

    on_app_launch(self) -> None

    + +

    Should be run on app launch.

    + +
    +

    set_main_menu_location()

    +

    set_main_menu_location(self, location: str) -> None

    + +

    Set the location represented by the current main menu window.

    + +
    +

    set_main_menu_window()

    +

    set_main_menu_window(self, window: ba.Widget) -> None

    + +

    Set the current 'main' window, replacing any existing.

    + +
    +
    +

    ba.Vec3

    <top level class>

    @@ -6259,17 +6472,6 @@ method) and will convert it to a None value if it does not exist. For more info, see notes on 'existables' here: https://ballistica.net/wiki/Coding-Style-Guide

    -
    -

    ba.get_valid_languages()

    -

    get_valid_languages() -> List[str]

    - -

    Return a list containing names of all available languages.

    - -

    Category: General Utility Functions

    - -

    Languages that may be present but are not displayable on the running -version of the game are ignored.

    -

    ba.getactivity()

    getactivity(doraise: bool = True) -> <varies>

    @@ -6317,6 +6519,8 @@ in the background if necessary.

    Return the in-progress collision.

    +

    Category: Gameplay Functions

    +

    ba.getmaps()

    getmaps(playtype: str) -> List[str]

    @@ -6732,16 +6936,6 @@ are applied to the Widget.

    'screen' should be a string description of an app location ('Main Menu', etc.)

    -
    -

    ba.setlanguage()

    -

    setlanguage(language: Optional[str], print_change: bool = True, store_to_config: bool = True) -> None

    - -

    Set the active language used for the game.

    - -

    Category: General Utility Functions

    - -

    Pass None to use OS default language.

    -

    ba.setmusic()

    setmusic(musictype: Optional[MusicType], continuous: bool = False) -> None

    diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index 86a2b794..23ffe9c2 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -29,8 +29,8 @@ namespace ballistica { // These are set automatically via script; don't change here. -const int kAppBuildNumber = 20216; -const char* kAppVersion = "1.5.26"; +const int kAppBuildNumber = 20218; +const char* kAppVersion = "1.5.27"; // Our standalone globals. // These are separated out for easy access. diff --git a/src/generated_src/ballistica/binding.py b/src/generated_src/ballistica/binding.py index 8fd60e69..17d2c7f6 100644 --- a/src/generated_src/ballistica/binding.py +++ b/src/generated_src/ballistica/binding.py @@ -10,7 +10,7 @@ def get_binding_values() -> object: import json import copy import ba - from ba import _lang + from ba import _language from ba import _music from ba import _input from ba import _apputils @@ -99,8 +99,8 @@ def get_binding_values() -> object: party.handle_party_invite, # kHandlePartyInviteCall _music.do_play_music, # kDoPlayMusicCall ba.app.handle_deep_link, # kDeepLinkCall - _lang.get_resource, # kGetResourceCall - _lang.translate, # kTranslateCall + ba.app.lang.get_resource, # kGetResourceCall + ba.app.lang.translate, # kTranslateCall ba.Lstr, # kLStrClass ba.Call, # kCallClass _apputils.garbage_collect, # kGarbageCollectCall @@ -122,5 +122,5 @@ def get_binding_values() -> object: _enums.SpecialChar, # kSpecialCharClass _player.Player, # kPlayerClass _hooks.get_player_icon, # kGetPlayerIconCall - _lang.Lstr.from_json, # kLstrFromJsonCall + _language.Lstr.from_json, # kLstrFromJsonCall ) # yapf: disable