Merge branch 'efroemling:main' into main

This commit is contained in:
Vishal 2024-08-31 19:22:41 +05:30 committed by GitHub
commit 1f48ac708c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 282 additions and 878 deletions

56
.efrocachemap generated
View File

@ -4096,26 +4096,26 @@
"build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1",
"build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "0a166c970637de5e318e4f07e8c0aeae",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "71fee3398234820d038235f02ea7ed6a",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "682d622e82fbdf7d41f170146ae8cf6f",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "ed9a7e820ab3a59d6326b898973d5cff",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "4efa411d0d43debb7c8db1db1d9a4658",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "8892f774f2d079604d77241adaf3e529",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "c479f3ab633f0b19f3e733943b3c1c3b",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "2797e7b22388c68b2e41a1eb7c820a62",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "f58b978d2978fdf6e2f7f0cc13fbb2c7",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "1ad711461c2a65ecb1fbab1aac2aacae",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "ec90ed3ec92a6db3f8b9651f2464d8d4",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "12cc0539e86a408b01ac3b3a74cff98a",
"build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "1f59835f71e01827f056f503302c45c4",
"build/prefab/full/mac_x86_64_gui/release/ballisticakit": "c747103d9fe3206f94b54ed9ae5d4aa4",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "603f77208cb0df9161b84387631b8778",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "00a72972b6b0531b176406ad0e8d243f",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "af0063f60bb84a65ca71df83d00b9507",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "817b90e953f655e83cd8e69d1feb5526",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "c132d48c3b560ac0dcf090a3bce8ba26",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "b3c44cd6282d4100b730e997baa3f28e",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "d6246c930e7e2d2d9a6aff6788f33b69",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "001c67c4d4d33e20755399e0b2ed1593",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "6f6bfaf19daf6e866f4fecbc889b8854",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "ebc7449903d7868c631c504aed10f371",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "91f03e7dbfc0d7eb75568704f681fba5",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "0b32b6eb05df0a7e23e55ff2e7235a8f",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "4ed4b63cc506815f759412a295bfe088",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "1890731fb8a2fb43c4cde72af9d7a4e6",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "1a4764a504dcb20591ac3472c48db8d9",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "9bfd6234aac4ffd7d7601a40017a73be",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "941f8fb79d54522ca5df97a735c3babe",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "290a3d9840efc5e88532e13b76b3ae6b",
"build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "621a6684b54b0a9101f808209bcca1ee",
"build/prefab/full/mac_x86_64_gui/release/ballisticakit": "43c8b0b29e5fb257e4cde0f7fce1c680",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "9a30c6b88ffc11bdf6765780616d8ba1",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "8a8f63850fc296b060a994f4e001d74f",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "2864e9ae1d2d566def85f1cdf6e863fe",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "dc0db7ca99661a896634fc05187a5c75",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "f56bb0fafe0a45cecdcd06a10a6924c9",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "e3a58b09fea193d78187f97cf922717d",
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "73ad3303fe1a82005918fbc5dae3446c",
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "fa659b5d6119acba6570c92ce4d35ae2",
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "73ad3303fe1a82005918fbc5dae3446c",
@ -4132,14 +4132,14 @@
"build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "3e5c5fd0a09f55ba7b05ce1e2ec7171e",
"build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "1659535e95e3047fda529543e265ac97",
"build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "3e5c5fd0a09f55ba7b05ce1e2ec7171e",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "d4aca724c3184c6c2c82e7b11c1a352a",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "f4fbab89e11f52329d65a9afcabc2e5e",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "09733e1291806ece13f108ad6fb72b68",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "099e20f57cfa2c36bf7b44e389d01d9a",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "aae5ce60761c700d23a480cac83a2df5",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "1fdc6ee85373af9df3a0b5eb601990af",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "809e62f1d2ac54c239a3a13c2d2c8ee7",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "68b49b66a76b9ab0d1b17de2bab19c04",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "6b6aae30362b1e9aaf3f1a8daae0b0b4",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "25db08ee2b4c79ca631e517b622c741d",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "2ea419644782c4c52d1fa784d0ced86d",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "c40627a46ee127997153df041e47eda5",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "fff81b29e2d86031ec25c4a672a243e7",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "2a87e2f53c658d0be3918476cd8a04ee",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "1e91536979b318b9a8325ab0e871ecec",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "c6506399a93ef93e8322425f78e2e6b0",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "cb299985623bbcc86015cb103a424ae6",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "efa61468cf098f77cc6a234461d8b86d",

View File

@ -1,6 +1,14 @@
### 1.7.37 (build 21968, api 8, 2024-08-29)
### 1.7.37 (build 21977, api 9, 2024-08-30)
- Bumping api version to 9. As you'll see below, there's some UI changes that
will require a bit of work for any UI mods to adapt to. If your mods don't
touch UI stuff at all you can simply bump your api version and call it a day.
I'm hopeful that api version won't need to be bumped again for along time (if
ever).
- Playlist customization no longer requires pro.
- Soundtrack customization no longer requires pro.
- Campaign hard mode no longer requires pro.
- Full player profile color customization no longer requires pro.
- Removed nag screens for purchasing pro or bundle offers.
- Switching over to the new 'toolbar mode' UI that has been in the works for
several years. This includes a number of handy things such as consistent
buttons and widgets for league status, currencies, inventory, and the store.
@ -34,7 +42,7 @@
back-button handling are more automatic and windows don't have to hard-code
where they came from. There are also other benefits such as better state
saving/restoring. When writing a MainWindow, pretty much all navigation should
only need too use methods: `can_change_main_window()`, `main_window_back()`,
only need too use methods: `main_window_has_control()`, `main_window_back()`,
and `main_window_replace()`.
- Finally got things updated so language testing works again, and made it a bit
spiffier while at it. You now simply point the game at your test language and

View File

@ -399,7 +399,6 @@
"ba_data/python/bauiv1lib/__pycache__/resourcetypeinfo.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/sendinfo.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/serverdialog.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/specialoffer.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/tabs.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/teamnamescolors.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/tournamententry.cpython-312.opt-1.pyc",
@ -558,7 +557,6 @@
"ba_data/python/bauiv1lib/soundtrack/edit.py",
"ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py",
"ba_data/python/bauiv1lib/soundtrack/macmusicapp.py",
"ba_data/python/bauiv1lib/specialoffer.py",
"ba_data/python/bauiv1lib/store/__init__.py",
"ba_data/python/bauiv1lib/store/__pycache__/__init__.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/store/__pycache__/browser.cpython-312.opt-1.pyc",

View File

@ -425,7 +425,6 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bauiv1lib/soundtrack/edit.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/soundtrack/macmusicapp.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/specialoffer.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/store/__init__.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/store/browser.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/store/button.py \
@ -704,7 +703,6 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bauiv1lib/soundtrack/__pycache__/edit.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/soundtrack/__pycache__/entrytypeselect.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/soundtrack/__pycache__/macmusicapp.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/specialoffer.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/store/__pycache__/__init__.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/store/__pycache__/browser.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/store/__pycache__/button.cpython-312.opt-1.pyc \

View File

@ -62,6 +62,7 @@ from _babase import (
have_chars,
have_permission,
in_logic_thread,
in_main_menu,
increment_analytics_count,
invoke_main_menu,
is_os_playing_music,
@ -262,6 +263,7 @@ __all__ = [
'have_chars',
'have_permission',
'in_logic_thread',
'in_main_menu',
'increment_analytics_count',
'InputDeviceNotFoundError',
'InputType',

View File

@ -16,7 +16,7 @@ classic submodules directly, it is much harder to make it cleanly handle
classic not being present.
"""
# ba_meta require api 8
# ba_meta require api 9
# Note: Code relying on classic should import things from here *only*
# for type-checking and use the versions in ba*.app.classic at runtime;

View File

@ -15,6 +15,7 @@ from babase import (
AppIntentDefault,
invoke_main_menu,
screenmessage,
in_main_menu,
)
import _baclassic
@ -120,20 +121,28 @@ class ClassicAppMode(AppMode):
def _jump_to_main_window(self, window: MainWindow) -> None:
"""Jump to a window with the main menu as its parent."""
from bauiv1lib.mainmenu import MainMenuWindow
from bauiv1lib.ingamemenu import InGameMenuWindow
ui = app.ui_v1
old_window = ui.get_main_window()
if isinstance(old_window, MainMenuWindow):
if isinstance(old_window, (MainMenuWindow, InGameMenuWindow)):
# If we're currently in the top level menu window, just push
# our mainwindow on to the end.
old_window.main_window_replace(window)
else:
# Blow away the window stack.
# Blow away the window stack and build a fresh one.
ui.clear_main_window()
ui.set_main_window(
window,
from_window=False, # Disable from-check.
back_state=MainMenuWindow.do_get_main_window_state(),
back_state=(
MainMenuWindow.do_get_main_window_state()
if in_main_menu()
else InGameMenuWindow.do_get_main_window_state()
),
)
def _root_ui_menu_press(self) -> None:

View File

@ -216,24 +216,6 @@ class ClassicAppSubsystem(babase.AppSubsystem):
cfg['launchCount'] = launch_count
cfg.commit()
# Run a test in a few seconds to see if we should pop up an existing
# pending special offer.
def check_special_offer() -> None:
assert plus is not None
from bauiv1lib.specialoffer import show_offer
if (
'pendingSpecialOffer' in cfg
and plus.get_v1_account_public_login_id()
== cfg['pendingSpecialOffer']['a']
):
self.special_offer = cfg['pendingSpecialOffer']['o']
show_offer()
if babase.app.env.gui:
babase.apptimer(3.0, check_special_offer)
# If there's a leftover log file, attempt to upload it to the
# master-server and/or get rid of it.
babase.handle_leftover_v1_cloud_log_file()
@ -832,7 +814,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
app = bauiv1.app
env = app.env
with bascenev1.ContextRef.empty():
from bauiv1lib import specialoffer
# from bauiv1lib import specialoffer
assert app.classic is not None
if app.env.headless:
@ -927,11 +909,11 @@ class ClassicAppSubsystem(babase.AppSubsystem):
# (we may not have heard back from the server)
# ..if that doesn't work they'll just have to wait
# until the next opportunity.
if not specialoffer.show_offer():
# if not specialoffer.show_offer():
def try_again() -> None:
if not specialoffer.show_offer():
# Try one last time..
bauiv1.apptimer(2.0, specialoffer.show_offer)
# def try_again() -> None:
# if not specialoffer.show_offer():
# # Try one last time..
# bauiv1.apptimer(2.0, specialoffer.show_offer)
bauiv1.apptimer(2.0, try_again)
# bauiv1.apptimer(2.0, try_again)

View File

@ -52,7 +52,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
TARGET_BALLISTICA_BUILD = 21968
TARGET_BALLISTICA_BUILD = 21977
TARGET_BALLISTICA_VERSION = '1.7.37'

View File

@ -2,7 +2,7 @@
#
"""Gameplay-centric api for classic BombSquad."""
# ba_meta require api 8
# ba_meta require api 9
# The stuff we expose here at the top level is our 'public' api for use
# from other modules/packages. Code *within* this package should import

View File

@ -4,6 +4,7 @@
from __future__ import annotations
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, NewType
@ -243,7 +244,6 @@ def cameraflash(duration: float = 999.0) -> None:
Duration is in seconds.
"""
# pylint: disable=too-many-locals
import random
from bascenev1._nodeactor import NodeActor
x_spread = 10

View File

@ -2,4 +2,4 @@
#
"""Library of stuff using the bascenev1 api: games, actors, etc."""
# ba_meta require api 8
# ba_meta require api 9

View File

@ -207,20 +207,22 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
)
def _ui_menu(self) -> None:
from bauiv1lib import specialoffer
# from bauiv1lib import specialoffer
# if specialoffer.show_offer():
# return
if specialoffer.show_offer():
return
bui.containerwidget(edit=self._root_ui, transition='out_left')
with self.context:
bs.timer(0.1, bs.Call(bs.WeakCall(self.session.end)))
def _ui_restart(self) -> None:
from bauiv1lib.tournamententry import TournamentEntryWindow
from bauiv1lib import specialoffer
if specialoffer.show_offer():
return
# from bauiv1lib import specialoffer
# if specialoffer.show_offer():
# return
# If we're in a tournament and it looks like there's no time left,
# disallow.
@ -268,10 +270,10 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
self.end({'outcome': 'restart'})
def _ui_next(self) -> None:
from bauiv1lib.specialoffer import show_offer
# from bauiv1lib.specialoffer import show_offer
if show_offer():
return
# if show_offer():
# return
# If we didn't just complete this level but are choosing to play the
# next one, set it as current (this won't happen otherwise).

View File

@ -2,7 +2,7 @@
#
"""Defines assault minigame."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Defines a capture-the-flag game."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Provides the chosen-one mini-game."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Provides the Conquest game."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""DeathMatch game and support classes."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Provides an easter egg hunt game."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Elimination mini-game."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -3,7 +3,7 @@
# pylint: disable=too-many-lines
"""Implements football games (both co-op and teams varieties)."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Hockey game and support classes."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Defines a keep-away game type."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Defines the King of the Hill game."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Defines a bomb-dodging mini-game."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Provides Ninja Fight mini-game."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -5,7 +5,7 @@
# Yes this is a long one..
# pylint: disable=too-many-lines
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Defines Race mini-game."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -5,7 +5,7 @@
# We wear the cone of shame.
# pylint: disable=too-many-lines
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Implements Target Practice game."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Ballistica Template Feature Set - just an example."""
# ba_meta require api 8
# ba_meta require api 9
# Package up various private bits (including stuff from our native
# module) into a nice clean public API.

View File

@ -2,7 +2,7 @@
#
"""Ballistica user interface api version 1"""
# ba_meta require api 8
# ba_meta require api 9
# The stuff we expose here at the top level is our 'public' api.
# It should only be imported by code outside of this package or
@ -60,6 +60,7 @@ from babase import (
getclass,
have_permission,
in_logic_thread,
in_main_menu,
increment_analytics_count,
is_browser_likely_available,
is_xcode_build,
@ -188,6 +189,7 @@ __all__ = [
'hscrollwidget',
'imagewidget',
'in_logic_thread',
'in_main_menu',
'increment_analytics_count',
'is_browser_likely_available',
'is_party_icon_visible',

View File

@ -99,7 +99,7 @@ class MainWindow(Window):
edit=self._root_widget, transition=self._main_window_transition_out
)
def can_change_main_window(self) -> bool:
def main_window_has_control(self) -> bool:
"""Is this MainWindow allowed to change the global main window?
It is a good idea to make sure this is True before calling
@ -118,12 +118,12 @@ class MainWindow(Window):
def main_window_back(self) -> None:
"""Move back in the main window stack."""
# Users should always check can_change_main_window() before
# Users should always check main_window_has_control() before
# calling us. Error if it seems they did not.
if not self.can_change_main_window():
if not self.main_window_has_control():
raise RuntimeError(
'main_window_back() should only be called'
' if can_change_main_window() returns True'
' if main_window_has_control() returns True'
' (it currently is False).'
)
@ -137,13 +137,13 @@ class MainWindow(Window):
) -> None:
"""Replace ourself with a new MainWindow."""
# Users should always check can_change_main_window() before
# Users should always check main_window_has_control() before
# creating new MainWindows and passing them in here. Error if it
# seems they did not.
if not self.can_change_main_window():
if not self.main_window_has_control():
raise RuntimeError(
'main_window_replace() should only be called'
' if can_change_main_window() returns True'
' if main_window_has_control() returns True'
' (it currently is False).'
)

View File

@ -2,4 +2,4 @@
#
"""Library of stuff using the bauiv1 api: windows, custom controls, etc."""
# ba_meta require api 8
# ba_meta require api 9

View File

@ -12,6 +12,8 @@ import bauiv1 as bui
if TYPE_CHECKING:
from typing import Any, Sequence
REQUIRE_PRO = False
class ColorPicker(PopupWindow):
"""A popup UI to select from a set of colors.
@ -105,9 +107,8 @@ class ColorPicker(PopupWindow):
on_activate_call=bui.WeakCall(self._select_other),
)
# Custom colors are limited to pro currently.
assert bui.app.classic is not None
if not bui.app.classic.accounts.have_pro():
if REQUIRE_PRO and not bui.app.classic.accounts.have_pro():
bui.imagewidget(
parent=self.root_widget,
position=(50, 12),
@ -137,7 +138,7 @@ class ColorPicker(PopupWindow):
# Requires pro.
assert bui.app.classic is not None
if not bui.app.classic.accounts.have_pro():
if REQUIRE_PRO and not bui.app.classic.accounts.have_pro():
purchase.PurchaseWindow(items=['pro'])
self._transition_out()
return

View File

@ -16,6 +16,8 @@ if TYPE_CHECKING:
from bauiv1lib.coop.tournamentbutton import TournamentButton
HARD_REQUIRES_PRO = False
class CoopBrowserWindow(bui.MainWindow):
"""Window for browsing co-op levels/games/etc."""
@ -90,6 +92,7 @@ class CoopBrowserWindow(bui.MainWindow):
if (
self._campaign_difficulty == 'hard'
and HARD_REQUIRES_PRO
and not classic.accounts.have_pro_options()
):
plus.add_v1_account_transaction(
@ -430,7 +433,12 @@ class CoopBrowserWindow(bui.MainWindow):
bui.imagewidget(
edit=self._hard_button_lock_image,
opacity=(
0.0 if bui.app.classic.accounts.have_pro_options() else 1.0
0.0
if (
(not HARD_REQUIRES_PRO)
or bui.app.classic.accounts.have_pro_options()
)
else 1.0
),
)
except Exception:
@ -490,6 +498,7 @@ class CoopBrowserWindow(bui.MainWindow):
if difficulty != self._campaign_difficulty:
if (
difficulty == 'hard'
and HARD_REQUIRES_PRO
and not bui.app.classic.accounts.have_pro_options()
):
PurchaseWindow(items=['pro'])

View File

@ -49,7 +49,11 @@ class InGameMenuWindow(bui.MainWindow):
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
return self.do_get_main_window_state()
@classmethod
def do_get_main_window_state(cls) -> bui.MainWindowState:
"""Classmethod to gen a windowstate for the main menu."""
return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
transition=transition, origin_widget=origin_widget

View File

@ -2,7 +2,7 @@
#
"""Defines a default keyboards."""
# ba_meta require api 8
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -153,6 +153,33 @@ class MainMenuWindow(bui.MainWindow):
app = bui.app
assert app.classic is not None
uiscale = app.ui_v1.uiscale
# Temp note about UI changes.
bui.textwidget(
parent=self._root_widget,
position=(
(-400, 400)
if uiscale is bui.UIScale.LARGE
else (
(-270, 320)
if uiscale is bui.UIScale.MEDIUM
else (-280, 280)
)
),
size=(0, 0),
scale=0.4,
flatness=1.0,
text=(
'WARNING: This build contains a revamped UI\n'
'which is still a work-in-progress. A number\n'
'of features are not currently functional or\n'
'contain bugs. To go back to the stable legacy UI,\n'
'grab version 1.7.36 from ballistica.net'
),
h_align='left',
v_align='top',
)
self._have_quit_button = app.classic.platform in (
'windows',
@ -183,7 +210,6 @@ class MainMenuWindow(bui.MainWindow):
side_button_2_y_offs = 10.0
side_button_2_scale = 0.5
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL:
root_widget_scale = 1.3
button_y_offs = -20.0
@ -204,6 +230,24 @@ class MainMenuWindow(bui.MainWindow):
scale=root_widget_scale,
)
# Version/copyright info.
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, button_y_offs - 10),
size=(0, 0),
scale=0.4,
flatness=1.0,
color=(1, 1, 1, 0.3),
text=(
f'{app.env.engine_version}'
f' build {app.env.engine_build_number}.'
f' Copyright 2024 Eric Froemling.'
),
h_align='center',
v_align='center',
transition_delay=self._t_delay_play,
)
# In kiosk mode, provide a button to get back to the kiosk menu.
if bui.app.env.demo or bui.app.env.arcade:
# h, v, scale = positions[self._p_index]
@ -443,7 +487,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.confirm import QuitWindow
# no-op if we're not currently in control.
if not self.can_change_main_window():
if not self.main_window_has_control():
return
# Note: Normally we should go through bui.quit(confirm=True) but
@ -456,7 +500,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.credits import CreditsWindow
# no-op if we're not currently in control.
if not self.can_change_main_window():
if not self.main_window_has_control():
return
self.main_window_replace(
@ -469,7 +513,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.helpui import HelpWindow
# no-op if we're not currently in control.
if not self.can_change_main_window():
if not self.main_window_has_control():
return
self.main_window_replace(
@ -536,7 +580,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.gather import GatherWindow
# no-op if we're not currently in control.
if not self.can_change_main_window():
if not self.main_window_has_control():
return
self.main_window_replace(
@ -548,7 +592,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.watch import WatchWindow
# no-op if we're not currently in control.
if not self.can_change_main_window():
if not self.main_window_has_control():
return
self.main_window_replace(
@ -560,7 +604,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.play import PlayWindow
# no-op if we're not currently in control.
if not self.can_change_main_window():
if not self.main_window_has_control():
return
classic = bui.app.classic

View File

@ -541,7 +541,7 @@ class PlayWindow(bui.MainWindow):
# pylint: disable=cyclic-import
# no-op if we're not currently in control.
if not self.can_change_main_window():
if not self.main_window_has_control():
return
if self._is_main_menu:
@ -567,7 +567,7 @@ class PlayWindow(bui.MainWindow):
from bauiv1lib.coop.browser import CoopBrowserWindow
# no-op if we're not currently in control.
if not self.can_change_main_window():
if not self.main_window_has_control():
return
plus = bui.app.plus
@ -588,7 +588,7 @@ class PlayWindow(bui.MainWindow):
from bauiv1lib.playlist.browser import PlaylistBrowserWindow
# no-op if we're not currently in control.
if not self.can_change_main_window():
if not self.main_window_has_control():
return
self._save_state()
@ -604,7 +604,7 @@ class PlayWindow(bui.MainWindow):
from bauiv1lib.playlist.browser import PlaylistBrowserWindow
# no-op if we're not currently in control.
if not self.can_change_main_window():
if not self.main_window_has_control():
return
self._save_state()

View File

@ -1,569 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI for presenting sales/etc."""
from __future__ import annotations
import copy
import logging
from typing import TYPE_CHECKING
import bauiv1 as bui
if TYPE_CHECKING:
from typing import Any
class SpecialOfferWindow(bui.Window):
"""Window for presenting sales/etc."""
def __init__(self, offer: dict[str, Any], transition: str = 'in_right'):
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
from babase import SpecialChar
from bauiv1lib.store import item as storeitemui
plus = bui.app.plus
assert plus is not None
assert bui.app.classic is not None
store = bui.app.classic.store
self._cancel_delay = offer.get('cancelDelay', 0)
# First thing: if we're offering pro or an IAP, see if we have a
# price for it.
# If not, abort and go into zombie mode (the user should never see
# us that way).
real_price: str | None
# Misnomer: 'pro' actually means offer 'pro_sale'.
if offer['item'] in ['pro', 'pro_fullprice']:
real_price = plus.get_price(
'pro' if offer['item'] == 'pro_fullprice' else 'pro_sale'
)
if real_price is None and bui.app.env.debug:
print('NOTE: Faking prices for debug build.')
real_price = '$1.23'
zombie = real_price is None
elif isinstance(offer['price'], str):
# (a string price implies IAP id)
real_price = plus.get_price(offer['price'])
if real_price is None and bui.app.env.debug:
print('NOTE: Faking price for debug build.')
real_price = '$1.23'
zombie = real_price is None
else:
real_price = None
zombie = False
if real_price is None:
real_price = '?'
if offer['item'] in ['pro', 'pro_fullprice']:
self._offer_item = 'pro'
else:
self._offer_item = offer['item']
# If we wanted a real price but didn't find one, go zombie.
if zombie:
return
# This can pop up suddenly, so lets block input for 1 second.
bui.lock_all_input()
bui.apptimer(1.0, bui.unlock_all_input)
bui.getsound('ding').play()
bui.apptimer(0.3, bui.getsound('ooh').play)
self._offer = copy.deepcopy(offer)
self._width = 580
self._height = 590
uiscale = bui.app.ui_v1.uiscale
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
transition=transition,
scale=(
1.2
if uiscale is bui.UIScale.SMALL
else 1.15 if uiscale is bui.UIScale.MEDIUM else 1.0
),
stack_offset=(
(0, -15) if uiscale is bui.UIScale.SMALL else (0, 0)
),
)
)
self._is_bundle_sale = False
try:
if offer['item'] in ['pro', 'pro_fullprice']:
original_price_str = plus.get_price('pro')
if original_price_str is None:
original_price_str = '?'
new_price_str = plus.get_price('pro_sale')
if new_price_str is None:
new_price_str = '?'
percent_off_text = ''
else:
# If the offer includes bonus tickets, it's a bundle-sale.
if (
'bonusTickets' in offer
and offer['bonusTickets'] is not None
):
self._is_bundle_sale = True
original_price = plus.get_v1_account_misc_read_val(
'price.' + self._offer_item, 9999
)
# For pure ticket prices we can show a percent-off.
if isinstance(offer['price'], int):
new_price = offer['price']
tchar = bui.charstr(SpecialChar.TICKET)
original_price_str = tchar + str(original_price)
new_price_str = tchar + str(new_price)
percent_off = int(
round(
100.0 - (float(new_price) / original_price) * 100.0
)
)
percent_off_text = ' ' + bui.Lstr(
resource='store.salePercentText'
).evaluate().replace('${PERCENT}', str(percent_off))
else:
original_price_str = new_price_str = '?'
percent_off_text = ''
except Exception:
logging.exception('Error setting up special-offer: %s.', offer)
original_price_str = new_price_str = '?'
percent_off_text = ''
# If its a bundle sale, change the title.
if self._is_bundle_sale:
sale_text = bui.Lstr(
resource='store.saleBundleText',
fallback_resource='store.saleText',
).evaluate()
else:
# For full pro we say 'Upgrade?' since its not really a sale.
if offer['item'] == 'pro_fullprice':
sale_text = bui.Lstr(
resource='store.upgradeQuestionText',
fallback_resource='store.saleExclaimText',
).evaluate()
else:
sale_text = bui.Lstr(
resource='store.saleExclaimText',
fallback_resource='store.saleText',
).evaluate()
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 40),
size=(0, 0),
text=sale_text
+ (
(' ' + bui.Lstr(resource='store.oneTimeOnlyText').evaluate())
if self._offer['oneTimeOnly']
else ''
)
+ percent_off_text,
h_align='center',
v_align='center',
maxwidth=self._width * 0.9 - 220,
scale=1.4,
color=(0.3, 1, 0.3),
)
self._flash_on = False
self._flashing_timer: bui.AppTimer | None = bui.AppTimer(
0.05, bui.WeakCall(self._flash_cycle), repeat=True
)
bui.apptimer(0.6, bui.WeakCall(self._stop_flashing))
size = store.get_store_item_display_size(self._offer_item)
display: dict[str, Any] = {}
storeitemui.instantiate_store_item_display(
self._offer_item,
display,
parent_widget=self._root_widget,
b_pos=(
self._width * 0.5
- size[0] * 0.5
+ 10
- ((size[0] * 0.5 + 30) if self._is_bundle_sale else 0),
self._height * 0.5
- size[1] * 0.5
+ 20
+ (20 if self._is_bundle_sale else 0),
),
b_width=size[0],
b_height=size[1],
button=not self._is_bundle_sale,
)
# Wire up the parts we need.
if self._is_bundle_sale:
self._plus_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.5 + 50),
size=(0, 0),
text='+',
h_align='center',
v_align='center',
maxwidth=self._width * 0.9,
scale=1.4,
color=(0.5, 0.5, 0.5),
)
self._plus_tickets = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5 + 120, self._height * 0.5 + 50),
size=(0, 0),
text=bui.charstr(SpecialChar.TICKET_BACKING)
+ str(offer['bonusTickets']),
h_align='center',
v_align='center',
maxwidth=self._width * 0.9,
scale=2.5,
color=(0.2, 1, 0.2),
)
self._price_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, 150),
size=(0, 0),
text=real_price,
h_align='center',
v_align='center',
maxwidth=self._width * 0.9,
scale=1.4,
color=(0.2, 1, 0.2),
)
# Total-value if they supplied it.
total_worth_item = offer.get('valueItem', None)
if total_worth_item is not None:
price = plus.get_price(total_worth_item)
total_worth_price = (
store.get_clean_price(price) if price is not None else None
)
if total_worth_price is not None:
total_worth_text = bui.Lstr(
resource='store.totalWorthText',
subs=[('${TOTAL_WORTH}', total_worth_price)],
)
self._total_worth_text = bui.textwidget(
parent=self._root_widget,
text=total_worth_text,
position=(self._width * 0.5, 210),
scale=0.9,
maxwidth=self._width * 0.7,
size=(0, 0),
h_align='center',
v_align='center',
shadow=1.0,
flatness=1.0,
color=(0.3, 1, 1),
)
elif offer['item'] == 'pro_fullprice':
# for full-price pro we simply show full price
bui.textwidget(edit=display['price_widget'], text=real_price)
bui.buttonwidget(
edit=display['button'], on_activate_call=self._purchase
)
else:
# Show old/new prices otherwise (for pro sale).
bui.buttonwidget(
edit=display['button'], on_activate_call=self._purchase
)
bui.imagewidget(edit=display['price_slash_widget'], opacity=1.0)
bui.textwidget(
edit=display['price_widget_left'], text=original_price_str
)
bui.textwidget(
edit=display['price_widget_right'], text=new_price_str
)
# Add ticket button only if this is ticket-purchasable.
# if isinstance(offer.get('price'), int):
# self._get_tickets_button = bui.buttonwidget(
# parent=self._root_widget,
# position=(self._width - 125, self._height - 68),
# size=(90, 55),
# scale=1.0,
# button_type='square',
# color=(0.7, 0.5, 0.85),
# textcolor=(0.2, 1, 0.2),
# autoselect=True,
# label=bui.Lstr(resource='getTicketsWindow.titleText'),
# on_activate_call=self._on_get_more_tickets_press,
# )
# self._ticket_text_update_timer = bui.AppTimer(
# 1.0, bui.WeakCall(self._update_tickets_text), repeat=True
# )
# self._update_tickets_text()
self._update_timer = bui.AppTimer(
1.0, bui.WeakCall(self._update), repeat=True
)
self._cancel_button = bui.buttonwidget(
parent=self._root_widget,
position=(
(50, 40)
if self._is_bundle_sale
else (self._width * 0.5 - 75, 40)
),
size=(150, 60),
scale=1.0,
on_activate_call=self._cancel,
autoselect=True,
label=bui.Lstr(resource='noThanksText'),
)
self._cancel_countdown_text = bui.textwidget(
parent=self._root_widget,
text='',
position=(
(50 + 150 + 20, 40 + 27)
if self._is_bundle_sale
else (self._width * 0.5 - 75 + 150 + 20, 40 + 27)
),
scale=1.1,
size=(0, 0),
h_align='left',
v_align='center',
shadow=1.0,
flatness=1.0,
color=(0.6, 0.5, 0.5),
)
self._update_cancel_button_graphics()
if self._is_bundle_sale:
self._purchase_button = bui.buttonwidget(
parent=self._root_widget,
position=(self._width - 200, 40),
size=(150, 60),
scale=1.0,
on_activate_call=self._purchase,
autoselect=True,
label=bui.Lstr(resource='store.purchaseText'),
)
bui.containerwidget(
edit=self._root_widget,
cancel_button=self._cancel_button,
start_button=(
self._purchase_button if self._is_bundle_sale else None
),
selected_child=(
self._purchase_button
if self._is_bundle_sale
else display['button']
),
)
def _stop_flashing(self) -> None:
self._flashing_timer = None
bui.textwidget(edit=self._title_text, color=(0.3, 1, 0.3))
def _flash_cycle(self) -> None:
if not self._root_widget:
return
self._flash_on = not self._flash_on
bui.textwidget(
edit=self._title_text,
color=(0.3, 1, 0.3) if self._flash_on else (1, 0.5, 0),
)
def _update_cancel_button_graphics(self) -> None:
bui.buttonwidget(
edit=self._cancel_button,
color=(
(0.5, 0.5, 0.5) if self._cancel_delay > 0 else (0.7, 0.4, 0.34)
),
textcolor=(
(0.5, 0.5, 0.5) if self._cancel_delay > 0 else (0.9, 0.9, 1.0)
),
)
bui.textwidget(
edit=self._cancel_countdown_text,
text=str(self._cancel_delay) if self._cancel_delay > 0 else '',
)
def _update(self) -> None:
plus = bui.app.plus
assert plus is not None
# If we've got seconds left on our countdown, update it.
if self._cancel_delay > 0:
self._cancel_delay = max(0, self._cancel_delay - 1)
self._update_cancel_button_graphics()
can_die = False
# We go away if we see that our target item is owned.
if self._offer_item == 'pro':
assert bui.app.classic is not None
if bui.app.classic.accounts.have_pro():
can_die = True
else:
if plus.get_purchased(self._offer_item):
can_die = True
if can_die:
self._transition_out('out_left')
def _transition_out(self, transition: str = 'out_left') -> None:
# Also clear any pending-special-offer we've stored at this point.
cfg = bui.app.config
if 'pendingSpecialOffer' in cfg:
del cfg['pendingSpecialOffer']
cfg.commit()
bui.containerwidget(edit=self._root_widget, transition=transition)
# def _update_tickets_text(self) -> None:
# from babase import SpecialChar
# plus = bui.app.plus
# assert plus is not None
# if not self._root_widget:
# return
# sval: str | bui.Lstr
# if plus.get_v1_account_state() == 'signed_in':
# sval = bui.charstr(SpecialChar.TICKET) + str(
# plus.get_v1_account_ticket_count()
# )
# else:
# sval = bui.Lstr(resource='getTicketsWindow.titleText')
# bui.buttonwidget(edit=self._get_tickets_button, label=sval)
# def _on_get_more_tickets_press(self) -> None:
# from bauiv1lib import account
# from bauiv1lib import gettickets
# plus = bui.app.plus
# assert plus is not None
# if plus.get_v1_account_state() != 'signed_in':
# account.show_sign_in_prompt()
# return
# gettickets.GetTicketsWindow(modal=True).get_root_widget()
def _purchase(self) -> None:
# from bauiv1lib import gettickets
from bauiv1lib import confirm
plus = bui.app.plus
assert plus is not None
assert bui.app.classic is not None
store = bui.app.classic.store
if self._offer['item'] == 'pro':
plus.purchase('pro_sale')
elif self._offer['item'] == 'pro_fullprice':
plus.purchase('pro')
elif self._is_bundle_sale:
# With bundle sales, the price is the name of the IAP.
plus.purchase(self._offer['price'])
else:
ticket_count: int | None
try:
ticket_count = plus.get_v1_account_ticket_count()
except Exception:
ticket_count = None
if ticket_count is not None and ticket_count < self._offer['price']:
# gettickets.show_get_tickets_prompt()
bui.getsound('error').play()
return
def do_it() -> None:
assert plus is not None
plus.in_game_purchase(
'offer:' + str(self._offer['id']), self._offer['price']
)
bui.getsound('swish').play()
confirm.ConfirmWindow(
bui.Lstr(
resource='store.purchaseConfirmText',
subs=[
(
'${ITEM}',
store.get_store_item_name_translated(
self._offer['item']
),
)
],
),
width=400,
height=120,
action=do_it,
ok_text=bui.Lstr(
resource='store.purchaseText', fallback_resource='okText'
),
)
def _cancel(self) -> None:
if self._cancel_delay > 0:
bui.getsound('error').play()
return
self._transition_out('out_right')
def show_offer() -> bool:
"""(internal)"""
try:
from bauiv1lib import feedback
plus = bui.app.plus
assert plus is not None
app = bui.app
if app.classic is None:
raise RuntimeError(
'Classic feature-set is required to show offers.'
)
# Space things out a bit so we don't hit the poor user with an
# ad and then an in-game offer.
has_been_long_enough_since_ad = True
if app.classic.ads.last_ad_completion_time is not None and (
bui.apptime() - app.classic.ads.last_ad_completion_time < 30.0
):
has_been_long_enough_since_ad = False
if (
app.classic.special_offer is not None
and has_been_long_enough_since_ad
):
# Special case: for pro offers, store this in our prefs so
# we can re-show it if the user kills us (set phasers to
# 'NAG'!!!).
if app.classic.special_offer.get('item') == 'pro_fullprice':
cfg = app.config
cfg['pendingSpecialOffer'] = {
'a': plus.get_v1_account_public_login_id(),
'o': app.classic.special_offer,
}
cfg.commit()
if app.classic.special_offer['item'] == 'rating':
# Go with a native thing if we've got one.
if bui.native_review_request_supported():
bui.native_review_request()
else:
if app.ui_v1.available:
feedback.ask_for_rating()
else:
if app.ui_v1.available:
SpecialOfferWindow(app.classic.special_offer)
app.classic.special_offer = None
return True
except Exception:
logging.exception('Error showing offer.')
return False

View File

@ -125,55 +125,11 @@ class StoreBrowserWindow(bui.MainWindow):
bui.containerwidget(
edit=self._root_widget, on_cancel_call=self._back
)
# backbutton = bui.get_special_widget('back_button')
backbuttonspecial = True
else:
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
# backbutton = self._back_button
backbuttonspecial = False
# self._ticket_count_text: bui.Widget | None = None
# self._get_tickets_button: bui.Widget | None = None
# if bool(False):
# if app.classic.allow_ticket_purchases:
# self._get_tickets_button = bui.buttonwidget(
# parent=self._root_widget,
# size=(210, 65),
# on_activate_call=self._on_get_more_tickets_press,
# autoselect=True,
# scale=0.9,
# text_scale=1.4,
# left_widget=backbutton,
# color=(0.7, 0.5, 0.85),
# textcolor=(0.2, 1.0, 0.2),
# label=bui.Lstr(resource='getTicketsWindow.titleText'),
# )
# else:
# self._ticket_count_text = bui.textwidget(
# parent=self._root_widget,
# size=(210, 64),
# color=(0.2, 1.0, 0.2),
# h_align='center',
# v_align='center',
# )
# Move this dynamically to keep it out of the way of the party icon.
# self._update_get_tickets_button_pos()
# self._get_ticket_pos_update_timer = bui.AppTimer(
# 1.0,
# bui.WeakCall(self._update_get_tickets_button_pos),
# repeat=True,
# )
# if self._get_tickets_button and not backbuttonspecial:
# bui.widget(
# edit=self._back_button, right_widget=self._get_tickets_button
# )
# self._ticket_text_update_timer = bui.AppTimer(
# 1.0, bui.WeakCall(self._update_tickets_text), repeat=True
# )
# self._update_tickets_text()
if (
app.classic.platform in ['mac', 'ios']
and app.classic.subplatform == 'appstore'
@ -316,17 +272,6 @@ class StoreBrowserWindow(bui.MainWindow):
)
self._update_tabs()
# if self._get_tickets_button:
# last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button
# bui.widget(
# edit=self._get_tickets_button, down_widget=last_tab_button
# )
# bui.widget(
# edit=last_tab_button,
# up_widget=self._get_tickets_button,
# right_widget=self._get_tickets_button,
# )
if uiscale is bui.UIScale.SMALL:
first_tab_button = self._tab_row.tabs[tabs_def[0][0]].button
last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button
@ -346,28 +291,6 @@ class StoreBrowserWindow(bui.MainWindow):
self._status_textwidget: bui.Widget | None = None
self._restore_state()
# def _update_get_tickets_button_pos(self) -> None:
# assert bui.app.classic is not None
# uiscale = bui.app.ui_v1.uiscale
# pos = (
# self._width
# - 252
# - (
# self._x_inset
# + (
# 47
# if uiscale is bui.UIScale.SMALL
# and bui.is_party_icon_visible()
# else 0
# )
# ),
# self._height - 70,
# )
# if self._get_tickets_button:
# bui.buttonwidget(edit=self._get_tickets_button, position=pos)
# if self._ticket_count_text:
# bui.textwidget(edit=self._ticket_count_text, position=pos)
def _restore_purchases(self) -> None:
from bauiv1lib import account
@ -411,25 +334,6 @@ class StoreBrowserWindow(bui.MainWindow):
bui.textwidget(edit=tab_data['text'], text='')
bui.imagewidget(edit=tab_data['img'], opacity=0.0)
# def _update_tickets_text(self) -> None:
# from bauiv1 import SpecialChar
# if not self._root_widget:
# return
# plus = bui.app.plus
# assert plus is not None
# sval: str | bui.Lstr
# if plus.get_v1_account_state() == 'signed_in':
# sval = bui.charstr(SpecialChar.TICKET) + str(
# plus.get_v1_account_ticket_count()
# )
# else:
# sval = bui.Lstr(resource='getTicketsWindow.titleText')
# if self._get_tickets_button:
# bui.buttonwidget(edit=self._get_tickets_button, label=sval)
# if self._ticket_count_text:
# bui.textwidget(edit=self._ticket_count_text, text=sval)
def _set_tab(self, tab_id: TabID) -> None:
if self._current_tab is tab_id:
return
@ -491,7 +395,6 @@ class StoreBrowserWindow(bui.MainWindow):
window = self._window()
if window is not None and (window.request is self):
window.request = None
# noinspection PyProtectedMember
window._on_response(data)
# Kick off a server request.
@ -601,8 +504,6 @@ class StoreBrowserWindow(bui.MainWindow):
from bauiv1lib import account
from bauiv1lib.confirm import ConfirmWindow
# from bauiv1lib import gettickets
assert bui.app.classic is not None
store = bui.app.classic.store
@ -1292,8 +1193,6 @@ class StoreBrowserWindow(bui.MainWindow):
for tab_id, tab in self._tab_row.tabs.items()
if sel == tab.button
]
# if sel == self._get_tickets_button:
# sel_name = 'GetTickets'
if sel == self._scrollwidget:
sel_name = 'Scroll'
elif sel == self._back_button:
@ -1327,8 +1226,6 @@ class StoreBrowserWindow(bui.MainWindow):
if self._show_tab is not None:
current_tab = self._show_tab
# if sel_name == 'GetTickets' and self._get_tickets_button:
# sel = self._get_tickets_button
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'Scroll':
@ -1342,7 +1239,7 @@ class StoreBrowserWindow(bui.MainWindow):
else:
sel = self._tab_row.tabs[current_tab].button
# If we were requested to show a tab, select it too..
# If we were requested to show a tab, select it too.
if (
self._show_tab is not None
and self._show_tab in self._tab_row.tabs
@ -1354,63 +1251,21 @@ class StoreBrowserWindow(bui.MainWindow):
except Exception:
logging.exception('Error restoring state for %s.', self)
# def _on_get_more_tickets_press(self) -> None:
# # pylint: disable=cyclic-import
# from bauiv1lib.account import show_sign_in_prompt
# from bauiv1lib.gettickets import GetTicketsWindow
# # no-op if our underlying widget is dead or on its way out.
# if not self._root_widget or self._root_widget.transitioning_out:
# return
# plus = bui.app.plus
# assert plus is not None
# if plus.get_v1_account_state() != 'signed_in':
# show_sign_in_prompt()
# return
# self._save_state()
# bui.containerwidget(edit=self._root_widget, transition='out_left')
# window = GetTicketsWindow(
# from_modal_store=self._modal,
# store_back_location=self._back_location,
# )
# if not self._modal:
# assert bui.app.classic is not None
# bui.app.ui_v1.set_main_window(window, from_window=self)
def _back(self) -> None:
# pylint: disable=cyclic-import
# from bauiv1lib.coop.browser import CoopBrowserWindow
# from bauiv1lib.mainmenu import MainMenuWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
if self._modal:
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
else:
# no-op if we're not currently in control.
if not self.main_window_has_control():
return
self.main_window_back()
# if not self._modal:
# assert bui.app.classic is not None
# if self._back_location == 'CoopBrowserWindow':
# bui.app.ui_v1.set_main_window(
# CoopBrowserWindow(transition='in_left'),
# from_window=self,
# is_back=True,
# )
# else:
# bui.app.ui_v1.set_main_window(
# MainMenuWindow(transition='in_left'),
# from_window=self,
# is_back=True,
# is_top_level=True,
# )
if self._on_close_call is not None:
self._on_close_call()

View File

@ -66,6 +66,6 @@ void AppMode::LanguageChanged() {}
auto AppMode::LastClientJoinTime() const -> millisecs_t { return -1; }
auto AppMode::InClassicMainMenuSession() const -> bool { return false; }
auto AppMode::IsInMainMenu() const -> bool { return false; }
} // namespace ballistica::base

View File

@ -68,8 +68,8 @@ class AppMode {
/// Called when language changes.
virtual void LanguageChanged();
/// Are we currently in a classic 'main menu' session?
virtual auto InClassicMainMenuSession() const -> bool;
/// Are we currently in a 'main menu' situation (as opposed to gameplay)?
virtual auto IsInMainMenu() const -> bool;
/// Get current party size (for legacy parties).
virtual auto GetPartySize() const -> int;

View File

@ -6,6 +6,7 @@
#include <unordered_map>
#include "ballistica/base/app_adapter/app_adapter.h"
#include "ballistica/base/app_mode/app_mode.h"
#include "ballistica/base/assets/sound_asset.h"
#include "ballistica/base/input/input.h"
#include "ballistica/base/platform/base_platform.h"
@ -350,6 +351,37 @@ static PyMethodDef PyInLogicThreadDef = {
"Returns whether or not the current thread is the logic thread.",
};
// ------------------------------ in_main_menu ---------------------------------
static auto PyInMainMenu(PyObject* self, PyObject* args,
PyObject* keywds) -> PyObject* {
BA_PYTHON_TRY;
static const char* kwlist[] = {nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "",
const_cast<char**>(kwlist))) {
return nullptr;
}
BA_PRECONDITION(g_base->InLogicThread());
if (g_base->app_mode()->IsInMainMenu()) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
BA_PYTHON_CATCH;
}
static PyMethodDef PyInMainMenuDef = {
"in_main_menu", // name
(PyCFunction)PyInMainMenu, // method
METH_VARARGS | METH_KEYWORDS, // flags
"in_main_menu() -> bool\n"
"\n"
"(internal)\n"
"\n"
"Returns whether or not the app-mode is currently in a main menu\n"
"situation (as opposed to gameplay).",
};
// ----------------------------- set_thread_name -------------------------------
static auto PySetThreadName(PyObject* self, PyObject* args,
@ -1949,6 +1981,7 @@ auto PythonMoethodsBase3::GetMethods() -> std::vector<PyMethodDef> {
PyGetThreadNameDef,
PySetThreadNameDef,
PyInLogicThreadDef,
PyInMainMenuDef,
PyRequestPermissionDef,
PyHavePermissionDef,
PyUnlockAllInputDef,

View File

@ -67,7 +67,7 @@ base::InputDeviceDelegate* ClassicAppMode::CreateInputDeviceDelegate(
// Go with 5 minute ban.
const int kKickBanSeconds = 5 * 60;
bool ClassicAppMode::InClassicMainMenuSession() const {
bool ClassicAppMode::IsInMainMenu() const {
scene_v1::HostSession* hostsession =
scene_v1::ContextRefSceneV1::FromAppForegroundContext().GetHostSession();
return (hostsession && hostsession->is_main_menu());

View File

@ -155,7 +155,7 @@ class ClassicAppMode : public base::AppMode {
void OnAppStart() override;
void OnAppSuspend() override;
void OnAppUnsuspend() override;
auto InClassicMainMenuSession() const -> bool override;
auto IsInMainMenu() const -> bool override;
auto CreateInputDeviceDelegate(base::InputDevice* device)
-> base::InputDeviceDelegate* override;

View File

@ -39,9 +39,9 @@ auto main(int argc, char** argv) -> int {
namespace ballistica {
// These are set automatically via script; don't modify them here.
const int kEngineBuildNumber = 21968;
const int kEngineBuildNumber = 21977;
const char* kEngineVersion = "1.7.37";
const int kEngineApiVersion = 8;
const int kEngineApiVersion = 9;
#if BA_MONOLITHIC_BUILD

View File

@ -3,7 +3,9 @@
#include "ballistica/ui_v1/widget/root_widget.h"
#include "ballistica/base/app_mode/app_mode.h"
#include "ballistica/base/graphics/renderer/renderer.h"
#include "ballistica/base/assets/assets.h"
#include "ballistica/base/graphics/renderer/render_pass.h"
#include "ballistica/base/graphics/support/frame_def.h"
#include "ballistica/shared/foundation/inline.h"
#include "ballistica/ui_v1/python/ui_v1_python.h"
#include "ballistica/ui_v1/widget/button_widget.h"
@ -43,6 +45,8 @@ struct RootWidget::ButtonDef {
uint32_t visibility_mask{};
bool selectable{true};
bool enable_sound{true};
bool allow_in_main_menu{true};
bool allow_in_game{true};
float h_align{};
float x{};
float y{};
@ -81,6 +85,8 @@ struct RootWidget::Button {
bool selectable{true};
bool fully_offscreen{};
bool enabled{};
bool allow_in_main_menu{true};
bool allow_in_game{true};
uint32_t visibility_mask{};
};
@ -222,6 +228,8 @@ void RootWidget::AddMeter_(MeterType type, float h_align, float r, float g,
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullNoBack)
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullRoot));
bd.allow_in_game = false;
// Show some in store mode.
if (type == MeterType::kLevel || type == MeterType::kTickets) {
bd.visibility_mask |=
@ -491,6 +499,8 @@ void RootWidget::AddMeter_(MeterType type, float h_align, float r, float g,
}
bd.pre_buffer = -10.0f;
bd.allow_in_game = false;
Button* btn = AddButton_(bd);
if (type == MeterType::kTokens) {
get_tokens_button_ = btn;
@ -662,11 +672,7 @@ void RootWidget::Setup() {
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullNoBack)
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullRoot));
// on desktop, stick this in the top left corner
// if (g_ui->scale() == UIScale::kLarge) {
// bd.h_align = 0.0f;
// bd.x = 120.0f;
// }
bd.allow_in_game = false;
Button* b = account_button_ = AddButton_(bd);
top_left_buttons_.push_back(b);
@ -675,9 +681,9 @@ void RootWidget::Setup() {
{
TextDef td;
td.button = b;
td.y = 9.0f;
td.width = bd.width * 0.9f;
td.text = "Player Name";
td.y = 0.0f;
td.width = bd.width * 0.8f;
td.text = "AccountName";
td.scale = 1.2f;
td.depth_min = 0.3f;
td.color_r = 0.5f;
@ -688,19 +694,19 @@ void RootWidget::Setup() {
}
// Clan.
{
TextDef td;
td.button = b;
td.y = -12.0f;
td.width = bd.width * 0.9f;
td.depth_min = 0.3f;
td.text = "Clan Name";
td.color_a = 0.6f;
td.scale = 0.6f;
td.flatness = 1.0f;
td.shadow = 0.0f;
AddText_(td);
}
// {
// TextDef td;
// td.button = b;
// td.y = -12.0f;
// td.width = bd.width * 0.9f;
// td.depth_min = 0.3f;
// td.text = "Clan Name";
// td.color_a = 0.6f;
// td.scale = 0.6f;
// td.flatness = 1.0f;
// td.shadow = 0.0f;
// AddText_(td);
// }
}
// float anchorx = 0.0f;
@ -737,6 +743,7 @@ void RootWidget::Setup() {
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullRoot));
b.pre_buffer = 5.0f;
b.enable_sound = false;
b.allow_in_main_menu = false;
menu_button_ = AddButton_(b);
top_right_buttons_.push_back(menu_button_);
}
@ -795,6 +802,7 @@ void RootWidget::Setup() {
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullNoBack)
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullRoot));
b.pre_buffer = 20.0f;
b.allow_in_game = false;
inbox_button_ = AddButton_(b);
bottom_left_buttons_.push_back(inbox_button_);
}
@ -819,6 +827,7 @@ void RootWidget::Setup() {
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullNoBack)
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullRoot));
b.pre_buffer = 20.0f;
b.allow_in_game = false;
achievements_button_ = AddButton_(b);
bottom_left_buttons_.push_back(achievements_button_);
bx += 80.0f;
@ -1007,7 +1016,7 @@ void RootWidget::Setup() {
(static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFull)
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullNoBack)
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullRoot));
bd.allow_in_game = false;
AddButton_(bd);
}
@ -1026,6 +1035,7 @@ void RootWidget::Setup() {
float spacing = 130.0f;
b.x = -1.5f * spacing;
b.call = UIV1Python::ObjID::kRootUIChestSlot1PressCall;
b.allow_in_game = false;
AddButton_(b);
b.x = -0.5f * spacing;
@ -1090,6 +1100,7 @@ void RootWidget::Setup() {
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullRoot));
b.disable_offset_scale = 1.5f;
b.pre_buffer = 20.0f;
b.allow_in_game = false;
inventory_button_ = AddButton_(b);
bottom_right_buttons_.push_back(inventory_button_);
}
@ -1109,6 +1120,8 @@ void RootWidget::Setup() {
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullNoBack)
| static_cast<uint32_t>(Widget::ToolbarVisibility::kMenuFullRoot));
b.pre_buffer = 20.0f;
b.allow_in_game = false;
store_button_ = AddButton_(b);
bottom_right_buttons_.push_back(store_button_);
}
@ -1147,6 +1160,8 @@ auto RootWidget::AddButton_(const ButtonDef& def) -> RootWidget::Button* {
b.h_align = def.h_align;
b.v_align = def.v_align;
b.selectable = def.selectable;
b.allow_in_game = def.allow_in_game;
b.allow_in_main_menu = def.allow_in_main_menu;
b.widget = Object::New<ButtonWidget>();
b.widget->SetColor(def.color_r, def.color_g, def.color_b);
b.widget->set_opacity(def.opacity);
@ -1246,11 +1261,8 @@ void RootWidget::UpdateForFocusedWindow() {
}
void RootWidget::UpdateForFocusedWindow_(Widget* widget) {
// Take note if the current session is the main menu; we do a few things
// differently there.
//
// FIXME - need a more generalized way to determine this.
in_main_menu_ = g_base->app_mode()->InClassicMainMenuSession();
// Take note whether we're currently in a main menu vs gameplay.
in_main_menu_ = g_base->app_mode()->IsInMainMenu();
if (widget == nullptr) {
toolbar_visibility_ = ToolbarVisibility::kInGame;
@ -1278,9 +1290,23 @@ void RootWidget::StepPositions_(float dt) {
// When we're in the main menu, always disable the menu button and
// shift the party button a bit to the right
if (in_main_menu_) {
if (&b == menu_button_) {
if (!b.allow_in_main_menu) {
enable_button = false;
}
// Disallow menu button in the main menu.
// if (&b == menu_button_) {
// enable_button = false;
// }
} else {
if (!b.allow_in_game) {
enable_button = false;
}
// Disallow most buttons in-game.
// if (&b == inbox_button_ || &b == achievements_button_
// || &b == account_button_ || &b == level_meter_button_
// || &b == trophy_meter_button_ || &b == store_button_ || &b == ) {
// enable_button = false;
// }
}
if (&b == back_button_) {
// Back button is always disabled in medium/large UI.