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/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1",
"build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae", "build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "0a166c970637de5e318e4f07e8c0aeae", "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "d6246c930e7e2d2d9a6aff6788f33b69",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "71fee3398234820d038235f02ea7ed6a", "build/prefab/full/linux_arm64_gui/release/ballisticakit": "001c67c4d4d33e20755399e0b2ed1593",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "682d622e82fbdf7d41f170146ae8cf6f", "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "6f6bfaf19daf6e866f4fecbc889b8854",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "ed9a7e820ab3a59d6326b898973d5cff", "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "ebc7449903d7868c631c504aed10f371",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "4efa411d0d43debb7c8db1db1d9a4658", "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "91f03e7dbfc0d7eb75568704f681fba5",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "8892f774f2d079604d77241adaf3e529", "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "0b32b6eb05df0a7e23e55ff2e7235a8f",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "c479f3ab633f0b19f3e733943b3c1c3b", "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "4ed4b63cc506815f759412a295bfe088",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "2797e7b22388c68b2e41a1eb7c820a62", "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "1890731fb8a2fb43c4cde72af9d7a4e6",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "f58b978d2978fdf6e2f7f0cc13fbb2c7", "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "1a4764a504dcb20591ac3472c48db8d9",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "1ad711461c2a65ecb1fbab1aac2aacae", "build/prefab/full/mac_arm64_gui/release/ballisticakit": "9bfd6234aac4ffd7d7601a40017a73be",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "ec90ed3ec92a6db3f8b9651f2464d8d4", "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "941f8fb79d54522ca5df97a735c3babe",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "12cc0539e86a408b01ac3b3a74cff98a", "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "290a3d9840efc5e88532e13b76b3ae6b",
"build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "1f59835f71e01827f056f503302c45c4", "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "621a6684b54b0a9101f808209bcca1ee",
"build/prefab/full/mac_x86_64_gui/release/ballisticakit": "c747103d9fe3206f94b54ed9ae5d4aa4", "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "43c8b0b29e5fb257e4cde0f7fce1c680",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "603f77208cb0df9161b84387631b8778", "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "9a30c6b88ffc11bdf6765780616d8ba1",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "00a72972b6b0531b176406ad0e8d243f", "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "8a8f63850fc296b060a994f4e001d74f",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "af0063f60bb84a65ca71df83d00b9507", "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "2864e9ae1d2d566def85f1cdf6e863fe",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "817b90e953f655e83cd8e69d1feb5526", "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "dc0db7ca99661a896634fc05187a5c75",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "c132d48c3b560ac0dcf090a3bce8ba26", "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "f56bb0fafe0a45cecdcd06a10a6924c9",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "b3c44cd6282d4100b730e997baa3f28e", "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/debug/libballisticaplus.a": "73ad3303fe1a82005918fbc5dae3446c",
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "fa659b5d6119acba6570c92ce4d35ae2", "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "fa659b5d6119acba6570c92ce4d35ae2",
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "73ad3303fe1a82005918fbc5dae3446c", "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_gui/release/libballisticaplus.a": "3e5c5fd0a09f55ba7b05ce1e2ec7171e",
"build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "1659535e95e3047fda529543e265ac97", "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/mac_x86_64_server/release/libballisticaplus.a": "3e5c5fd0a09f55ba7b05ce1e2ec7171e",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "d4aca724c3184c6c2c82e7b11c1a352a", "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "6b6aae30362b1e9aaf3f1a8daae0b0b4",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "f4fbab89e11f52329d65a9afcabc2e5e", "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "25db08ee2b4c79ca631e517b622c741d",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "09733e1291806ece13f108ad6fb72b68", "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "2ea419644782c4c52d1fa784d0ced86d",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "099e20f57cfa2c36bf7b44e389d01d9a", "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "c40627a46ee127997153df041e47eda5",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "aae5ce60761c700d23a480cac83a2df5", "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "fff81b29e2d86031ec25c4a672a243e7",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "1fdc6ee85373af9df3a0b5eb601990af", "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "2a87e2f53c658d0be3918476cd8a04ee",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "809e62f1d2ac54c239a3a13c2d2c8ee7", "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "1e91536979b318b9a8325ab0e871ecec",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "68b49b66a76b9ab0d1b17de2bab19c04", "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/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "cb299985623bbcc86015cb103a424ae6", "src/assets/ba_data/python/babase/_mgen/enums.py": "cb299985623bbcc86015cb103a424ae6",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "efa61468cf098f77cc6a234461d8b86d", "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. - Playlist customization no longer requires pro.
- Soundtrack 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 - 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 several years. This includes a number of handy things such as consistent
buttons and widgets for league status, currencies, inventory, and the store. 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 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 where they came from. There are also other benefits such as better state
saving/restoring. When writing a MainWindow, pretty much all navigation should 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()`. and `main_window_replace()`.
- Finally got things updated so language testing works again, and made it a bit - 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 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__/resourcetypeinfo.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/sendinfo.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__/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__/tabs.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/teamnamescolors.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", "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/edit.py",
"ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py", "ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py",
"ba_data/python/bauiv1lib/soundtrack/macmusicapp.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/__init__.py",
"ba_data/python/bauiv1lib/store/__pycache__/__init__.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/store/__pycache__/__init__.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/store/__pycache__/browser.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/edit.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.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/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/__init__.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/store/browser.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/store/browser.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/store/button.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__/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__/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/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__/__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__/browser.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/store/__pycache__/button.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_chars,
have_permission, have_permission,
in_logic_thread, in_logic_thread,
in_main_menu,
increment_analytics_count, increment_analytics_count,
invoke_main_menu, invoke_main_menu,
is_os_playing_music, is_os_playing_music,
@ -262,6 +263,7 @@ __all__ = [
'have_chars', 'have_chars',
'have_permission', 'have_permission',
'in_logic_thread', 'in_logic_thread',
'in_main_menu',
'increment_analytics_count', 'increment_analytics_count',
'InputDeviceNotFoundError', 'InputDeviceNotFoundError',
'InputType', 'InputType',

View File

@ -16,7 +16,7 @@ classic submodules directly, it is much harder to make it cleanly handle
classic not being present. 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* # Note: Code relying on classic should import things from here *only*
# for type-checking and use the versions in ba*.app.classic at runtime; # for type-checking and use the versions in ba*.app.classic at runtime;

View File

@ -15,6 +15,7 @@ from babase import (
AppIntentDefault, AppIntentDefault,
invoke_main_menu, invoke_main_menu,
screenmessage, screenmessage,
in_main_menu,
) )
import _baclassic import _baclassic
@ -120,20 +121,28 @@ class ClassicAppMode(AppMode):
def _jump_to_main_window(self, window: MainWindow) -> None: def _jump_to_main_window(self, window: MainWindow) -> None:
"""Jump to a window with the main menu as its parent.""" """Jump to a window with the main menu as its parent."""
from bauiv1lib.mainmenu import MainMenuWindow from bauiv1lib.mainmenu import MainMenuWindow
from bauiv1lib.ingamemenu import InGameMenuWindow
ui = app.ui_v1 ui = app.ui_v1
old_window = ui.get_main_window() 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) old_window.main_window_replace(window)
else: else:
# Blow away the window stack. # Blow away the window stack and build a fresh one.
ui.clear_main_window() ui.clear_main_window()
ui.set_main_window( ui.set_main_window(
window, window,
from_window=False, # Disable from-check. 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: def _root_ui_menu_press(self) -> None:

View File

@ -216,24 +216,6 @@ class ClassicAppSubsystem(babase.AppSubsystem):
cfg['launchCount'] = launch_count cfg['launchCount'] = launch_count
cfg.commit() 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 # If there's a leftover log file, attempt to upload it to the
# master-server and/or get rid of it. # master-server and/or get rid of it.
babase.handle_leftover_v1_cloud_log_file() babase.handle_leftover_v1_cloud_log_file()
@ -832,7 +814,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
app = bauiv1.app app = bauiv1.app
env = app.env env = app.env
with bascenev1.ContextRef.empty(): with bascenev1.ContextRef.empty():
from bauiv1lib import specialoffer # from bauiv1lib import specialoffer
assert app.classic is not None assert app.classic is not None
if app.env.headless: if app.env.headless:
@ -927,11 +909,11 @@ class ClassicAppSubsystem(babase.AppSubsystem):
# (we may not have heard back from the server) # (we may not have heard back from the server)
# ..if that doesn't work they'll just have to wait # ..if that doesn't work they'll just have to wait
# until the next opportunity. # until the next opportunity.
if not specialoffer.show_offer(): # if not specialoffer.show_offer():
def try_again() -> None: # def try_again() -> None:
if not specialoffer.show_offer(): # if not specialoffer.show_offer():
# Try one last time.. # # Try one last time..
bauiv1.apptimer(2.0, specialoffer.show_offer) # 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 # Build number and version of the ballistica binary we expect to be
# using. # using.
TARGET_BALLISTICA_BUILD = 21968 TARGET_BALLISTICA_BUILD = 21977
TARGET_BALLISTICA_VERSION = '1.7.37' TARGET_BALLISTICA_VERSION = '1.7.37'

View File

@ -2,7 +2,7 @@
# #
"""Gameplay-centric api for classic BombSquad.""" """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 # 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 # from other modules/packages. Code *within* this package should import

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
"""Implements football games (both co-op and teams varieties).""" """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) # (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations from __future__ import annotations

View File

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

View File

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

View File

@ -2,7 +2,7 @@
# #
"""Defines the King of the Hill game.""" """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) # (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations from __future__ import annotations

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
# #
"""Ballistica Template Feature Set - just an example.""" """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 # Package up various private bits (including stuff from our native
# module) into a nice clean public API. # module) into a nice clean public API.

View File

@ -2,7 +2,7 @@
# #
"""Ballistica user interface api version 1""" """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. # 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 # It should only be imported by code outside of this package or
@ -60,6 +60,7 @@ from babase import (
getclass, getclass,
have_permission, have_permission,
in_logic_thread, in_logic_thread,
in_main_menu,
increment_analytics_count, increment_analytics_count,
is_browser_likely_available, is_browser_likely_available,
is_xcode_build, is_xcode_build,
@ -188,6 +189,7 @@ __all__ = [
'hscrollwidget', 'hscrollwidget',
'imagewidget', 'imagewidget',
'in_logic_thread', 'in_logic_thread',
'in_main_menu',
'increment_analytics_count', 'increment_analytics_count',
'is_browser_likely_available', 'is_browser_likely_available',
'is_party_icon_visible', 'is_party_icon_visible',

View File

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

View File

@ -2,4 +2,4 @@
# #
"""Library of stuff using the bauiv1 api: windows, custom controls, etc.""" """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: if TYPE_CHECKING:
from typing import Any, Sequence from typing import Any, Sequence
REQUIRE_PRO = False
class ColorPicker(PopupWindow): class ColorPicker(PopupWindow):
"""A popup UI to select from a set of colors. """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), on_activate_call=bui.WeakCall(self._select_other),
) )
# Custom colors are limited to pro currently.
assert bui.app.classic is not None 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( bui.imagewidget(
parent=self.root_widget, parent=self.root_widget,
position=(50, 12), position=(50, 12),
@ -137,7 +138,7 @@ class ColorPicker(PopupWindow):
# Requires pro. # Requires pro.
assert bui.app.classic is not None 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']) purchase.PurchaseWindow(items=['pro'])
self._transition_out() self._transition_out()
return return

View File

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

View File

@ -49,7 +49,11 @@ class InGameMenuWindow(bui.MainWindow):
@override @override
def get_main_window_state(self) -> bui.MainWindowState: def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes. # 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( return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls( create_call=lambda transition, origin_widget: cls(
transition=transition, origin_widget=origin_widget transition=transition, origin_widget=origin_widget

View File

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

View File

@ -153,6 +153,33 @@ class MainMenuWindow(bui.MainWindow):
app = bui.app app = bui.app
assert app.classic is not None 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 ( self._have_quit_button = app.classic.platform in (
'windows', 'windows',
@ -183,7 +210,6 @@ class MainMenuWindow(bui.MainWindow):
side_button_2_y_offs = 10.0 side_button_2_y_offs = 10.0
side_button_2_scale = 0.5 side_button_2_scale = 0.5
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL: if uiscale is bui.UIScale.SMALL:
root_widget_scale = 1.3 root_widget_scale = 1.3
button_y_offs = -20.0 button_y_offs = -20.0
@ -204,6 +230,24 @@ class MainMenuWindow(bui.MainWindow):
scale=root_widget_scale, 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. # In kiosk mode, provide a button to get back to the kiosk menu.
if bui.app.env.demo or bui.app.env.arcade: if bui.app.env.demo or bui.app.env.arcade:
# h, v, scale = positions[self._p_index] # h, v, scale = positions[self._p_index]
@ -443,7 +487,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.confirm import QuitWindow from bauiv1lib.confirm import QuitWindow
# no-op if we're not currently in control. # no-op if we're not currently in control.
if not self.can_change_main_window(): if not self.main_window_has_control():
return return
# Note: Normally we should go through bui.quit(confirm=True) but # Note: Normally we should go through bui.quit(confirm=True) but
@ -456,7 +500,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.credits import CreditsWindow from bauiv1lib.credits import CreditsWindow
# no-op if we're not currently in control. # no-op if we're not currently in control.
if not self.can_change_main_window(): if not self.main_window_has_control():
return return
self.main_window_replace( self.main_window_replace(
@ -469,7 +513,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.helpui import HelpWindow from bauiv1lib.helpui import HelpWindow
# no-op if we're not currently in control. # no-op if we're not currently in control.
if not self.can_change_main_window(): if not self.main_window_has_control():
return return
self.main_window_replace( self.main_window_replace(
@ -536,7 +580,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.gather import GatherWindow from bauiv1lib.gather import GatherWindow
# no-op if we're not currently in control. # no-op if we're not currently in control.
if not self.can_change_main_window(): if not self.main_window_has_control():
return return
self.main_window_replace( self.main_window_replace(
@ -548,7 +592,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.watch import WatchWindow from bauiv1lib.watch import WatchWindow
# no-op if we're not currently in control. # no-op if we're not currently in control.
if not self.can_change_main_window(): if not self.main_window_has_control():
return return
self.main_window_replace( self.main_window_replace(
@ -560,7 +604,7 @@ class MainMenuWindow(bui.MainWindow):
from bauiv1lib.play import PlayWindow from bauiv1lib.play import PlayWindow
# no-op if we're not currently in control. # no-op if we're not currently in control.
if not self.can_change_main_window(): if not self.main_window_has_control():
return return
classic = bui.app.classic classic = bui.app.classic

View File

@ -541,7 +541,7 @@ class PlayWindow(bui.MainWindow):
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
# no-op if we're not currently in control. # no-op if we're not currently in control.
if not self.can_change_main_window(): if not self.main_window_has_control():
return return
if self._is_main_menu: if self._is_main_menu:
@ -567,7 +567,7 @@ class PlayWindow(bui.MainWindow):
from bauiv1lib.coop.browser import CoopBrowserWindow from bauiv1lib.coop.browser import CoopBrowserWindow
# no-op if we're not currently in control. # no-op if we're not currently in control.
if not self.can_change_main_window(): if not self.main_window_has_control():
return return
plus = bui.app.plus plus = bui.app.plus
@ -588,7 +588,7 @@ class PlayWindow(bui.MainWindow):
from bauiv1lib.playlist.browser import PlaylistBrowserWindow from bauiv1lib.playlist.browser import PlaylistBrowserWindow
# no-op if we're not currently in control. # no-op if we're not currently in control.
if not self.can_change_main_window(): if not self.main_window_has_control():
return return
self._save_state() self._save_state()
@ -604,7 +604,7 @@ class PlayWindow(bui.MainWindow):
from bauiv1lib.playlist.browser import PlaylistBrowserWindow from bauiv1lib.playlist.browser import PlaylistBrowserWindow
# no-op if we're not currently in control. # no-op if we're not currently in control.
if not self.can_change_main_window(): if not self.main_window_has_control():
return return
self._save_state() 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( bui.containerwidget(
edit=self._root_widget, on_cancel_call=self._back edit=self._root_widget, on_cancel_call=self._back
) )
# backbutton = bui.get_special_widget('back_button')
backbuttonspecial = True backbuttonspecial = True
else: else:
bui.containerwidget(edit=self._root_widget, cancel_button=btn) bui.containerwidget(edit=self._root_widget, cancel_button=btn)
# backbutton = self._back_button
backbuttonspecial = False 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 ( if (
app.classic.platform in ['mac', 'ios'] app.classic.platform in ['mac', 'ios']
and app.classic.subplatform == 'appstore' and app.classic.subplatform == 'appstore'
@ -316,17 +272,6 @@ class StoreBrowserWindow(bui.MainWindow):
) )
self._update_tabs() 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: if uiscale is bui.UIScale.SMALL:
first_tab_button = self._tab_row.tabs[tabs_def[0][0]].button first_tab_button = self._tab_row.tabs[tabs_def[0][0]].button
last_tab_button = self._tab_row.tabs[tabs_def[-1][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._status_textwidget: bui.Widget | None = None
self._restore_state() 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: def _restore_purchases(self) -> None:
from bauiv1lib import account from bauiv1lib import account
@ -411,25 +334,6 @@ class StoreBrowserWindow(bui.MainWindow):
bui.textwidget(edit=tab_data['text'], text='') bui.textwidget(edit=tab_data['text'], text='')
bui.imagewidget(edit=tab_data['img'], opacity=0.0) 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: def _set_tab(self, tab_id: TabID) -> None:
if self._current_tab is tab_id: if self._current_tab is tab_id:
return return
@ -491,7 +395,6 @@ class StoreBrowserWindow(bui.MainWindow):
window = self._window() window = self._window()
if window is not None and (window.request is self): if window is not None and (window.request is self):
window.request = None window.request = None
# noinspection PyProtectedMember
window._on_response(data) window._on_response(data)
# Kick off a server request. # Kick off a server request.
@ -601,8 +504,6 @@ class StoreBrowserWindow(bui.MainWindow):
from bauiv1lib import account from bauiv1lib import account
from bauiv1lib.confirm import ConfirmWindow from bauiv1lib.confirm import ConfirmWindow
# from bauiv1lib import gettickets
assert bui.app.classic is not None assert bui.app.classic is not None
store = bui.app.classic.store store = bui.app.classic.store
@ -1292,8 +1193,6 @@ class StoreBrowserWindow(bui.MainWindow):
for tab_id, tab in self._tab_row.tabs.items() for tab_id, tab in self._tab_row.tabs.items()
if sel == tab.button if sel == tab.button
] ]
# if sel == self._get_tickets_button:
# sel_name = 'GetTickets'
if sel == self._scrollwidget: if sel == self._scrollwidget:
sel_name = 'Scroll' sel_name = 'Scroll'
elif sel == self._back_button: elif sel == self._back_button:
@ -1327,8 +1226,6 @@ class StoreBrowserWindow(bui.MainWindow):
if self._show_tab is not None: if self._show_tab is not None:
current_tab = self._show_tab current_tab = self._show_tab
# if sel_name == 'GetTickets' and self._get_tickets_button:
# sel = self._get_tickets_button
if sel_name == 'Back': if sel_name == 'Back':
sel = self._back_button sel = self._back_button
elif sel_name == 'Scroll': elif sel_name == 'Scroll':
@ -1342,7 +1239,7 @@ class StoreBrowserWindow(bui.MainWindow):
else: else:
sel = self._tab_row.tabs[current_tab].button 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 ( if (
self._show_tab is not None self._show_tab is not None
and self._show_tab in self._tab_row.tabs and self._show_tab in self._tab_row.tabs
@ -1354,63 +1251,21 @@ class StoreBrowserWindow(bui.MainWindow):
except Exception: except Exception:
logging.exception('Error restoring state for %s.', self) 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: 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: 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( bui.containerwidget(
edit=self._root_widget, transition=self._transition_out edit=self._root_widget, transition=self._transition_out
) )
else: else:
# no-op if we're not currently in control.
if not self.main_window_has_control():
return
self.main_window_back() 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: if self._on_close_call is not None:
self._on_close_call() self._on_close_call()

View File

@ -66,6 +66,6 @@ void AppMode::LanguageChanged() {}
auto AppMode::LastClientJoinTime() const -> millisecs_t { return -1; } 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 } // namespace ballistica::base

View File

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

View File

@ -6,6 +6,7 @@
#include <unordered_map> #include <unordered_map>
#include "ballistica/base/app_adapter/app_adapter.h" #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/assets/sound_asset.h"
#include "ballistica/base/input/input.h" #include "ballistica/base/input/input.h"
#include "ballistica/base/platform/base_platform.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.", "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 ------------------------------- // ----------------------------- set_thread_name -------------------------------
static auto PySetThreadName(PyObject* self, PyObject* args, static auto PySetThreadName(PyObject* self, PyObject* args,
@ -1949,6 +1981,7 @@ auto PythonMoethodsBase3::GetMethods() -> std::vector<PyMethodDef> {
PyGetThreadNameDef, PyGetThreadNameDef,
PySetThreadNameDef, PySetThreadNameDef,
PyInLogicThreadDef, PyInLogicThreadDef,
PyInMainMenuDef,
PyRequestPermissionDef, PyRequestPermissionDef,
PyHavePermissionDef, PyHavePermissionDef,
PyUnlockAllInputDef, PyUnlockAllInputDef,

View File

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

View File

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

View File

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

View File

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