diff --git a/.efrocachemap b/.efrocachemap
index 4583763f..2a75284a 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/dd/d1/e12c5331256ecffcd7684d6d2963",
- "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c5/35/8d811eec1a47f4e70d4b27eb3bf5",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6f/43/d0f61fb34a76e11b9ebd4334038d",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a4/56/614e09d8ab86355f0d65c1ce76fe",
- "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f7/c5/f695953295c2d79dfb01555aea89",
- "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7b/19/ff1bba3a148b6e0f0823d7d7c7bf",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f7/dd/d5e4d872192060d46821d6911c13",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/64/58/962c2e707ee66d310848f375796a",
- "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/db/58/65c487facba1de6ba8ce65f19f01",
- "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/40/cb/afd71350fdc9827ca99baab73001",
- "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/f2/05/e9252b68a96ee418da35dd4d2530",
- "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/bb/a2/e1f5b3f561a08bb09cc3ffb2492f",
+ "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/81/be/dd6f786f096520a5450b96b0efd9",
+ "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b0/14/defa61c54179e1810f8308cac55c",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/89/2d/9c41cd7bba801f79906a397f9b23",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/78/bb/4656448316b4b5ff7bc348ede9f2",
+ "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c0/e0/54ff7942beddb65315194b79da09",
+ "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/5b/52/1771901e2344bad49238986c80b2",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2d/d7/e1ffb234f71e715587456fc7cfd8",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/99/88/2ee8c18b63f64425516050c8a304",
+ "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ac/a2/75427a8ecc1f19d402b58f9014e6",
+ "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d7/d4/4640f90f0ae2d2b98b8fbf099c2f",
+ "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/95/94/a110e7c8d338d5a6cefe2e975e8b",
+ "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/51/26/638d82bbf9acbfaffd8cfe780671",
"build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/66/c9/3b04209f599dea8b8ca4be7d3404",
"build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0d/3b/b7b46c3131cff8a40dfaa001af38",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/57/40/0c1d88af3ce14e0f8870ab9ac7ad",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8a/72/02b4eddf662001f05f98288d4ad4",
- "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fc/bc/51529aac7531d1a62cf13eb79153",
- "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ea/7b/de9ce5284627cc77b2fffc354a66",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/96/00/78b64146e33ec35dcde9c278328a",
- "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6d/25/0e5918fa1eb7285538d14051761c"
+ "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ac/e6/0e2476b60bfa7d3af4e9a2a6db1f",
+ "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f3/81/218f5899da12bcaf1128175e7ef0",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/84/14/923a2f78920255adffe574b613d6",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4e/f0/8e6fdd837a08e6daae5bd9e743dc"
}
\ No newline at end of file
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index 5c65f9f5..c67b7241 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -29,8 +29,8 @@
achname
achs
acinstance
- ack
ack'ed
+ ack
acked
acks
acnt
@@ -152,8 +152,8 @@
bacommon
badguy
bafoundation
- ballistica
ballistica's
+ ballistica
ballisticacore
ballisticacorecb
bamaster
@@ -800,8 +800,8 @@
gamedata
gameinstance
gamemap
- gamepad
gamepad's
+ gamepad
gamepadadvanced
gamepads
gamepadselect
@@ -1189,8 +1189,8 @@
lsqlite
lssl
lstart
- lstr
lstr's
+ lstr
lstrs
lsval
ltex
@@ -1823,8 +1823,8 @@
sessionname
sessionplayer
sessionplayers
- sessionteam
sessionteam's
+ sessionteam
sessionteams
sessiontype
setactivity
@@ -2009,6 +2009,7 @@
sysconfigdata
sysctl
syslogmodule
+ tabdefs
tabval
tagargs
tagversion
@@ -2156,8 +2157,8 @@
txtw
typeargs
typecheck
- typechecker
typechecker's
+ typechecker
typedval
typeshed
typestr
diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py
index 8c4c25cc..b07c37b1 100644
--- a/assets/src/ba_data/python/ba/_app.py
+++ b/assets/src/ba_data/python/ba/_app.py
@@ -7,6 +7,14 @@ import random
from typing import TYPE_CHECKING
import _ba
+from ba._music import MusicSubsystem
+from ba._language import LanguageSubsystem
+from ba._ui import UISubsystem
+from ba._achievement import AchievementSubsystem
+from ba._plugin import PluginSubsystem
+from ba._account import AccountSubsystem
+from ba._meta import MetadataSubsystem
+from ba._ads import AdsSubsystem
if TYPE_CHECKING:
import ba
@@ -165,14 +173,6 @@ class App:
the single shared instance.
"""
# pylint: disable=too-many-statements
- from ba._music import MusicSubsystem
- from ba._language import LanguageSubsystem
- from ba._ui import UISubsystem
- from ba._achievement import AchievementSubsystem
- from ba._plugin import PluginSubsystem
- from ba._account import AccountSubsystem
- from ba._meta import MetadataSubsystem
- from ba._ads import AdsSubsystem
# Config.
self.config_file_healthy = False
diff --git a/assets/src/ba_data/python/ba/_apputils.py b/assets/src/ba_data/python/ba/_apputils.py
index b599a7e2..422c2813 100644
--- a/assets/src/ba_data/python/ba/_apputils.py
+++ b/assets/src/ba_data/python/ba/_apputils.py
@@ -175,6 +175,7 @@ def print_live_object_warnings(when: Any,
from ba._session import Session
from ba._actor import Actor
from ba._activity import Activity
+
sessions: List[ba.Session] = []
activities: List[ba.Activity] = []
actors: List[ba.Actor] = []
diff --git a/assets/src/ba_data/python/bastd/ui/tabs.py b/assets/src/ba_data/python/bastd/ui/tabs.py
index c37fbdba..6c090e7b 100644
--- a/assets/src/ba_data/python/bastd/ui/tabs.py
+++ b/assets/src/ba_data/python/bastd/ui/tabs.py
@@ -4,7 +4,8 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, TypeVar, Generic
import ba
@@ -12,6 +13,70 @@ if TYPE_CHECKING:
from typing import Any, Callable, Dict, Tuple, List, Sequence, Optional
+@dataclass
+class Tab:
+ """Info for an individual tab in a TabRow"""
+ button: ba.Widget
+ position: Tuple[float, float]
+ size: Tuple[float, float]
+
+
+T = TypeVar('T')
+
+
+class TabRow(Generic[T]):
+ """Encapsulates a row of tab-styled buttons.
+
+ Tabs are indexed by id which is an arbitrary user-provided type.
+ """
+
+ def __init__(self,
+ parent: ba.Widget,
+ tabdefs: List[Tuple[T, ba.Lstr]],
+ pos: Tuple[float, float],
+ size: Tuple[float, float],
+ on_select_call: Callable[[T], None] = None) -> None:
+ if not tabdefs:
+ raise ValueError('At least one tab def is required')
+ self.tabs: Dict[T, Tab] = {}
+ tab_pos_v = pos[1]
+ tab_button_width = float(size[0]) / len(tabdefs)
+ tab_spacing = (250.0 - tab_button_width) * 0.06
+ h = pos[0]
+ for tab_id, tab_label in tabdefs:
+ pos = (h + tab_spacing * 0.5, tab_pos_v)
+ size = (tab_button_width - tab_spacing, 50.0)
+ btn = ba.buttonwidget(parent=parent,
+ position=pos,
+ autoselect=True,
+ button_type='tab',
+ size=size,
+ label=tab_label,
+ enable_sound=False,
+ on_activate_call=ba.Call(
+ self._tick_and_call, on_select_call,
+ tab_id))
+ h += tab_button_width
+ self.tabs[tab_id] = Tab(button=btn, position=pos, size=size)
+
+ def update_appearance(self, selected_tab_id: T) -> None:
+ """Update appearances to make the provided tab appear selected."""
+ for tab_id, tab in self.tabs.items():
+ if tab_id == selected_tab_id:
+ ba.buttonwidget(edit=tab.button,
+ color=(0.5, 0.4, 0.93),
+ textcolor=(0.85, 0.75, 0.95)) # lit
+ else:
+ ba.buttonwidget(edit=tab.button,
+ color=(0.52, 0.48, 0.63),
+ textcolor=(0.65, 0.6, 0.7)) # unlit
+
+ def _tick_and_call(self, call: Optional[Callable], arg: Any) -> None:
+ ba.playsound(ba.getsound('click01'))
+ if call is not None:
+ call(arg)
+
+
def create_tab_buttons(parent_widget: ba.Widget,
tabs: List[Tuple[str, ba.Lstr]],
pos: Sequence[float],
@@ -25,7 +90,7 @@ def create_tab_buttons(parent_widget: ba.Widget,
tab_buttons_indexed = []
tab_button_width = float(size[0]) / len(tabs)
- # add a bit more visual spacing as our buttons get narrower
+ # Add a bit more visual spacing as our buttons get narrower.
tab_spacing = (250.0 - tab_button_width) * 0.06
positions = []
sizes = []
diff --git a/assets/src/ba_data/python/bastd/ui/watch.py b/assets/src/ba_data/python/bastd/ui/watch.py
index f7c40c9e..c286a9a9 100644
--- a/assets/src/ba_data/python/bastd/ui/watch.py
+++ b/assets/src/ba_data/python/bastd/ui/watch.py
@@ -5,6 +5,7 @@
from __future__ import annotations
import os
+from enum import Enum
from typing import TYPE_CHECKING, cast
import _ba
@@ -14,6 +15,12 @@ if TYPE_CHECKING:
from typing import Any, Optional, Tuple, Dict
+class TabID(Enum):
+ """Our available tab types."""
+ MY_REPLAYS = 'my_replays'
+ TEST_TAB = 'test_tab'
+
+
class WatchWindow(ba.Window):
"""Window for watching replays."""
@@ -22,7 +29,7 @@ class WatchWindow(ba.Window):
origin_widget: ba.Widget = None):
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
- from bastd.ui import tabs
+ from bastd.ui.tabs import TabRow
ba.set_analytics_screen('Watch Window')
scale_origin: Optional[Tuple[float, float]]
if origin_widget is not None:
@@ -47,7 +54,7 @@ class WatchWindow(ba.Window):
x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
self._height = (578 if uiscale is ba.UIScale.SMALL else
670 if uiscale is ba.UIScale.MEDIUM else 800)
- self._current_tab: Optional[str] = None
+ self._current_tab: Optional[TabID] = None
extra_top = 20 if uiscale is ba.UIScale.SMALL else 0
super().__init__(root_widget=ba.containerwidget(
@@ -90,32 +97,35 @@ class WatchWindow(ba.Window):
text=ba.Lstr(resource=self._r + '.titleText'),
maxwidth=400)
- tabs_def = [('my_replays',
- ba.Lstr(resource=self._r + '.myReplaysText'))]
+ tabdefs = [
+ (TabID.MY_REPLAYS, ba.Lstr(resource=self._r + '.myReplaysText')),
+ # (TabID.TEST_TAB, ba.Lstr(value='Testing')),
+ ]
scroll_buffer_h = 130 + 2 * x_inset
tab_buffer_h = 750 + 2 * x_inset
- self._tab_buttons = tabs.create_tab_buttons(
- self._root_widget,
- tabs_def,
- pos=(tab_buffer_h * 0.5, self._height - 130),
- size=(self._width - tab_buffer_h, 50),
- on_select_call=self._set_tab)
+ self._tab_row = TabRow(self._root_widget,
+ tabdefs,
+ pos=(tab_buffer_h * 0.5, self._height - 130),
+ size=(self._width - tab_buffer_h, 50),
+ on_select_call=self._set_tab)
if ba.app.ui.use_toolbars:
- ba.widget(edit=self._tab_buttons[tabs_def[-1][0]],
+ first_tab = self._tab_row.tabs[tabdefs[0][0]]
+ last_tab = self._tab_row.tabs[tabdefs[-1][0]]
+ ba.widget(edit=last_tab.button,
right_widget=_ba.get_special_widget('party_button'))
if uiscale is ba.UIScale.SMALL:
bbtn = _ba.get_special_widget('back_button')
- ba.widget(edit=self._tab_buttons[tabs_def[0][0]],
+ ba.widget(edit=first_tab.button,
up_widget=bbtn,
left_widget=bbtn)
self._scroll_width = self._width - scroll_buffer_h
self._scroll_height = self._height - 180
- # not actually using a scroll widget anymore; just an image
+ # Not actually using a scroll widget anymore; just an image.
scroll_left = (self._width - self._scroll_width) * 0.5
scroll_bottom = self._height - self._scroll_height - 79 - 48
buffer_h = 10
@@ -131,21 +141,21 @@ class WatchWindow(ba.Window):
self._restore_state()
- def _set_tab(self, tab: str) -> None:
+ def _set_tab(self, tab_id: TabID) -> None:
# pylint: disable=too-many-locals
- from bastd.ui import tabs
- if self._current_tab == tab:
+ if self._current_tab == tab_id:
return
- self._current_tab = tab
+ self._current_tab = tab_id
- # We wanna preserve our current tab between runs.
+ # Preserve our current tab between runs.
cfg = ba.app.config
- cfg['Watch Tab'] = tab
+ cfg['Watch Tab'] = tab_id.value
cfg.commit()
# Update tab colors based on which is selected.
- tabs.update_tab_button_colors(self._tab_buttons, tab)
+ # tabs.update_tab_button_colors(self._tab_buttons, tab)
+ self._tab_row.update_appearance(tab_id)
if self._tab_container:
self._tab_container.delete()
@@ -157,7 +167,7 @@ class WatchWindow(ba.Window):
self._tab_data = {}
uiscale = ba.app.ui.uiscale
- if tab == 'my_replays':
+ if tab_id is TabID.MY_REPLAYS:
c_width = self._scroll_width
c_height = self._scroll_height - 20
sub_scroll_height = c_height - 63
@@ -212,7 +222,7 @@ class WatchWindow(ba.Window):
text_scale=tscl,
label=ba.Lstr(resource=self._r + '.watchReplayButtonText'),
autoselect=True)
- ba.widget(edit=btn1, up_widget=self._tab_buttons[tab])
+ ba.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button)
if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
ba.widget(edit=btn1,
left_widget=_ba.get_special_widget('back_button'))
@@ -255,8 +265,9 @@ class WatchWindow(ba.Window):
ba.widget(edit=scrlw,
autoselect=True,
left_widget=btn1,
- up_widget=self._tab_buttons[tab])
- ba.widget(edit=self._tab_buttons[tab], down_widget=scrlw)
+ up_widget=self._tab_row.tabs[tab_id].button)
+ ba.widget(edit=self._tab_row.tabs[tab_id].button,
+ down_widget=scrlw)
self._my_replay_selected = None
self._refresh_my_replays()
@@ -463,23 +474,28 @@ class WatchWindow(ba.Window):
corner_scale=t_scale,
maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93)
if i == 0:
- ba.widget(edit=txt, up_widget=self._tab_buttons['my_replays'])
+ ba.widget(
+ edit=txt,
+ up_widget=self._tab_row.tabs[TabID.MY_REPLAYS].button)
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
+ selected_tab_ids = [
+ tab_id for tab_id, tab in self._tab_row.tabs.items()
+ if sel == tab.button
+ ]
if sel == self._back_button:
sel_name = 'Back'
- elif sel in list(self._tab_buttons.values()):
- sel_name = 'Tab:' + list(self._tab_buttons.keys())[list(
- self._tab_buttons.values()).index(sel)]
+ elif selected_tab_ids:
+ assert len(selected_tab_ids) == 1
+ sel_name = f'Tab:{selected_tab_ids[0].value}'
elif sel == self._tab_container:
sel_name = 'TabContainer'
else:
raise ValueError(f'unrecognized selection {sel}')
ba.app.ui.window_states[self.__class__.__name__] = {
- 'sel_name': sel_name,
- 'tab': self._current_tab
+ 'sel_name': sel_name
}
except Exception:
ba.print_exception(f'Error saving state for {self}.')
@@ -488,21 +504,28 @@ class WatchWindow(ba.Window):
try:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
{}).get('sel_name')
- current_tab = ba.app.config.get('Watch Tab')
- if current_tab is None or current_tab not in self._tab_buttons:
- current_tab = 'my_replays'
+ assert isinstance(sel_name, (str, type(None)))
+ try:
+ current_tab = TabID(ba.app.config.get('Watch Tab'))
+ except ValueError:
+ current_tab = TabID.MY_REPLAYS
self._set_tab(current_tab)
+
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'TabContainer':
sel = self._tab_container
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
- sel = self._tab_buttons[sel_name.split(':')[-1]]
+ try:
+ sel_tab_id = TabID(sel_name.split(':')[-1])
+ except ValueError:
+ sel_tab_id = TabID.MY_REPLAYS
+ sel = self._tab_row.tabs[sel_tab_id].button
else:
if self._tab_container is not None:
sel = self._tab_container
else:
- sel = self._tab_buttons[current_tab]
+ sel = self._tab_row.tabs[current_tab].button
ba.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
ba.print_exception(f'Error restoring state for {self}.')
diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
index 773bfe03..92811cfe 100644
--- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
@@ -814,6 +814,7 @@
sval
symbolification
syscalls
+ tabdefs
talloc
tegra
telefonaktiebolaget
diff --git a/docs/ba_module.md b/docs/ba_module.md
index 3c2d804d..b169ace3 100644
--- a/docs/ba_module.md
+++ b/docs/ba_module.md
@@ -1,5 +1,5 @@
-
last updated on 2020-10-17 for Ballistica version 1.5.27 build 20223
+last updated on 2020-10-19 for Ballistica version 1.5.27 build 20224
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!
diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc
index 4bab72f6..717a5a84 100644
--- a/src/ballistica/ballistica.cc
+++ b/src/ballistica/ballistica.cc
@@ -5,23 +5,15 @@
#include