From 981375f51e728ae7e7d2f520f9cf4d9504e05113 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Tue, 20 Oct 2020 13:59:06 -0700 Subject: [PATCH] Work on type-safe TabRow class --- .efrocachemap | 32 +++---- .idea/dictionaries/ericf.xml | 13 +-- assets/src/ba_data/python/ba/_app.py | 16 ++-- assets/src/ba_data/python/ba/_apputils.py | 1 + assets/src/ba_data/python/bastd/ui/tabs.py | 69 +++++++++++++- assets/src/ba_data/python/bastd/ui/watch.py | 93 ++++++++++++------- .../.idea/dictionaries/ericf.xml | 1 + docs/ba_module.md | 2 +- src/ballistica/ballistica.cc | 10 +- 9 files changed, 160 insertions(+), 77 deletions(-) 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 #include "ballistica/app/app.h" -#include "ballistica/app/app_config.h" -#include "ballistica/app/app_globals.h" -#include "ballistica/audio/audio.h" #include "ballistica/audio/audio_server.h" #include "ballistica/core/fatal_error.h" #include "ballistica/core/logging.h" #include "ballistica/core/thread.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" #include "ballistica/dynamics/bg/bg_dynamics_server.h" #include "ballistica/game/account.h" -#include "ballistica/graphics/graphics.h" #include "ballistica/graphics/graphics_server.h" -#include "ballistica/input/input.h" -#include "ballistica/media/media.h" #include "ballistica/media/media_server.h" #include "ballistica/networking/network_write_module.h" -#include "ballistica/networking/networking.h" #include "ballistica/platform/platform.h" #include "ballistica/python/python.h" #include "ballistica/scene/scene.h" @@ -29,7 +21,7 @@ namespace ballistica { // These are set automatically via script; don't change here. -const int kAppBuildNumber = 20223; +const int kAppBuildNumber = 20224; const char* kAppVersion = "1.5.27"; // Our standalone globals.