From 2190568baf6176c4aa50fc448a1b116b6fdd0de2 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Sat, 11 Jan 2025 14:42:12 -0800 Subject: [PATCH] Lots of stuff. Chests now functional for tourneys --- .efrocachemap | 111 ++- CHANGELOG.md | 17 +- ballisticakit-cmake/CMakeLists.txt | 2 + .../Generic/BallisticaKitGeneric.vcxproj | 2 + .../BallisticaKitGeneric.vcxproj.filters | 6 + .../Headless/BallisticaKitHeadless.vcxproj | 2 + .../BallisticaKitHeadless.vcxproj.filters | 6 + config/requirements.txt | 2 +- src/assets/.asset_manifest_private.json | 23 + src/assets/.asset_manifest_public.json | 8 + src/assets/Makefile | 31 + src/assets/ba_data/python/babase/_app.py | 7 +- src/assets/ba_data/python/babase/_apputils.py | 4 +- .../ba_data/python/baclassic/__init__.py | 25 +- .../ba_data/python/baclassic/_appmode.py | 46 +- .../ba_data/python/baclassic/_appsubsystem.py | 77 +- src/assets/ba_data/python/baclassic/_chest.py | 91 ++ .../ba_data/python/baclassic/_clienteffect.py | 77 ++ .../ba_data/python/baclassic/_tournament.py | 108 ++- src/assets/ba_data/python/baenv.py | 2 +- .../ba_data/python/baplus/_appsubsystem.py | 1 + src/assets/ba_data/python/baplus/_cloud.py | 23 +- .../python/bascenev1lib/activity/coopscore.py | 118 ++- .../python/bascenev1lib/actor/image.py | 12 +- src/assets/ba_data/python/bauiv1/__init__.py | 6 + .../python/bauiv1lib/account/viewer.py | 13 +- src/assets/ba_data/python/bauiv1lib/chest.py | 894 ++++++++++++++++-- .../ba_data/python/bauiv1lib/connectivity.py | 19 +- .../ba_data/python/bauiv1lib/coop/browser.py | 11 +- .../python/bauiv1lib/coop/tournamentbutton.py | 426 +++++---- .../python/bauiv1lib/gather/privatetab.py | 6 +- .../python/bauiv1lib/gather/publictab.py | 71 +- .../ba_data/python/bauiv1lib/gettokens.py | 29 +- src/assets/ba_data/python/bauiv1lib/inbox.py | 485 ++++++---- .../python/bauiv1lib/league/rankwindow.py | 21 +- .../ba_data/python/bauiv1lib/mainmenu.py | 6 +- .../ba_data/python/bauiv1lib/partyqueue.py | 4 + src/assets/ba_data/python/bauiv1lib/play.py | 76 +- .../ba_data/python/bauiv1lib/playoptions.py | 6 +- .../python/bauiv1lib/profile/upgrade.py | 5 +- .../ba_data/python/bauiv1lib/purchase.py | 8 +- .../python/bauiv1lib/resourcetypeinfo.py | 15 +- .../python/bauiv1lib/settings/allsettings.py | 211 ++--- .../python/bauiv1lib/settings/audio.py | 34 +- .../ba_data/python/bauiv1lib/store/browser.py | 9 +- .../python/bauiv1lib/tournamententry.py | 142 +-- .../base/app_adapter/app_adapter_apple.cc | 4 +- .../base/app_adapter/app_adapter_sdl.cc | 10 +- src/ballistica/base/assets/asset.cc | 10 +- src/ballistica/base/assets/assets.cc | 36 +- src/ballistica/base/assets/assets.h | 4 +- src/ballistica/base/assets/sound_asset.cc | 2 +- src/ballistica/base/audio/audio.cc | 2 +- src/ballistica/base/audio/audio_server.cc | 53 +- src/ballistica/base/audio/audio_source.cc | 4 +- src/ballistica/base/base.cc | 45 +- src/ballistica/base/base.h | 11 +- .../base/dynamics/bg/bg_dynamics_server.cc | 2 +- .../base/graphics/gl/renderer_gl.cc | 12 +- src/ballistica/base/graphics/graphics.cc | 20 +- .../base/graphics/graphics_server.cc | 4 +- .../base/graphics/renderer/renderer.cc | 4 +- .../base/graphics/support/camera.cc | 7 +- .../base/graphics/support/screen_messages.cc | 23 +- .../base/input/device/joystick_input.cc | 8 +- .../base/input/device/touch_input.cc | 4 +- src/ballistica/base/input/input.cc | 28 +- .../base/input/support/remote_app_server.cc | 6 +- src/ballistica/base/logic/logic.cc | 8 +- .../python/methods/python_methods_base_1.cc | 4 +- .../python/methods/python_methods_base_2.cc | 7 +- src/ballistica/base/support/classic_soft.h | 6 + src/ballistica/base/support/context.h | 56 +- src/ballistica/base/ui/dev_console.cc | 2 +- src/ballistica/base/ui/ui.cc | 4 +- src/ballistica/classic/classic.cc | 7 + src/ballistica/classic/classic.h | 3 + .../classic/python/classic_python.cc | 55 ++ .../classic/python/classic_python.h | 19 + .../python/methods/python_methods_classic.cc | 54 +- .../classic/support/classic_app_mode.cc | 99 +- .../classic/support/classic_app_mode.h | 34 +- src/ballistica/classic/support/stress_test.cc | 2 +- src/ballistica/core/core.cc | 14 +- src/ballistica/core/core.h | 6 +- src/ballistica/core/platform/core_platform.cc | 15 +- src/ballistica/core/platform/core_platform.h | 10 +- src/ballistica/core/support/core_config.h | 4 +- .../scene_v1/connection/connection.cc | 14 +- .../connection/connection_to_client.cc | 15 +- .../scene_v1/connection/connection_to_host.cc | 2 +- src/ballistica/scene_v1/dynamics/dynamics.cc | 2 +- src/ballistica/scene_v1/node/globals_node.cc | 4 +- src/ballistica/scene_v1/node/globals_node.h | 2 +- .../scene_v1/node/session_globals_node.cc | 4 +- .../scene_v1/node/session_globals_node.h | 2 +- src/ballistica/scene_v1/node/sound_node.cc | 2 +- src/ballistica/scene_v1/node/spaz_node.cc | 2 +- src/ballistica/scene_v1/node/terrain_node.cc | 4 +- src/ballistica/scene_v1/node/text_node.cc | 6 +- .../scene_v1/node/time_display_node.cc | 8 +- .../class/python_class_scene_data_asset.cc | 2 +- .../scene_v1/support/client_session.cc | 2 +- .../scene_v1/support/client_session_net.cc | 6 +- .../scene_v1/support/host_session.cc | 10 +- src/ballistica/scene_v1/support/player.cc | 4 +- src/ballistica/scene_v1/support/scene.cc | 4 +- .../support/scene_v1_input_device_delegate.cc | 2 +- .../scene_v1/support/session_stream.cc | 4 +- src/ballistica/shared/ballistica.cc | 12 +- .../shared/foundation/event_loop.cc | 11 +- .../shared/foundation/fatal_error.cc | 10 +- src/ballistica/shared/foundation/macros.cc | 10 +- src/ballistica/shared/foundation/macros.h | 4 +- src/ballistica/shared/foundation/object.cc | 4 +- src/ballistica/shared/python/python_ref.cc | 22 + src/ballistica/shared/python/python_ref.h | 4 + .../python/class/python_class_ui_sound.cc | 2 +- .../python/methods/python_methods_ui_v1.cc | 192 +++- src/ballistica/ui_v1/ui_v1.cc | 1 - src/ballistica/ui_v1/ui_v1.h | 3 - .../ui_v1/widget/check_box_widget.cc | 6 +- src/ballistica/ui_v1/widget/root_widget.cc | 335 ++++++- src/ballistica/ui_v1/widget/root_widget.h | 49 +- src/ballistica/ui_v1/widget/scroll_widget.cc | 33 +- src/ballistica/ui_v1/widget/scroll_widget.h | 5 + src/ballistica/ui_v1/widget/spinner_widget.cc | 58 ++ src/ballistica/ui_v1/widget/spinner_widget.h | 36 + src/ballistica/ui_v1/widget/text_widget.cc | 6 +- .../baclassicmeta/pyembed/binding_classic.py | 6 + tests/test_efro/test_dataclassio.py | 389 +++++++- tools/bacommon/bs.py | 733 ++++++++++++++ tools/bacommon/cloud.py | 238 ----- tools/efro/dataclassio/_api.py | 33 +- tools/efro/dataclassio/_base.py | 71 +- tools/efro/dataclassio/_inputter.py | 125 ++- tools/efro/dataclassio/_outputter.py | 9 + tools/efro/dataclassio/templatemultitype.py | 63 ++ tools/efro/message/_protocol.py | 9 +- tools/efro/util.py | 15 +- 140 files changed, 4952 insertions(+), 1649 deletions(-) create mode 100644 src/assets/ba_data/python/baclassic/_chest.py create mode 100644 src/assets/ba_data/python/baclassic/_clienteffect.py create mode 100644 src/ballistica/ui_v1/widget/spinner_widget.cc create mode 100644 src/ballistica/ui_v1/widget/spinner_widget.h create mode 100644 tools/bacommon/bs.py create mode 100644 tools/efro/dataclassio/templatemultitype.py diff --git a/.efrocachemap b/.efrocachemap index 716860508..81cfcf3b0 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -53,6 +53,7 @@ "build/assets/ba_data/audio/assassinFall.ogg": "201d192debe8bda9e9dead28e9cc6939", "build/assets/ba_data/audio/assassinHit1.ogg": "caaab755b159e399b121be1aec8f61b9", "build/assets/ba_data/audio/assassinHit2.ogg": "a08ac94f02040af67bc46eff6a691a84", + "build/assets/ba_data/audio/aww.ogg": "80cf7a35a58ec6633d3c5440764de3f5", "build/assets/ba_data/audio/bear1.ogg": "acddcf643e9fbf8d92eacf50992c81d0", "build/assets/ba_data/audio/bear2.ogg": "74f7ce4f64e0fb943ab4ec34fdc83779", "build/assets/ba_data/audio/bear3.ogg": "142d1f3d021c8639fbbc1a2ed0f3dc93", @@ -95,6 +96,7 @@ "build/assets/ba_data/audio/cheer.ogg": "b9f1cce825ca00295b61b088f353715b", "build/assets/ba_data/audio/click01.ogg": "204f93a3eb7d82cf8ca9172ee5f01c11", "build/assets/ba_data/audio/corkPop.ogg": "7b0bdbb0cdaf40ec5adf3311ecfe620a", + "build/assets/ba_data/audio/corkPop2.ogg": "1ad5aff84af244acb2f3eaf95ddb7f6a", "build/assets/ba_data/audio/cowboy1.ogg": "e12671599d7f6fa7e246b3ad4d83ad7d", "build/assets/ba_data/audio/cowboy2.ogg": "0cd227e165a76bf829ac0e3ac129929c", "build/assets/ba_data/audio/cowboy3.ogg": "9ce2c947a9ddcf9c6e793721d5b1eb66", @@ -121,6 +123,7 @@ "build/assets/ba_data/audio/dingSmallHigh.ogg": "73ce9e68ef59847dc7621d38ed019c42", "build/assets/ba_data/audio/dripity.ogg": "db450cee4e5241fbaa80a2874cb3cf7f", "build/assets/ba_data/audio/drumRoll.ogg": "8d8234c10e7b9dee277a4e26aec3c9e1", + "build/assets/ba_data/audio/drumRollShort.ogg": "b4273cdd69c0b09bd1da5146a6b1bc76", "build/assets/ba_data/audio/error.ogg": "a39731636b92282052e15eb5b9413816", "build/assets/ba_data/audio/explosion01.ogg": "51bba8fc738410a61da8d535d8485a20", "build/assets/ba_data/audio/explosion02.ogg": "b4ce0ea7abd1c5b52ad7a868f1029a2d", @@ -147,6 +150,7 @@ "build/assets/ba_data/audio/frostyHit02.ogg": "c608e2ce52c872d701110367fa447656", "build/assets/ba_data/audio/frostyHit03.ogg": "20860a3e0acd4104d2b62ac4a624749f", "build/assets/ba_data/audio/fuse01.ogg": "f24744d8b4590b5898015dd98b2b9374", + "build/assets/ba_data/audio/gasp.ogg": "91b267abf7015cf36d263e961f83856b", "build/assets/ba_data/audio/gladiator1.ogg": "528596df10aec80417a4df83325fc09d", "build/assets/ba_data/audio/gladiator2.ogg": "a0f42843c7a73523e90ed32d766959f1", "build/assets/ba_data/audio/gladiator3.ogg": "b3fa702291a4da6f439e72481177a5c2", @@ -218,6 +222,7 @@ "build/assets/ba_data/audio/menuMusic.ogg": "b25ee0041baf71b08c7650ae9f4daab0", "build/assets/ba_data/audio/metalHit.ogg": "ced0188b46245cd60b248fc7bc13b706", "build/assets/ba_data/audio/metalSkid.ogg": "a069f5022be74229c008a19b3e00e64c", + "build/assets/ba_data/audio/nice.ogg": "fb362c97a244ed3a2c2f98c38691fb00", "build/assets/ba_data/audio/ninjaAttack1.ogg": "82c98bc81f3e3e224ddd3151be918dd6", "build/assets/ba_data/audio/ninjaAttack2.ogg": "1f0952bb9df1877fbb9e05b8649b63da", "build/assets/ba_data/audio/ninjaAttack3.ogg": "e5edefe727e4fe2bcb16ab90edc36c39", @@ -287,6 +292,7 @@ "build/assets/ba_data/audio/raceBeep1.ogg": "2d271c487cefbd67f15a21a0904b3e1f", "build/assets/ba_data/audio/raceBeep2.ogg": "47cf9d039e19a3446444bfeb82b394b6", "build/assets/ba_data/audio/refWhistle.ogg": "8ebbde5488b834e73f1d8ee4dad8b1f3", + "build/assets/ba_data/audio/revUp.ogg": "19862620186556029821e41b403f3a11", "build/assets/ba_data/audio/robot1.ogg": "feed97d9abb23097d659b10c6276bab8", "build/assets/ba_data/audio/robot2.ogg": "d9fc996427452f76b0527d5ca84983af", "build/assets/ba_data/audio/robot3.ogg": "bf766b82d438323f89fe4688dd78f5ef", @@ -395,7 +401,11 @@ "build/assets/ba_data/audio/wizardFall.ogg": "e422bd05ae30a28b02b1c55fb0c1fa00", "build/assets/ba_data/audio/wizardHit1.ogg": "7686120c658c811064efda94ac3e90ca", "build/assets/ba_data/audio/wizardHit2.ogg": "d245e07803be7158da49d4962ccf483a", + "build/assets/ba_data/audio/woo.ogg": "e84ef95cb1d97b65ff529687d6076da9", + "build/assets/ba_data/audio/woo2.ogg": "5dfb2e489f406d11772f96ded5611c4f", + "build/assets/ba_data/audio/woo3.ogg": "637878bb31decb36e6db7a5295eea933", "build/assets/ba_data/audio/woodDebrisFall.ogg": "e163c84d87821e3e19ec8b0bf1fef9a7", + "build/assets/ba_data/audio/wow.ogg": "581d224d771195bde72548509cc1cc32", "build/assets/ba_data/audio/wrestler1.ogg": "7486e02349206e8082b44105d0a1195c", "build/assets/ba_data/audio/wrestler2.ogg": "0f197b1d7e6c2e0cc86e57d1b53581aa", "build/assets/ba_data/audio/wrestler3.ogg": "911ad7c64018e8d5fa5f722a04de8837", @@ -404,6 +414,7 @@ "build/assets/ba_data/audio/wrestlerFall.ogg": "3c6bb84fb09a0829fd60066b1807a16c", "build/assets/ba_data/audio/wrestlerHit1.ogg": "1950d463514448069f0d3c0f00108eaa", "build/assets/ba_data/audio/wrestlerHit2.ogg": "5b549fb2406fd72d1d0947fc8173cc08", + "build/assets/ba_data/audio/yeah.ogg": "2c55f21c39cf5f41a81317dec3f5d7fa", "build/assets/ba_data/audio/zoeAttack01.ogg": "0b0536b8afba7cb773beffeaa2e4bb90", "build/assets/ba_data/audio/zoeAttack02.ogg": "931a5b3d78e2322443fe1e51e6c25b99", "build/assets/ba_data/audio/zoeAttack03.ogg": "e1d1f58f038bedda8c22fc518aa37c7e", @@ -421,21 +432,21 @@ "build/assets/ba_data/audio/zoeOw.ogg": "b2d705c31c9dcc1efdc71394764c3beb", "build/assets/ba_data/audio/zoePickup01.ogg": "e9366dc2d2b8ab8b0c4e2c14c02d0789", "build/assets/ba_data/audio/zoeScream01.ogg": "903e0e45ee9b3373e9d9ce20c814374e", - "build/assets/ba_data/data/langdata.json": "ce2f76ab5f36cbc0212d1b3c424eb954", + "build/assets/ba_data/data/langdata.json": "dbbd8f26d2f85c0b649d461e991b80cb", "build/assets/ba_data/data/languages/arabic.json": "3c22e7b6d7b09a812a2e28b35c9e9241", "build/assets/ba_data/data/languages/belarussian.json": "0b60a9d4496d1213c2d0b647d346ce30", "build/assets/ba_data/data/languages/chinese.json": "fc45d2838b834889c06920ae7c2102fa", "build/assets/ba_data/data/languages/chinesetraditional.json": "904b35b656c53f9830e406565edd5120", - "build/assets/ba_data/data/languages/croatian.json": "1e541070309ff6be95b0c39940aa7e99", + "build/assets/ba_data/data/languages/croatian.json": "e131a87cf5783e0fbb3d211a927efe1a", "build/assets/ba_data/data/languages/czech.json": "d18b7d1c6bf51fc81af4084ef0e69e3e", "build/assets/ba_data/data/languages/danish.json": "8e57db30c5250df2abff14a822f83ea7", - "build/assets/ba_data/data/languages/dutch.json": "f4e1e8e9231cda9d1bcc7e87a7f8821e", - "build/assets/ba_data/data/languages/english.json": "b5917c3b975155e35fedb655dbd7568c", + "build/assets/ba_data/data/languages/dutch.json": "4085dec5af362cf068b494524ced3872", + "build/assets/ba_data/data/languages/english.json": "527d106870b0690cc39a80b88e60ab7a", "build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880", "build/assets/ba_data/data/languages/filipino.json": "3d9269a90a2fee164d0a7513c4f130a3", "build/assets/ba_data/data/languages/french.json": "6d20655730b1017ef187fd828b91d43c", - "build/assets/ba_data/data/languages/german.json": "a150dbb5c0f43984757f7db295d96203", - "build/assets/ba_data/data/languages/gibberish.json": "df76e851aee59657b69e34efd54fee06", + "build/assets/ba_data/data/languages/german.json": "b92ec951b5a0ce4f73677051ca59a06b", + "build/assets/ba_data/data/languages/gibberish.json": "2569fe1b2f686670f825e2faaa8c5dc3", "build/assets/ba_data/data/languages/greek.json": "d28d1092fbb00ed857cbd53124c0dc78", "build/assets/ba_data/data/languages/hindi.json": "567e6976b3c72f891431ad7fcc62ab16", "build/assets/ba_data/data/languages/hungarian.json": "9d88004a98f0fbe2ea72edd5e0b3002e", @@ -451,7 +462,7 @@ "build/assets/ba_data/data/languages/russian.json": "fc64ed6b6356ea11385ee5c20748425a", "build/assets/ba_data/data/languages/serbian.json": "623fa4129a1154c2f32ed7867e56ff6a", "build/assets/ba_data/data/languages/slovak.json": "c11c29708b3742cdc2a92b4fa0d6d29f", - "build/assets/ba_data/data/languages/spanish.json": "499b464318a8c9d1fb271cf480862b57", + "build/assets/ba_data/data/languages/spanish.json": "f8ab976d219e579546bb98b6d7fd12ce", "build/assets/ba_data/data/languages/swedish.json": "3b179e7333183c70adb0811246b09959", "build/assets/ba_data/data/languages/tamil.json": "ead39b864228696a9b0d19344bc4b5ec", "build/assets/ba_data/data/languages/thai.json": "383540a1e9c7c131ac579f51afc87471", @@ -1362,10 +1373,18 @@ "build/assets/ba_data/textures/chestIconMulti.ktx": "c026aa573aab141044b503437ffdaeea", "build/assets/ba_data/textures/chestIconMulti.pvr": "3f41eaa108068751ba40e08964c45ec0", "build/assets/ba_data/textures/chestIconMulti_preview.png": "47b6839bdc19ad9e3d3d6282ce0138dd", + "build/assets/ba_data/textures/chestIconTint.dds": "04c079c1a79d548ca02969e9934d591b", + "build/assets/ba_data/textures/chestIconTint.ktx": "ff39e97211bb71f59091aac2f51ebbed", + "build/assets/ba_data/textures/chestIconTint.pvr": "129b44259f8b62c0e1d4c08612b9c691", + "build/assets/ba_data/textures/chestIconTint_preview.png": "022f3f8bb8bc9c082dbb21958ec8722f", "build/assets/ba_data/textures/chestIcon_preview.png": "d3046ccf2fedefa9ad766054e350a5d6", "build/assets/ba_data/textures/chestOpenIcon.dds": "325e93c7147c3bf098857d0e3eff4a73", "build/assets/ba_data/textures/chestOpenIcon.ktx": "562afa582aa621b946c22460e0caca61", "build/assets/ba_data/textures/chestOpenIcon.pvr": "703c88d07b9ab22cbab647677eb05370", + "build/assets/ba_data/textures/chestOpenIconTint.dds": "c3003ed402d46da9d5c8351f028c5d95", + "build/assets/ba_data/textures/chestOpenIconTint.ktx": "d32fe2152a1bf6feff1f2416627b2023", + "build/assets/ba_data/textures/chestOpenIconTint.pvr": "bd665e8289bfd5c369c473e973dd2d65", + "build/assets/ba_data/textures/chestOpenIconTint_preview.png": "e5a2377c127998faaef83e06d4cc2584", "build/assets/ba_data/textures/chestOpenIcon_preview.png": "40ae165451f052265d07ea62f7d08713", "build/assets/ba_data/textures/circle.dds": "0f4c08ab481dcadce164227a146fdb62", "build/assets/ba_data/textures/circle.ktx": "a6ff1b802324d1874aaa9c24050cfcf3", @@ -2359,6 +2378,10 @@ "build/assets/ba_data/textures/sparks.ktx": "919af9b0b1b2e756c232c34bc15d506a", "build/assets/ba_data/textures/sparks.pvr": "249e98bbadad09b59f0e8bdd46b1c9b1", "build/assets/ba_data/textures/sparks_preview.png": "9b6b74000a0f56899dd09a956a864ec9", + "build/assets/ba_data/textures/spinner.dds": "54b0a3a695689974defcb7d0ddc40101", + "build/assets/ba_data/textures/spinner.ktx": "9e47f8b7dcca8f061bff6a04a30a2833", + "build/assets/ba_data/textures/spinner.pvr": "312c7ffa74c39d262d00971881ecd93b", + "build/assets/ba_data/textures/spinner_preview.png": "680af969ab856865098dfcf4fb8b6845", "build/assets/ba_data/textures/star.dds": "e799668a604f3e680f7676994e894c1d", "build/assets/ba_data/textures/star.ktx": "725da9d09a93c636bd825788f859c62d", "build/assets/ba_data/textures/star.pvr": "bb68b74f455c36dcd7a40f9d38b5c74f", @@ -4103,47 +4126,47 @@ "build/assets/windows/Win32/ucrtbased.dll": "bfd1180c269d3950b76f35a63655e9e1", "build/assets/windows/Win32/vc_redist.x86.exe": "15a5f1f876503885adbdf5b3989b3718", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", - "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "390472d8d44b0a650796bfd6022d0549", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "23f27d6f139c653f23a15a0af7f7e02c", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "097b082f8f5abb18d824b2044da1de78", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "c027478f038af2faedcc4103e75bc39d", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "5c7f18d5ff9eb14421fe3743550ddf57", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "cef0bc0d149a4c433c2e4bee5b47414a", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "25546af5cf64a0e1e1d1f46b38298820", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "7f172d6c531c371837302f370f77db08", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "0d79db47846defaffaa0bf97ec6b23fc", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "45373e10bb60989787e415ae938756fe", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "0cc3834e81761efdc50babed9e5c7151", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "4e28c4abb0e7128d08f2b84a87d8a765", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "96b856d3db5d061527383b42e588a333", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "124afb5eec04365e42b059b0c2ec98e7", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "0271c53794bdb4a762ae7210d9828fb7", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "f8b3744f90503502618130c8d28f1aa4", - "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "33a0ae6f1ea5a0b0c60055ce01478488", - "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "aad882eaf2230b89973e2cf4f13c9759", - "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "33a0ae6f1ea5a0b0c60055ce01478488", - "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "aad882eaf2230b89973e2cf4f13c9759", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "c20929c73caa78445525c5788b6963e0", - "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "0f21a43d99552df99e0d21c646e6e698", - "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "c20929c73caa78445525c5788b6963e0", - "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "0f21a43d99552df99e0d21c646e6e698", - "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "01dab862a43d9e7c4ee4e49212442d42", - "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "ae4e3f563892f6b9311c4b7284f28c11", - "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "01dab862a43d9e7c4ee4e49212442d42", - "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "ae4e3f563892f6b9311c4b7284f28c11", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "2f581e3dead7038e5b94bc096a7b8c80", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "4538ad0d6b4794de96fb78742ebcdc84", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "0d1a2f2066ae412549034e981ae39e2c", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "55180e66455f91d3af8f15c0c81607a4", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "bb2c15187840ef373ae79cdf1623d3b5", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "8ee1bb4440c364f9fa91791276d64b87", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "863f70d88ab9b3c7c6910d94e26b6f35", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "bb928df1aa05c84889b45f2a00544e64", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "2444899f2f36667dc27fcf5537fc76b6", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "efebe5318abe421031108d9908b4cb84", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "9b4928361ff7e3aa85ea6cca5f3903d8", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "9fad073f6fd49702ceb1ee5057c4f9de", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "ac52ed0994f33ea4997da21d0a7f6877", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "43f429f37c145fc2bf80c01dfd095a7e", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "ea87a5838b465c40b60ff911cccbe189", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "3cafa3287637bba5e56b363bb4fc8f9a", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "1fbe4ab96f25d20b421c8206c9230948", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "0a4dbc635f8e9b3f42e89dd2027bd9d3", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "6114856f2ab42ef6b20a1affdf53032d", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "bfa75071d993efc3853fa350c1de2bcb", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "3971d8deb2c33cfd18c4e3b338d48ec3", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "bd0c352337d72f020b431feca7974c3a", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "073b3690cfb03847ac3b3fe9c017e520", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "c7f0d9c4db7a67b4d72f3c406c585897", + "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "1c375e8003442dd3d059bc0baa260e61", + "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "40daac4bbc8990d5140f97e792bc4fb1", + "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "1c375e8003442dd3d059bc0baa260e61", + "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "40daac4bbc8990d5140f97e792bc4fb1", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "19af08729bed7249eaf9acd697966f3f", + "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "97d2486072bf3a83edb0250ea2d3e69e", + "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "19af08729bed7249eaf9acd697966f3f", + "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "97d2486072bf3a83edb0250ea2d3e69e", + "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "51884d81e2d7bdeb6b59a72f0247c8e1", + "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "36bb6f32ab12e2a46b82155a93b2e527", + "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "51884d81e2d7bdeb6b59a72f0247c8e1", + "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "36bb6f32ab12e2a46b82155a93b2e527", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "f629e97c70198ab3688f11ab4a2d1e97", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "a868c0116adf9f48f503b50b50c70cd8", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "04216607232edde485d139e9e96c6612", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "bdc158a2dfd592cd8dcb235a162a82fe", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "4a0340814c6034b39b2c08287935e26a", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "546ccf01f342463d46302b05c0171044", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "67bb2ff991e00433c1abba2044a0d21e", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "8653a52a8fd75c527c976abb9d223e90", "src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c", "src/assets/ba_data/python/babase/_mgen/enums.py": "794d258d59fd17a61752843a9a0551ad", "src/ballistica/base/mgen/pyembed/binding_base.inc": "06042d31df0ff9af96b99477162e2a91", "src/ballistica/base/mgen/pyembed/binding_base_app.inc": "2d228e7c5578261d394f9c407f4becb1", - "src/ballistica/classic/mgen/pyembed/binding_classic.inc": "fc09126750304b2761049aa9d93a951e", + "src/ballistica/classic/mgen/pyembed/binding_classic.inc": "8ab156122cde23d9718923abe1b4ae5b", "src/ballistica/core/mgen/pyembed/binding_core.inc": "217c84a30f866aaca3a4373e82af7db2", "src/ballistica/core/mgen/pyembed/env.inc": "f015d726b44d2922112fc14d9f146d8b", "src/ballistica/core/mgen/python_modules_monolithic.h": "fb967ed1c7db0c77d8deb4f00a7103c5", diff --git a/CHANGELOG.md b/CHANGELOG.md index 705afddae..9ae37e122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.37 (build 22155, api 9, 2024-12-31) +### 1.7.37 (build 22178, api 9, 2025-01-11) - 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. @@ -176,6 +176,21 @@ should use arrow keys for navigation. To update any old UI code, search for and remove any 'claims_tab' arguments to UI calls since that argument no longer exists. +- Added a `get_unknown_type_fallback()` method to `dataclassio.IOMultiType`. + This be defined to allow multi-type data to be loadable even in the presence + of new types it doesn't recognize. +- Added a `lossy` arg to `dataclassio.dataclass_from_dict()` and + `dataclassio.dataclass_from_json()`. Enum value fallbacks and the new + multitype fallbacks are now only applied when `lossy` is True. This also flags + the returned dataclass to prevent it from being serialized back out. Fallbacks + are useful for forward compatibility, but they are also dangerous in that they + can silently modify/destroy data, so this mechanism will hopefully help keep + them used safely. +- Added a spinner widget (creatable via `bauiv1.spinnerwidget()`). This should + help things look more alive than the static 'loading...' text I've been using + in various places. +- Tournament now award chests instead of tickets. +- Tournaments are now free to enter if you are running this build or newer. ### 1.7.36 (build 21944, api 8, 2024-07-26) - Wired up Tokens, BombSquad's new purchasable currency. The first thing these diff --git a/ballisticakit-cmake/CMakeLists.txt b/ballisticakit-cmake/CMakeLists.txt index ef00fe331..67ff5d147 100644 --- a/ballisticakit-cmake/CMakeLists.txt +++ b/ballisticakit-cmake/CMakeLists.txt @@ -772,6 +772,8 @@ set(BALLISTICA_SOURCES ${BA_SRC_ROOT}/ballistica/ui_v1/widget/row_widget.h ${BA_SRC_ROOT}/ballistica/ui_v1/widget/scroll_widget.cc ${BA_SRC_ROOT}/ballistica/ui_v1/widget/scroll_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/spinner_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/spinner_widget.h ${BA_SRC_ROOT}/ballistica/ui_v1/widget/stack_widget.cc ${BA_SRC_ROOT}/ballistica/ui_v1/widget/stack_widget.h ${BA_SRC_ROOT}/ballistica/ui_v1/widget/text_widget.cc diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj index 4041ababf..55276809c 100644 --- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj @@ -764,6 +764,8 @@ + + diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters index 578def222..43c15ad7f 100644 --- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters @@ -1726,6 +1726,12 @@ ballistica\ui_v1\widget + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + ballistica\ui_v1\widget diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj index 978f56bb6..eb86bf1ed 100644 --- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj @@ -759,6 +759,8 @@ + + diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters index 578def222..43c15ad7f 100644 --- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters @@ -1726,6 +1726,12 @@ ballistica\ui_v1\widget + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + ballistica\ui_v1\widget diff --git a/config/requirements.txt b/config/requirements.txt index e4c0d9928..5ea4a2a6e 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -1,5 +1,5 @@ cpplint==2.0.0 -dmgbuild==1.6.2 +dmgbuild==1.6.4 filelock==3.16.1 furo==2024.8.6 mypy==1.14.1 diff --git a/src/assets/.asset_manifest_private.json b/src/assets/.asset_manifest_private.json index c404d695a..0c1da6e12 100644 --- a/src/assets/.asset_manifest_private.json +++ b/src/assets/.asset_manifest_private.json @@ -52,6 +52,7 @@ "ba_data/audio/assassinFall.ogg", "ba_data/audio/assassinHit1.ogg", "ba_data/audio/assassinHit2.ogg", + "ba_data/audio/aww.ogg", "ba_data/audio/bear1.ogg", "ba_data/audio/bear2.ogg", "ba_data/audio/bear3.ogg", @@ -94,6 +95,7 @@ "ba_data/audio/cheer.ogg", "ba_data/audio/click01.ogg", "ba_data/audio/corkPop.ogg", + "ba_data/audio/corkPop2.ogg", "ba_data/audio/cowboy1.ogg", "ba_data/audio/cowboy2.ogg", "ba_data/audio/cowboy3.ogg", @@ -120,6 +122,7 @@ "ba_data/audio/dingSmallHigh.ogg", "ba_data/audio/dripity.ogg", "ba_data/audio/drumRoll.ogg", + "ba_data/audio/drumRollShort.ogg", "ba_data/audio/error.ogg", "ba_data/audio/explosion01.ogg", "ba_data/audio/explosion02.ogg", @@ -146,6 +149,7 @@ "ba_data/audio/frostyHit02.ogg", "ba_data/audio/frostyHit03.ogg", "ba_data/audio/fuse01.ogg", + "ba_data/audio/gasp.ogg", "ba_data/audio/gladiator1.ogg", "ba_data/audio/gladiator2.ogg", "ba_data/audio/gladiator3.ogg", @@ -217,6 +221,7 @@ "ba_data/audio/menuMusic.ogg", "ba_data/audio/metalHit.ogg", "ba_data/audio/metalSkid.ogg", + "ba_data/audio/nice.ogg", "ba_data/audio/ninjaAttack1.ogg", "ba_data/audio/ninjaAttack2.ogg", "ba_data/audio/ninjaAttack3.ogg", @@ -286,6 +291,7 @@ "ba_data/audio/raceBeep1.ogg", "ba_data/audio/raceBeep2.ogg", "ba_data/audio/refWhistle.ogg", + "ba_data/audio/revUp.ogg", "ba_data/audio/robot1.ogg", "ba_data/audio/robot2.ogg", "ba_data/audio/robot3.ogg", @@ -394,7 +400,11 @@ "ba_data/audio/wizardFall.ogg", "ba_data/audio/wizardHit1.ogg", "ba_data/audio/wizardHit2.ogg", + "ba_data/audio/woo.ogg", + "ba_data/audio/woo2.ogg", + "ba_data/audio/woo3.ogg", "ba_data/audio/woodDebrisFall.ogg", + "ba_data/audio/wow.ogg", "ba_data/audio/wrestler1.ogg", "ba_data/audio/wrestler2.ogg", "ba_data/audio/wrestler3.ogg", @@ -403,6 +413,7 @@ "ba_data/audio/wrestlerFall.ogg", "ba_data/audio/wrestlerHit1.ogg", "ba_data/audio/wrestlerHit2.ogg", + "ba_data/audio/yeah.ogg", "ba_data/audio/zoeAttack01.ogg", "ba_data/audio/zoeAttack02.ogg", "ba_data/audio/zoeAttack03.ogg", @@ -1401,10 +1412,18 @@ "ba_data/textures/chestIconMulti.ktx", "ba_data/textures/chestIconMulti.pvr", "ba_data/textures/chestIconMulti_preview.png", + "ba_data/textures/chestIconTint.dds", + "ba_data/textures/chestIconTint.ktx", + "ba_data/textures/chestIconTint.pvr", + "ba_data/textures/chestIconTint_preview.png", "ba_data/textures/chestIcon_preview.png", "ba_data/textures/chestOpenIcon.dds", "ba_data/textures/chestOpenIcon.ktx", "ba_data/textures/chestOpenIcon.pvr", + "ba_data/textures/chestOpenIconTint.dds", + "ba_data/textures/chestOpenIconTint.ktx", + "ba_data/textures/chestOpenIconTint.pvr", + "ba_data/textures/chestOpenIconTint_preview.png", "ba_data/textures/chestOpenIcon_preview.png", "ba_data/textures/circle.dds", "ba_data/textures/circle.ktx", @@ -2398,6 +2417,10 @@ "ba_data/textures/sparks.ktx", "ba_data/textures/sparks.pvr", "ba_data/textures/sparks_preview.png", + "ba_data/textures/spinner.dds", + "ba_data/textures/spinner.ktx", + "ba_data/textures/spinner.pvr", + "ba_data/textures/spinner_preview.png", "ba_data/textures/star.dds", "ba_data/textures/star.ktx", "ba_data/textures/star.pvr", diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json index 84ab582ba..bd9cbca35 100644 --- a/src/assets/.asset_manifest_public.json +++ b/src/assets/.asset_manifest_public.json @@ -77,6 +77,8 @@ "ba_data/python/baclassic/__pycache__/_appmode.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/_appsubsystem.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/_benchmark.cpython-312.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_chest.cpython-312.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_clienteffect.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/_input.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/_music.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/_net.cpython-312.opt-1.pyc", @@ -93,6 +95,8 @@ "ba_data/python/baclassic/_appmode.py", "ba_data/python/baclassic/_appsubsystem.py", "ba_data/python/baclassic/_benchmark.py", + "ba_data/python/baclassic/_chest.py", + "ba_data/python/baclassic/_clienteffect.py", "ba_data/python/baclassic/_input.py", "ba_data/python/baclassic/_music.py", "ba_data/python/baclassic/_net.py", @@ -107,6 +111,7 @@ "ba_data/python/bacommon/__pycache__/app.cpython-312.opt-1.pyc", "ba_data/python/bacommon/__pycache__/assets.cpython-312.opt-1.pyc", "ba_data/python/bacommon/__pycache__/bacloud.cpython-312.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/bs.cpython-312.opt-1.pyc", "ba_data/python/bacommon/__pycache__/build.cpython-312.opt-1.pyc", "ba_data/python/bacommon/__pycache__/cloud.cpython-312.opt-1.pyc", "ba_data/python/bacommon/__pycache__/loggercontrol.cpython-312.opt-1.pyc", @@ -118,6 +123,7 @@ "ba_data/python/bacommon/app.py", "ba_data/python/bacommon/assets.py", "ba_data/python/bacommon/bacloud.py", + "ba_data/python/bacommon/bs.py", "ba_data/python/bacommon/build.py", "ba_data/python/bacommon/cloud.py", "ba_data/python/bacommon/loggercontrol.py", @@ -603,6 +609,7 @@ "ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-312.opt-1.pyc", "ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-312.opt-1.pyc", "ba_data/python/efro/dataclassio/__pycache__/extras.cpython-312.opt-1.pyc", + "ba_data/python/efro/dataclassio/__pycache__/templatemultitype.cpython-312.opt-1.pyc", "ba_data/python/efro/dataclassio/_api.py", "ba_data/python/efro/dataclassio/_base.py", "ba_data/python/efro/dataclassio/_inputter.py", @@ -610,6 +617,7 @@ "ba_data/python/efro/dataclassio/_pathcapture.py", "ba_data/python/efro/dataclassio/_prep.py", "ba_data/python/efro/dataclassio/extras.py", + "ba_data/python/efro/dataclassio/templatemultitype.py", "ba_data/python/efro/debug.py", "ba_data/python/efro/error.py", "ba_data/python/efro/logging.py", diff --git a/src/assets/Makefile b/src/assets/Makefile index 414ce46a4..80815e965 100644 --- a/src/assets/Makefile +++ b/src/assets/Makefile @@ -206,6 +206,8 @@ SCRIPT_TARGETS_PY_PUBLIC = \ $(BUILD_DIR)/ba_data/python/baclassic/_appmode.py \ $(BUILD_DIR)/ba_data/python/baclassic/_appsubsystem.py \ $(BUILD_DIR)/ba_data/python/baclassic/_benchmark.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_chest.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_clienteffect.py \ $(BUILD_DIR)/ba_data/python/baclassic/_input.py \ $(BUILD_DIR)/ba_data/python/baclassic/_music.py \ $(BUILD_DIR)/ba_data/python/baclassic/_net.py \ @@ -486,6 +488,8 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_appmode.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_appsubsystem.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_benchmark.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_chest.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_clienteffect.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_input.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_music.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_net.cpython-312.opt-1.pyc \ @@ -738,6 +742,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \ $(BUILD_DIR)/ba_data/python/bacommon/app.py \ $(BUILD_DIR)/ba_data/python/bacommon/assets.py \ $(BUILD_DIR)/ba_data/python/bacommon/bacloud.py \ + $(BUILD_DIR)/ba_data/python/bacommon/bs.py \ $(BUILD_DIR)/ba_data/python/bacommon/build.py \ $(BUILD_DIR)/ba_data/python/bacommon/cloud.py \ $(BUILD_DIR)/ba_data/python/bacommon/loggercontrol.py \ @@ -759,6 +764,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \ $(BUILD_DIR)/ba_data/python/efro/dataclassio/_pathcapture.py \ $(BUILD_DIR)/ba_data/python/efro/dataclassio/_prep.py \ $(BUILD_DIR)/ba_data/python/efro/dataclassio/extras.py \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/templatemultitype.py \ $(BUILD_DIR)/ba_data/python/efro/debug.py \ $(BUILD_DIR)/ba_data/python/efro/error.py \ $(BUILD_DIR)/ba_data/python/efro/logging.py \ @@ -778,6 +784,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/app.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/assets.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/bacloud.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/bs.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/build.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/cloud.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/loggercontrol.cpython-312.opt-1.pyc \ @@ -799,6 +806,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \ $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/extras.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/templatemultitype.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/efro/__pycache__/debug.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/efro/__pycache__/error.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/efro/__pycache__/logging.cpython-312.opt-1.pyc \ @@ -5259,6 +5267,7 @@ AUDIO_TARGETS = \ $(BUILD_DIR)/ba_data/audio/assassinFall.ogg \ $(BUILD_DIR)/ba_data/audio/assassinHit1.ogg \ $(BUILD_DIR)/ba_data/audio/assassinHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/aww.ogg \ $(BUILD_DIR)/ba_data/audio/bear1.ogg \ $(BUILD_DIR)/ba_data/audio/bear2.ogg \ $(BUILD_DIR)/ba_data/audio/bear3.ogg \ @@ -5301,6 +5310,7 @@ AUDIO_TARGETS = \ $(BUILD_DIR)/ba_data/audio/cheer.ogg \ $(BUILD_DIR)/ba_data/audio/click01.ogg \ $(BUILD_DIR)/ba_data/audio/corkPop.ogg \ + $(BUILD_DIR)/ba_data/audio/corkPop2.ogg \ $(BUILD_DIR)/ba_data/audio/cowboy1.ogg \ $(BUILD_DIR)/ba_data/audio/cowboy2.ogg \ $(BUILD_DIR)/ba_data/audio/cowboy3.ogg \ @@ -5327,6 +5337,7 @@ AUDIO_TARGETS = \ $(BUILD_DIR)/ba_data/audio/dingSmallHigh.ogg \ $(BUILD_DIR)/ba_data/audio/dripity.ogg \ $(BUILD_DIR)/ba_data/audio/drumRoll.ogg \ + $(BUILD_DIR)/ba_data/audio/drumRollShort.ogg \ $(BUILD_DIR)/ba_data/audio/error.ogg \ $(BUILD_DIR)/ba_data/audio/explosion01.ogg \ $(BUILD_DIR)/ba_data/audio/explosion02.ogg \ @@ -5353,6 +5364,7 @@ AUDIO_TARGETS = \ $(BUILD_DIR)/ba_data/audio/frostyHit02.ogg \ $(BUILD_DIR)/ba_data/audio/frostyHit03.ogg \ $(BUILD_DIR)/ba_data/audio/fuse01.ogg \ + $(BUILD_DIR)/ba_data/audio/gasp.ogg \ $(BUILD_DIR)/ba_data/audio/gladiator1.ogg \ $(BUILD_DIR)/ba_data/audio/gladiator2.ogg \ $(BUILD_DIR)/ba_data/audio/gladiator3.ogg \ @@ -5424,6 +5436,7 @@ AUDIO_TARGETS = \ $(BUILD_DIR)/ba_data/audio/menuMusic.ogg \ $(BUILD_DIR)/ba_data/audio/metalHit.ogg \ $(BUILD_DIR)/ba_data/audio/metalSkid.ogg \ + $(BUILD_DIR)/ba_data/audio/nice.ogg \ $(BUILD_DIR)/ba_data/audio/ninjaAttack1.ogg \ $(BUILD_DIR)/ba_data/audio/ninjaAttack2.ogg \ $(BUILD_DIR)/ba_data/audio/ninjaAttack3.ogg \ @@ -5493,6 +5506,7 @@ AUDIO_TARGETS = \ $(BUILD_DIR)/ba_data/audio/raceBeep1.ogg \ $(BUILD_DIR)/ba_data/audio/raceBeep2.ogg \ $(BUILD_DIR)/ba_data/audio/refWhistle.ogg \ + $(BUILD_DIR)/ba_data/audio/revUp.ogg \ $(BUILD_DIR)/ba_data/audio/robot1.ogg \ $(BUILD_DIR)/ba_data/audio/robot2.ogg \ $(BUILD_DIR)/ba_data/audio/robot3.ogg \ @@ -5601,7 +5615,11 @@ AUDIO_TARGETS = \ $(BUILD_DIR)/ba_data/audio/wizardFall.ogg \ $(BUILD_DIR)/ba_data/audio/wizardHit1.ogg \ $(BUILD_DIR)/ba_data/audio/wizardHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/woo.ogg \ + $(BUILD_DIR)/ba_data/audio/woo2.ogg \ + $(BUILD_DIR)/ba_data/audio/woo3.ogg \ $(BUILD_DIR)/ba_data/audio/woodDebrisFall.ogg \ + $(BUILD_DIR)/ba_data/audio/wow.ogg \ $(BUILD_DIR)/ba_data/audio/wrestler1.ogg \ $(BUILD_DIR)/ba_data/audio/wrestler2.ogg \ $(BUILD_DIR)/ba_data/audio/wrestler3.ogg \ @@ -5610,6 +5628,7 @@ AUDIO_TARGETS = \ $(BUILD_DIR)/ba_data/audio/wrestlerFall.ogg \ $(BUILD_DIR)/ba_data/audio/wrestlerHit1.ogg \ $(BUILD_DIR)/ba_data/audio/wrestlerHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/yeah.ogg \ $(BUILD_DIR)/ba_data/audio/zoeAttack01.ogg \ $(BUILD_DIR)/ba_data/audio/zoeAttack02.ogg \ $(BUILD_DIR)/ba_data/audio/zoeAttack03.ogg \ @@ -5723,7 +5742,9 @@ TEX2D_DDS_TARGETS = \ $(BUILD_DIR)/ba_data/textures/chestIcon.dds \ $(BUILD_DIR)/ba_data/textures/chestIconEmpty.dds \ $(BUILD_DIR)/ba_data/textures/chestIconMulti.dds \ + $(BUILD_DIR)/ba_data/textures/chestIconTint.dds \ $(BUILD_DIR)/ba_data/textures/chestOpenIcon.dds \ + $(BUILD_DIR)/ba_data/textures/chestOpenIconTint.dds \ $(BUILD_DIR)/ba_data/textures/circle.dds \ $(BUILD_DIR)/ba_data/textures/circleNoAlpha.dds \ $(BUILD_DIR)/ba_data/textures/circleOutline.dds \ @@ -5972,6 +5993,7 @@ TEX2D_DDS_TARGETS = \ $(BUILD_DIR)/ba_data/textures/softRect2.dds \ $(BUILD_DIR)/ba_data/textures/softRectVertical.dds \ $(BUILD_DIR)/ba_data/textures/sparks.dds \ + $(BUILD_DIR)/ba_data/textures/spinner.dds \ $(BUILD_DIR)/ba_data/textures/star.dds \ $(BUILD_DIR)/ba_data/textures/startButton.dds \ $(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor.dds \ @@ -6135,7 +6157,9 @@ TEX2D_PVR_TARGETS = \ $(BUILD_DIR)/ba_data/textures/chestIcon.pvr \ $(BUILD_DIR)/ba_data/textures/chestIconEmpty.pvr \ $(BUILD_DIR)/ba_data/textures/chestIconMulti.pvr \ + $(BUILD_DIR)/ba_data/textures/chestIconTint.pvr \ $(BUILD_DIR)/ba_data/textures/chestOpenIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/chestOpenIconTint.pvr \ $(BUILD_DIR)/ba_data/textures/circle.pvr \ $(BUILD_DIR)/ba_data/textures/circleNoAlpha.pvr \ $(BUILD_DIR)/ba_data/textures/circleOutline.pvr \ @@ -6384,6 +6408,7 @@ TEX2D_PVR_TARGETS = \ $(BUILD_DIR)/ba_data/textures/softRect2.pvr \ $(BUILD_DIR)/ba_data/textures/softRectVertical.pvr \ $(BUILD_DIR)/ba_data/textures/sparks.pvr \ + $(BUILD_DIR)/ba_data/textures/spinner.pvr \ $(BUILD_DIR)/ba_data/textures/star.pvr \ $(BUILD_DIR)/ba_data/textures/startButton.pvr \ $(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor.pvr \ @@ -6547,7 +6572,9 @@ TEX2D_KTX_TARGETS = \ $(BUILD_DIR)/ba_data/textures/chestIcon.ktx \ $(BUILD_DIR)/ba_data/textures/chestIconEmpty.ktx \ $(BUILD_DIR)/ba_data/textures/chestIconMulti.ktx \ + $(BUILD_DIR)/ba_data/textures/chestIconTint.ktx \ $(BUILD_DIR)/ba_data/textures/chestOpenIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/chestOpenIconTint.ktx \ $(BUILD_DIR)/ba_data/textures/circle.ktx \ $(BUILD_DIR)/ba_data/textures/circleNoAlpha.ktx \ $(BUILD_DIR)/ba_data/textures/circleOutline.ktx \ @@ -6796,6 +6823,7 @@ TEX2D_KTX_TARGETS = \ $(BUILD_DIR)/ba_data/textures/softRect2.ktx \ $(BUILD_DIR)/ba_data/textures/softRectVertical.ktx \ $(BUILD_DIR)/ba_data/textures/sparks.ktx \ + $(BUILD_DIR)/ba_data/textures/spinner.ktx \ $(BUILD_DIR)/ba_data/textures/star.ktx \ $(BUILD_DIR)/ba_data/textures/startButton.ktx \ $(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor.ktx \ @@ -6958,7 +6986,9 @@ TEX2D_PREVIEW_PNG_TARGETS = \ $(BUILD_DIR)/ba_data/textures/characterIconMask_preview.png \ $(BUILD_DIR)/ba_data/textures/chestIconEmpty_preview.png \ $(BUILD_DIR)/ba_data/textures/chestIconMulti_preview.png \ + $(BUILD_DIR)/ba_data/textures/chestIconTint_preview.png \ $(BUILD_DIR)/ba_data/textures/chestIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/chestOpenIconTint_preview.png \ $(BUILD_DIR)/ba_data/textures/chestOpenIcon_preview.png \ $(BUILD_DIR)/ba_data/textures/circleNoAlpha_preview.png \ $(BUILD_DIR)/ba_data/textures/circleOutlineNoAlpha_preview.png \ @@ -7208,6 +7238,7 @@ TEX2D_PREVIEW_PNG_TARGETS = \ $(BUILD_DIR)/ba_data/textures/softRectVertical_preview.png \ $(BUILD_DIR)/ba_data/textures/softRect_preview.png \ $(BUILD_DIR)/ba_data/textures/sparks_preview.png \ + $(BUILD_DIR)/ba_data/textures/spinner_preview.png \ $(BUILD_DIR)/ba_data/textures/star_preview.png \ $(BUILD_DIR)/ba_data/textures/startButton_preview.png \ $(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor_preview.png \ diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py index cd5d57441..b2d08ac46 100644 --- a/src/assets/ba_data/python/babase/_app.py +++ b/src/assets/ba_data/python/babase/_app.py @@ -68,9 +68,10 @@ class App: health_monitor: AppHealthMonitor # How long we allow shutdown tasks to run before killing them. - # Currently the entire app hard-exits if shutdown takes 10 seconds, - # so we need to keep it under that. - SHUTDOWN_TASK_TIMEOUT_SECONDS = 5 + # Currently the entire app hard-exits if shutdown takes 15 seconds, + # so we need to keep it under that. Staying above 10 should allow + # 10 second network timeouts to happen though. + SHUTDOWN_TASK_TIMEOUT_SECONDS = 12 class State(Enum): """High level state the app can be in.""" diff --git a/src/assets/ba_data/python/babase/_apputils.py b/src/assets/ba_data/python/babase/_apputils.py index 29545f406..9d58da480 100644 --- a/src/assets/ba_data/python/babase/_apputils.py +++ b/src/assets/ba_data/python/babase/_apputils.py @@ -30,7 +30,9 @@ def utc_now_cloud() -> datetime.datetime: Applies offsets pulled from server communication/etc. """ - # FIXME - do something smart here. + # TODO: wire this up. Just using local time for now. Make sure that + # BaseFeatureSet::TimeSinceEpochCloudSeconds() and this are synced + # up. return utc_now() diff --git a/src/assets/ba_data/python/baclassic/__init__.py b/src/assets/ba_data/python/baclassic/__init__.py index 3367c4b79..1c428fb2d 100644 --- a/src/assets/ba_data/python/baclassic/__init__.py +++ b/src/assets/ba_data/python/baclassic/__init__.py @@ -2,18 +2,11 @@ # """Components for the classic BombSquad experience. -This package is used as a dumping ground for functionality that is -necessary to keep classic BombSquad working, but which may no longer be -the best way to do things going forward. - -New code should try to avoid using code from here when possible. - -Functionality in this package should be exposed through the -ClassicAppSubsystem. This allows type-checked code to go through the -babase.app.classic singleton which forces it to explicitly handle the -possibility of babase.app.classic being None. When code instead imports -classic submodules directly, it is much harder to make it cleanly handle -classic not being present. +This package/feature-set contains functionality related to the classic +BombSquad experience. Note that much legacy BombSquad code is still a +bit tangled and thus this feature-set is largely inseperable from +scenev1 and uiv1. Future feature-sets will be designed in a more modular +way. """ # ba_meta require api 9 @@ -29,8 +22,16 @@ from baclassic._appmode import ClassicAppMode from baclassic._appsubsystem import ClassicAppSubsystem from baclassic._achievement import Achievement, AchievementSubsystem +from baclassic._chest import ( + ChestAppearanceDisplayInfo, + CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, + CHEST_APPEARANCE_DISPLAY_INFOS, +) __all__ = [ + 'ChestAppearanceDisplayInfo', + 'CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT', + 'CHEST_APPEARANCE_DISPLAY_INFOS', 'ClassicAppMode', 'ClassicAppSubsystem', 'Achievement', diff --git a/src/assets/ba_data/python/baclassic/_appmode.py b/src/assets/ba_data/python/baclassic/_appmode.py index e6cb68a17..c41185720 100644 --- a/src/assets/ba_data/python/baclassic/_appmode.py +++ b/src/assets/ba_data/python/baclassic/_appmode.py @@ -197,9 +197,9 @@ def _update_for_primary_account( if account is None: self._account_data_sub = None _baclassic.set_root_ui_account_values( - tickets_text='', - tokens_text='', - league_rank_text='', + tickets=-1, + tokens=-1, + league_rank=-1, league_type='', achievements_percent_text='', level_text='', @@ -250,7 +250,7 @@ def _on_sub_test_update(self, val: int | None) -> None: print(f'GOT SUB TEST UPDATE: {val}') def _on_classic_account_data_change( - self, val: bacommon.cloud.BSClassicAccountLiveData + self, val: bacommon.bs.ClassicAccountLiveData ) -> None: # print('ACCOUNT CHANGED:', val) achp = round(val.achievements / max(val.achievements_total, 1) * 100.0) @@ -264,11 +264,9 @@ def _on_classic_account_data_change( chest3 = val.chests.get('3') _baclassic.set_root_ui_account_values( - tickets_text=str(val.tickets), - tokens_text=str(val.tokens), - league_rank_text=( - '-' if val.league_rank is None else f'#{val.league_rank}' - ), + tickets=val.tickets, + tokens=val.tokens, + league_rank=(-1 if val.league_rank is None else val.league_rank), league_type=( '' if val.league_type is None else val.league_type.value ), @@ -292,17 +290,35 @@ def _on_classic_account_data_change( chest_0_unlock_time=( -1.0 if chest0 is None else chest0.unlock_time.timestamp() ), - chest_1_unlock_time=-1.0, - chest_2_unlock_time=-1.0, - chest_3_unlock_time=-1.0, + chest_1_unlock_time=( + -1.0 if chest1 is None else chest1.unlock_time.timestamp() + ), + chest_2_unlock_time=( + -1.0 if chest2 is None else chest2.unlock_time.timestamp() + ), + chest_3_unlock_time=( + -1.0 if chest3 is None else chest3.unlock_time.timestamp() + ), chest_0_ad_allow_time=( -1.0 if chest0 is None or chest0.ad_allow_time is None else chest0.ad_allow_time.timestamp() ), - chest_1_ad_allow_time=-1.0, - chest_2_ad_allow_time=-1.0, - chest_3_ad_allow_time=-1.0, + chest_1_ad_allow_time=( + -1.0 + if chest1 is None or chest1.ad_allow_time is None + else chest1.ad_allow_time.timestamp() + ), + chest_2_ad_allow_time=( + -1.0 + if chest2 is None or chest2.ad_allow_time is None + else chest2.ad_allow_time.timestamp() + ), + chest_3_ad_allow_time=( + -1.0 + if chest3 is None or chest3.ad_allow_time is None + else chest3.ad_allow_time.timestamp() + ), ) # Note that we have values and updated faded state accordingly. diff --git a/src/assets/ba_data/python/baclassic/_appsubsystem.py b/src/assets/ba_data/python/baclassic/_appsubsystem.py index 99702dc25..f2d0527fa 100644 --- a/src/assets/ba_data/python/baclassic/_appsubsystem.py +++ b/src/assets/ba_data/python/baclassic/_appsubsystem.py @@ -3,10 +3,10 @@ """Provides classic app subsystem.""" from __future__ import annotations -from typing import TYPE_CHECKING, override import random import logging import weakref +from typing import TYPE_CHECKING, override, assert_never from efro.dataclassio import dataclass_from_dict import babase @@ -26,6 +26,7 @@ if TYPE_CHECKING: from typing import Callable, Any, Sequence + import bacommon.bs from bascenev1lib.actor import spazappearance from bauiv1lib.party import PartyWindow @@ -509,11 +510,36 @@ def master_server_v1_post( request, 'post', data, callback, response_type ).start() - def get_tournament_prize_strings(self, entry: dict[str, Any]) -> list[str]: + def set_tournament_prize_image( + self, entry: dict[str, Any], index: int, image: bauiv1.Widget + ) -> None: """Given a tournament entry, return strings for its prize levels.""" from baclassic import _tournament - return _tournament.get_tournament_prize_strings(entry) + return _tournament.set_tournament_prize_chest_image(entry, index, image) + + def create_in_game_tournament_prize_image( + self, + entry: dict[str, Any], + index: int, + position: tuple[float, float], + ) -> None: + """Given a tournament entry, return strings for its prize levels.""" + from baclassic import _tournament + + _tournament.create_in_game_tournament_prize_image( + entry, index, position + ) + + def get_tournament_prize_strings( + self, entry: dict[str, Any], include_tickets: bool + ) -> list[str]: + """Given a tournament entry, return strings for its prize levels.""" + from baclassic import _tournament + + return _tournament.get_tournament_prize_strings( + entry, include_tickets=include_tickets + ) def getcampaign(self, name: str) -> bascenev1.Campaign: """Return a campaign by name.""" @@ -852,3 +878,48 @@ def invoke_main_menu_ui(self) -> None: is_top_level=True, suppress_warning=True, ) + + @staticmethod + def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None: + """Run client effects sent from the master server.""" + from baclassic._clienteffect import run_bs_client_effects + + run_bs_client_effects(effects) + + @staticmethod + def basic_client_ui_button_label_str( + label: bacommon.bs.BasicClientUI.ButtonLabel, + ) -> babase.Lstr: + """Given a client-ui label, return an Lstr.""" + import bacommon.bs + + cls = bacommon.bs.BasicClientUI.ButtonLabel + if label is cls.UNKNOWN: + # Server should not be sending us unknown stuff; make noise + # if they do. + logging.error( + 'Got BasicClientUI.ButtonLabel.UNKNOWN; should not happen.' + ) + return babase.Lstr(value='') + + rsrc: str | None = None + if label is cls.OK: + rsrc = 'okText' + elif label is cls.APPLY: + rsrc = 'applyText' + elif label is cls.CANCEL: + rsrc = 'cancelText' + elif label is cls.ACCEPT: + rsrc = 'gatherWindow.partyInviteAcceptText' + elif label is cls.DECLINE: + rsrc = 'gatherWindow.partyInviteDeclineText' + elif label is cls.IGNORE: + rsrc = 'gatherWindow.partyInviteIgnoreText' + elif label is cls.CLAIM: + rsrc = 'claimText' + elif label is cls.DISCARD: + rsrc = 'discardText' + else: + assert_never(label) + + return babase.Lstr(resource=rsrc) diff --git a/src/assets/ba_data/python/baclassic/_chest.py b/src/assets/ba_data/python/baclassic/_chest.py new file mode 100644 index 000000000..b5e18ceb8 --- /dev/null +++ b/src/assets/ba_data/python/baclassic/_chest.py @@ -0,0 +1,91 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Chest related functionality.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from bacommon.bs import ClassicChestAppearance + +if TYPE_CHECKING: + pass + + +@dataclass +class ChestAppearanceDisplayInfo: + """Info about how to locally display chest appearances.""" + + # NOTE TO SELF: Don't rename these attrs; the C++ layer is hard + # coded to look for them. + + texclosed: str + texclosedtint: str + texopen: str + texopentint: str + color: tuple[float, float, float] + tint: tuple[float, float, float] + tint2: tuple[float, float, float] + + +# Info for chest types we know how to draw. Anything not found in here +# should fall back to the DEFAULT entry. +CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT = ChestAppearanceDisplayInfo( + texclosed='chestIcon', + texclosedtint='chestIconTint', + texopen='chestOpenIcon', + texopentint='chestOpenIconTint', + color=(1, 1, 1), + tint=(1, 1, 1), + tint2=(1, 1, 1), +) + +CHEST_APPEARANCE_DISPLAY_INFOS: dict[ + ClassicChestAppearance, ChestAppearanceDisplayInfo +] = { + ClassicChestAppearance.L2: ChestAppearanceDisplayInfo( + texclosed='chestIcon', + texclosedtint='chestIconTint', + texopen='chestOpenIcon', + texopentint='chestOpenIconTint', + color=(0.8, 1.0, 0.93), + tint=(0.65, 1.0, 0.8), + tint2=(0.65, 1.0, 0.8), + ), + ClassicChestAppearance.L3: ChestAppearanceDisplayInfo( + texclosed='chestIcon', + texclosedtint='chestIconTint', + texopen='chestOpenIcon', + texopentint='chestOpenIconTint', + color=(0.75, 0.9, 1.3), + tint=(0.7, 1, 1.9), + tint2=(0.7, 1, 1.9), + ), + ClassicChestAppearance.L4: ChestAppearanceDisplayInfo( + texclosed='chestIcon', + texclosedtint='chestIconTint', + texopen='chestOpenIcon', + texopentint='chestOpenIconTint', + color=(0.7, 1.0, 1.4), + tint=(1.4, 1.6, 2.0), + tint2=(1.4, 1.6, 2.0), + ), + ClassicChestAppearance.L5: ChestAppearanceDisplayInfo( + texclosed='chestIcon', + texclosedtint='chestIconTint', + texopen='chestOpenIcon', + texopentint='chestOpenIconTint', + color=(0.75, 0.5, 2.4), + tint=(1.0, 0.8, 0.0), + tint2=(1.0, 0.8, 0.0), + ), + ClassicChestAppearance.L6: ChestAppearanceDisplayInfo( + texclosed='chestIcon', + texclosedtint='chestIconTint', + texopen='chestOpenIcon', + texopentint='chestOpenIconTint', + color=(1.1, 0.8, 0.0), + tint=(2, 2, 2), + tint2=(2, 2, 2), + ), +} diff --git a/src/assets/ba_data/python/baclassic/_clienteffect.py b/src/assets/ba_data/python/baclassic/_clienteffect.py new file mode 100644 index 000000000..38b0d9cc6 --- /dev/null +++ b/src/assets/ba_data/python/baclassic/_clienteffect.py @@ -0,0 +1,77 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to running client-effects from the master server.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, assert_never + +from efro.util import strict_partial + +import bacommon.bs +import bauiv1 + +if TYPE_CHECKING: + pass + + +def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None: + """Run effects.""" + # pylint: disable=too-many-branches + + delay = 0.0 + for effect in effects: + if isinstance(effect, bacommon.bs.ClientEffectScreenMessage): + textfin = bauiv1.Lstr( + translate=('serverResponses', effect.message) + ).evaluate() + if effect.subs is not None: + # Should always be even. + assert len(effect.subs) % 2 == 0 + for j in range(0, len(effect.subs) - 1, 2): + textfin = textfin.replace( + effect.subs[j], + effect.subs[j + 1], + ) + bauiv1.apptimer( + delay, + strict_partial( + bauiv1.screenmessage, textfin, color=effect.color + ), + ) + + elif isinstance(effect, bacommon.bs.ClientEffectSound): + smcls = bacommon.bs.ClientEffectSound.Sound + soundfile: str | None = None + if effect.sound is smcls.UNKNOWN: + # Server should avoid sending us sounds we don't + # support. Make some noise if it happens. + logging.error('Got unrecognized bacommon.bs.ClientEffectSound.') + elif effect.sound is smcls.CASH_REGISTER: + soundfile = 'cashRegister' + elif effect.sound is smcls.ERROR: + soundfile = 'error' + elif effect.sound is smcls.POWER_DOWN: + soundfile = 'powerdown01' + elif effect.sound is smcls.GUN_COCKING: + soundfile = 'gunCocking' + else: + assert_never(effect.sound) + if soundfile is not None: + bauiv1.apptimer( + delay, + strict_partial( + bauiv1.getsound(soundfile).play, volume=effect.volume + ), + ) + + elif isinstance(effect, bacommon.bs.ClientEffectDelay): + delay += effect.seconds + else: + # Server should not send us stuff we can't digest. Make + # some noise if it happens. + logging.error( + 'Got unrecognized bacommon.bs.ClientEffect;' + ' should not happen.' + ) diff --git a/src/assets/ba_data/python/baclassic/_tournament.py b/src/assets/ba_data/python/baclassic/_tournament.py index 242ba5275..e71d9608c 100644 --- a/src/assets/ba_data/python/baclassic/_tournament.py +++ b/src/assets/ba_data/python/baclassic/_tournament.py @@ -6,13 +6,23 @@ from typing import TYPE_CHECKING +from bacommon.bs import ClassicChestAppearance import babase +import bauiv1 +import bascenev1 + +from baclassic._chest import ( + CHEST_APPEARANCE_DISPLAY_INFOS, + CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, +) if TYPE_CHECKING: from typing import Any -def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]: +def get_tournament_prize_strings( + entry: dict[str, Any], include_tickets: bool +) -> list[str]: """Given a tournament entry, return strings for its prize levels.""" # pylint: disable=too-many-locals from bascenev1 import get_trophy_string @@ -27,7 +37,7 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]: trophy_type_2 = entry.get('prizeTrophy2') trophy_type_3 = entry.get('prizeTrophy3') out_vals = [] - for rng, prize, trophy_type in ( + for rng, ticket_prize, trophy_type in ( (range1, prize1, trophy_type_1), (range2, prize2, trophy_type_2), (range3, prize3, trophy_type_3), @@ -45,14 +55,100 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]: if trophy_type is not None: pvval += get_trophy_string(trophy_type) - # If we've got trophies but not for this entry, throw some space - # in to compensate so the ticket counts line up. - if prize is not None: + if ticket_prize is not None and include_tickets: pvval = ( babase.charstr(babase.SpecialChar.TICKET_BACKING) - + str(prize) + + str(ticket_prize) + pvval ) out_vals.append(prval) out_vals.append(pvval) return out_vals + + +def set_tournament_prize_chest_image( + entry: dict[str, Any], index: int, image: bauiv1.Widget +) -> None: + """Set image attrs representing a tourney prize chest.""" + ranges = [ + entry.get('prizeRange1'), + entry.get('prizeRange2'), + entry.get('prizeRange3'), + ] + chests = [ + entry.get('prizeChest1'), + entry.get('prizeChest2'), + entry.get('prizeChest3'), + ] + + assert 0 <= index < 3 + + # If tourney doesn't include this prize, just hide the image. + if ranges[index] is None: + bauiv1.imagewidget(edit=image, opacity=0.0) + return + + try: + appearance = ClassicChestAppearance(chests[index]) + except ValueError: + appearance = ClassicChestAppearance.DEFAULT + chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get( + appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT + ) + bauiv1.imagewidget( + edit=image, + opacity=1.0, + color=chestdisplayinfo.color, + texture=bauiv1.gettexture(chestdisplayinfo.texclosed), + tint_texture=bauiv1.gettexture(chestdisplayinfo.texclosedtint), + tint_color=chestdisplayinfo.tint, + tint2_color=chestdisplayinfo.tint2, + ) + + +def create_in_game_tournament_prize_image( + entry: dict[str, Any], index: int, position: tuple[float, float] +) -> None: + """Create a display for the prize chest (if any) in-game.""" + from bascenev1lib.actor.image import Image + + ranges = [ + entry.get('prizeRange1'), + entry.get('prizeRange2'), + entry.get('prizeRange3'), + ] + chests = [ + entry.get('prizeChest1'), + entry.get('prizeChest2'), + entry.get('prizeChest3'), + ] + + # If tourney doesn't include this prize, no-op. + if ranges[index] is None: + return + + try: + appearance = ClassicChestAppearance(chests[index]) + except ValueError: + appearance = ClassicChestAppearance.DEFAULT + chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get( + appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT + ) + Image( + # Provide magical extended dict version of texture that Image + # actor supports. + texture={ + 'texture': bascenev1.gettexture(chestdisplayinfo.texclosed), + 'tint_texture': bascenev1.gettexture( + chestdisplayinfo.texclosedtint + ), + 'tint_color': chestdisplayinfo.tint, + 'tint2_color': chestdisplayinfo.tint2, + 'mask_texture': None, + }, + color=chestdisplayinfo.color + (1.0,), + position=position, + scale=(48.0, 48.0), + transition=Image.Transition.FADE_IN, + transition_delay=2.0, + ).autoretain() diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index 370155c14..cf4a7be83 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -53,7 +53,7 @@ # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 22155 +TARGET_BALLISTICA_BUILD = 22178 TARGET_BALLISTICA_VERSION = '1.7.37' diff --git a/src/assets/ba_data/python/baplus/_appsubsystem.py b/src/assets/ba_data/python/baplus/_appsubsystem.py index fd5b52815..809f387b6 100644 --- a/src/assets/ba_data/python/baplus/_appsubsystem.py +++ b/src/assets/ba_data/python/baplus/_appsubsystem.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: from typing import Callable, Any + import bacommon.bs from babase import AccountV2Subsystem from baplus._cloud import CloudSubsystem diff --git a/src/assets/ba_data/python/baplus/_cloud.py b/src/assets/ba_data/python/baplus/_cloud.py index fdfcdf56a..8be76293d 100644 --- a/src/assets/ba_data/python/baplus/_cloud.py +++ b/src/assets/ba_data/python/baplus/_cloud.py @@ -15,6 +15,7 @@ from efro.message import Message, Response import bacommon.cloud + import bacommon.bs # TODO: Should make it possible to define a protocol in bacommon.cloud and @@ -120,45 +121,45 @@ def send_message_cb( @overload def send_message_cb( self, - msg: bacommon.cloud.BSPrivatePartyMessage, + msg: bacommon.bs.PrivatePartyMessage, on_response: Callable[ - [bacommon.cloud.BSPrivatePartyResponse | Exception], None + [bacommon.bs.PrivatePartyResponse | Exception], None ], ) -> None: ... @overload def send_message_cb( self, - msg: bacommon.cloud.BSInboxRequestMessage, + msg: bacommon.bs.InboxRequestMessage, on_response: Callable[ - [bacommon.cloud.BSInboxRequestResponse | Exception], None + [bacommon.bs.InboxRequestResponse | Exception], None ], ) -> None: ... @overload def send_message_cb( self, - msg: bacommon.cloud.BSInboxEntryProcessMessage, + msg: bacommon.bs.ClientUIActionMessage, on_response: Callable[ - [bacommon.cloud.BSInboxEntryProcessResponse | Exception], None + [bacommon.bs.ClientUIActionResponse | Exception], None ], ) -> None: ... @overload def send_message_cb( self, - msg: bacommon.cloud.BSChestInfoMessage, + msg: bacommon.bs.ChestInfoMessage, on_response: Callable[ - [bacommon.cloud.BSChestInfoResponse | Exception], None + [bacommon.bs.ChestInfoResponse | Exception], None ], ) -> None: ... @overload def send_message_cb( self, - msg: bacommon.cloud.BSChestActionMessage, + msg: bacommon.bs.ChestActionMessage, on_response: Callable[ - [bacommon.cloud.BSChestActionResponse | Exception], None + [bacommon.bs.ChestActionResponse | Exception], None ], ) -> None: ... @@ -229,7 +230,7 @@ def subscribe_test( def subscribe_classic_account_data( self, - updatecall: Callable[[bacommon.cloud.BSClassicAccountLiveData], None], + updatecall: Callable[[bacommon.bs.ClassicAccountLiveData], None], ) -> babase.CloudSubscription: """Subscribe to classic account data.""" raise NotImplementedError( diff --git a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py index 085e91113..ab1f5d05a 100644 --- a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py +++ b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py @@ -357,6 +357,7 @@ def show_ui(self) -> None: h_offs = 7.0 v_offs = -280.0 + v_offs2 = -236.0 # We wanna prevent controllers users from popping up browsers # or game-center widgets in cases where they can't easily get back @@ -384,7 +385,7 @@ def show_ui(self) -> None: bui.buttonwidget( parent=rootc, color=(0.45, 0.4, 0.5), - position=(160, v_offs + 439), + position=(240, v_offs2 + 439), size=(350, 62), label=( bui.Lstr(resource='tournamentStandingsText') @@ -406,7 +407,7 @@ def show_ui(self) -> None: show_next_button = self._is_more_levels and not (env.demo or env.arcade) if not show_next_button: - h_offs += 70 + h_offs += 60 # Due to virtual-bounds changes, have to squish buttons a bit to # avoid overlapping with tips at bottom. Could look nicer to @@ -614,7 +615,6 @@ def on_player_join(self, player: bs.Player) -> None: @override def on_begin(self) -> None: - # FIXME: Clean this up. # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals @@ -882,7 +882,7 @@ def on_begin(self) -> None: # If we're not doing the world's-best button, just show a title # instead. ts_height = 300 - ts_h_offs = 210 + ts_h_offs = 290 v_offs = 40 txt = Text( ( @@ -956,7 +956,6 @@ def on_begin(self) -> None: if display_scores[i][1] is None: name_str = '-' else: - # noinspection PyUnresolvedReferences name_str = ', '.join( [p['name'] for p in display_scores[i][1]['players']] ) @@ -1025,9 +1024,8 @@ def on_begin(self) -> None: ts_h_offs = -480 v_offs = 40 - # Only make this if we don't have the button - # (never want clients to see it so no need for client-only - # version, etc). + # Only make this if we don't have the button (never want clients + # to see it so no need for client-only version, etc). if self._have_achievements: if not self._account_has_achievements: Text( @@ -1069,7 +1067,6 @@ def _play_drumroll(self) -> None: ).autoretain() def _got_friend_score_results(self, results: list[Any] | None) -> None: - # FIXME: tidy this up # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements @@ -1205,7 +1202,6 @@ def _got_friend_score_results(self, results: list[Any] | None) -> None: ).autoretain() def _got_score_results(self, results: dict[str, Any] | None) -> None: - # FIXME: tidy this up # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements @@ -1222,11 +1218,12 @@ def _got_score_results(self, results: dict[str, Any] | None) -> None: # Delay a bit if results come in too fast. assert self._begin_time is not None base_delay = max(0, 2.7 - (bs.time() - self._begin_time)) - v_offs = 20 + # v_offs = 20 + v_offs = 64 if results is None: self._score_loading_status = Text( bs.Lstr(resource='worldScoresUnavailableText'), - position=(230, 150 + v_offs), + position=(280, 130 + v_offs), color=(1, 1, 1, 0.4), transition=Text.Transition.FADE_IN, transition_delay=base_delay + 0.3, @@ -1271,7 +1268,7 @@ def _got_score_results(self, results: dict[str, Any] | None) -> None: (1.5 + base_delay), bs.WeakCall(self._show_world_rank, offs_x), ) - ts_h_offs = 200 + ts_h_offs = 280 ts_height = 300 # Show world tops. @@ -1299,7 +1296,7 @@ def _got_score_results(self, results: dict[str, Any] | None) -> None: transition_delay=base_delay + 0.3, ).autoretain() else: - v_offs += 20 + v_offs += 40 h_offs_extra = 0 v_offs_names = 0 @@ -1326,6 +1323,37 @@ def _got_score_results(self, results: dict[str, Any] | None) -> None: random.randrange(0, len(times) + 1), (base_delay + i * 0.05, base_delay + 0.4 + i * 0.05), ) + + # Conundrum: We want to place line numbers to the + # left of our score column based on the largest + # score width. However scores may use Lstrs and thus + # may have different widths in different languages. + # We don't want to bake down the Lstrs we display + # because then clients can't view scores in their + # own language. So as a compromise lets measure + # max-width based on baked down Lstrs but then + # display regular Lstrs with max-width set based on + # that. Hopefully that'll look reasonable for most + # languages. + max_score_width = 10.0 + for tval in self._show_info['tops']: + score = int(tval[0]) + name_str = tval[1] + if name_str != '-': + max_score_width = max( + max_score_width, + bui.get_string_width( + ( + str(score) + if self._score_type == 'points' + else bs.timestring( + (score * 10) / 1000.0 + ).evaluate() + ), + suppress_warning=True, + ), + ) + for i, tval in enumerate(self._show_info['tops']): score = int(tval[0]) name_str = tval[1] @@ -1347,12 +1375,37 @@ def _got_score_results(self, results: dict[str, Any] | None) -> None: tdelay2 = times[i][1] if name_str != '-': + sstr = ( + str(score) + if self._score_type == 'points' + else bs.timestring((score * 10) / 1000.0) + ) + + # Line number. Text( - ( - str(score) - if self._score_type == 'points' - else bs.timestring((score * 10) / 1000.0) + str(i + 1), + position=( + ts_h_offs + + 20 + + h_offs_extra + - max_score_width + - 8.0, + ts_height / 2 + + -ts_height * (i + 1) / 10 + + v_offs + - 30.0, ), + scale=0.5, + h_align=Text.HAlign.RIGHT, + v_align=Text.VAlign.CENTER, + color=(0.3, 0.3, 0.3), + transition=Text.Transition.IN_LEFT, + transition_delay=tdelay1, + ).autoretain() + + # Score. + Text( + sstr, position=( ts_h_offs + 20 + h_offs_extra, ts_height / 2 @@ -1360,6 +1413,7 @@ def _got_score_results(self, results: dict[str, Any] | None) -> None: + v_offs - 30.0, ), + maxwidth=max_score_width, h_align=Text.HAlign.RIGHT, v_align=Text.VAlign.CENTER, color=color0, @@ -1367,6 +1421,7 @@ def _got_score_results(self, results: dict[str, Any] | None) -> None: transition=Text.Transition.IN_LEFT, transition_delay=tdelay1, ).autoretain() + # Player name. Text( bs.Lstr(value=name_str), position=( @@ -1470,16 +1525,12 @@ def _show_world_rank(self, offs_x: float) -> None: ] # pylint: disable=useless-suppression # pylint: disable=unbalanced-tuple-unpacking - ( - pr1, - pv1, - pr2, - pv2, - pr3, - pv3, - ) = bs.app.classic.get_tournament_prize_strings( - tourney_info + (pr1, pv1, pr2, pv2, pr3, pv3) = ( + bs.app.classic.get_tournament_prize_strings( + tourney_info, include_tickets=False + ) ) + # pylint: enable=unbalanced-tuple-unpacking # pylint: enable=useless-suppression @@ -1495,10 +1546,14 @@ def _show_world_rank(self, offs_x: float) -> None: transition_delay=2.0, ).autoretain() vval = -107 + 70 - for rng, val in ((pr1, pv1), (pr2, pv2), (pr3, pv3)): + for i, rng, val in ( + (0, pr1, pv1), + (1, pr2, pv2), + (2, pr3, pv3), + ): Text( rng, - position=(-410 + 10, vval), + position=(-430 + 10, vval), color=(1, 1, 1, 0.7), h_align=Text.HAlign.RIGHT, v_align=Text.VAlign.CENTER, @@ -1509,7 +1564,7 @@ def _show_world_rank(self, offs_x: float) -> None: ).autoretain() Text( val, - position=(-390 + 10, vval), + position=(-410 + 10, vval), color=(0.7, 0.7, 0.7, 1.0), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, @@ -1518,6 +1573,9 @@ def _show_world_rank(self, offs_x: float) -> None: maxwidth=300, transition_delay=2.0, ).autoretain() + bs.app.classic.create_in_game_tournament_prize_image( + tourney_info, i, (-410 + 70, vval) + ) vval -= 35 except Exception: logging.exception('Error showing prize ranges.') diff --git a/src/assets/ba_data/python/bascenev1lib/actor/image.py b/src/assets/ba_data/python/bascenev1lib/actor/image.py index 2a71c878e..4e38f6b12 100644 --- a/src/assets/ba_data/python/bascenev1lib/actor/image.py +++ b/src/assets/ba_data/python/bascenev1lib/actor/image.py @@ -56,15 +56,21 @@ def __init__( # pylint: disable=too-many-locals super().__init__() - # If they provided a dict as texture, assume its an icon. - # otherwise its just a texture value itself. + # If they provided a dict as texture, use it to wire up extended + # stuff like tints and masks. mask_texture: bs.Texture | None if isinstance(texture, dict): tint_color = texture['tint_color'] tint2_color = texture['tint2_color'] tint_texture = texture['tint_texture'] + + # Assume we're dealing with a character icon but allow + # overriding. + mask_tex_name = texture.get('mask_texture', 'characterIconMask') + mask_texture = ( + None if mask_tex_name is None else bs.gettexture(mask_tex_name) + ) texture = texture['texture'] - mask_texture = bs.gettexture('characterIconMask') else: tint_color = (1, 1, 1) tint2_color = None diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py index 2195ad5cb..1d51fdd60 100644 --- a/src/assets/ba_data/python/bauiv1/__init__.py +++ b/src/assets/ba_data/python/bauiv1/__init__.py @@ -114,9 +114,12 @@ hscrollwidget, imagewidget, Mesh, + root_ui_pause_updates, + root_ui_resume_updates, rowwidget, scrollwidget, set_party_window_open, + spinnerwidget, Sound, Texture, textwidget, @@ -218,6 +221,8 @@ 'quit', 'QuitType', 'request_permission', + 'root_ui_pause_updates', + 'root_ui_resume_updates', 'rowwidget', 'safecolor', 'screenmessage', @@ -228,6 +233,7 @@ 'set_ui_input_device', 'Sound', 'SpecialChar', + 'spinnerwidget', 'supports_max_fps', 'supports_vsync', 'supports_unicode_display', diff --git a/src/assets/ba_data/python/bauiv1lib/account/viewer.py b/src/assets/ba_data/python/bauiv1lib/account/viewer.py index 9f0d606a4..e38211f6b 100644 --- a/src/assets/ba_data/python/bauiv1lib/account/viewer.py +++ b/src/assets/ba_data/python/bauiv1lib/account/viewer.py @@ -100,17 +100,20 @@ def __init__( ) bui.widget(edit=self._scrollwidget, autoselect=True) + # Note to self: Make sure to always update loading text and + # spinner visibility together. self._loading_text = bui.textwidget( parent=self._scrollwidget, scale=0.5, - text=bui.Lstr( - value='${A}...', - subs=[('${A}', bui.Lstr(resource='loadingText'))], - ), + text='', size=(self._width - 60, 100), h_align='center', v_align='center', ) + self._loading_spinner = bui.spinnerwidget( + parent=self.root_widget, + position=(self._width * 0.5, self._height * 0.5), + ) # In cases where the user most likely has a browser/email, lets # offer a 'report this user' button. @@ -227,9 +230,11 @@ def _on_query_response(self, data: dict[str, Any] | None) -> None: edit=self._loading_text, text=bui.Lstr(resource='internal.unavailableNoConnectionText'), ) + bui.spinnerwidget(edit=self._loading_spinner, visible=False) else: try: self._loading_text.delete() + self._loading_spinner.delete() trophystr = '' try: trophystr = data['trophies'] diff --git a/src/assets/ba_data/python/bauiv1lib/chest.py b/src/assets/ba_data/python/bauiv1lib/chest.py index f46449f8f..5172bee6c 100644 --- a/src/assets/ba_data/python/bauiv1lib/chest.py +++ b/src/assets/ba_data/python/bauiv1lib/chest.py @@ -1,23 +1,27 @@ # Released under the MIT License. See LICENSE for details. # +# pylint: disable=too-many-lines """Provides chest related ui.""" from __future__ import annotations +import math +import random from typing import override, TYPE_CHECKING -import bacommon.cloud +import bacommon.bs import bauiv1 as bui if TYPE_CHECKING: - pass + import datetime + import baclassic + +_g_open_voices: list[tuple[float, str, float]] = [] -class ChestWindow(bui.MainWindow): - """Allows operations on a chest.""" - # def __del__(self) -> None: - # print('~ChestWindow()') +class ChestWindow(bui.MainWindow): + """Allows viewing and performing operations on a chest.""" def __init__( self, @@ -31,17 +35,25 @@ def __init__( assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - self._width = 1050 if uiscale is bui.UIScale.SMALL else 850 - self._height = ( - 500 - if uiscale is bui.UIScale.SMALL - else 500 if uiscale is bui.UIScale.MEDIUM else 500 - ) + self._width = 1050 if uiscale is bui.UIScale.SMALL else 650 + self._height = 550 if uiscale is bui.UIScale.SMALL else 450 self._xoffs = 70 if uiscale is bui.UIScale.SMALL else 0 - self._yoffs = -42 if uiscale is bui.UIScale.SMALL else -25 + self._yoffs = -50 if uiscale is bui.UIScale.SMALL else -35 self._action_in_flight = False self._open_now_button: bui.Widget | None = None + self._open_now_spinner: bui.Widget | None = None + self._open_now_texts: list[bui.Widget] = [] + self._open_now_images: list[bui.Widget] = [] self._watch_ad_button: bui.Widget | None = None + self._time_string_timer: bui.AppTimer | None = None + self._time_string_text: bui.Widget | None = None + self._prizesets: list[bacommon.bs.ChestInfoResponse.Chest.PrizeSet] = [] + self._prizeindex = -1 + self._prizesettxts: dict[int, list[bui.Widget]] = {} + self._prizesetimgs: dict[int, list[bui.Widget]] = {} + self._chestdisplayinfo: baclassic.ChestAppearanceDisplayInfo | None = ( + None + ) # The set of widgets we keep when doing a clear. self._core_widgets: list[bui.Widget] = [] @@ -51,7 +63,7 @@ def __init__( size=(self._width, self._height), toolbar_visibility='menu_full', scale=( - 1.55 + 1.45 if uiscale is bui.UIScale.SMALL else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.9 ), @@ -65,9 +77,15 @@ def __init__( origin_widget=origin_widget, ) + # Tell the root-ui to stop updating toolbar values immediately; + # this allows it to run animations based on the results of our + # chest opening. + bui.root_ui_pause_updates() + self._root_ui_updates_paused = True + self._title_text = bui.textwidget( parent=self._root_widget, - position=(0, self._height - 45 + self._yoffs), + position=(0, self._height - 50 + self._yoffs), size=(self._width, 25), text=f'Chest Slot {self._index + 1}', color=bui.app.ui_v1.title_color, @@ -96,11 +114,18 @@ def __init__( bui.containerwidget(edit=self._root_widget, cancel_button=btn) self._core_widgets.append(btn) + # Note: Don't need to explicitly clean this up. Just not adding + # it to core_widgets so it will go away on next reset. + self._loadingspinner = bui.spinnerwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + ) + self._infotext = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 200 + self._yoffs), size=(0, 0), - text=bui.Lstr(resource='loadingText'), + text='', maxwidth=700, scale=0.7, color=(0.6, 0.5, 0.6), @@ -131,12 +156,48 @@ def __init__( self._action_in_flight = True with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.cloud.BSChestInfoMessage(chest_id=str(self._index)), + bacommon.bs.ChestInfoMessage(chest_id=str(self._index)), on_response=bui.WeakCall(self._on_chest_info_response), ) + def __del__(self) -> None: + # print('~ChestWindow()') + + # Make sure UI updates are resumed if we haven't done so. + if self._root_ui_updates_paused: + bui.root_ui_resume_updates() + + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # Pull anything we need from self out here; if we do it in the + # lambda we keep self alive which is bad. + index = self._index + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + index=index, transition=transition, origin_widget=origin_widget + ) + ) + + def _update_time_display(self, unlock_time: datetime.datetime) -> None: + # Once text disappears, kill our timer. + if not self._time_string_text: + self._time_string_timer = None + return + now = bui.utc_now_cloud() + secs_till_open = max(0.0, (unlock_time - now).total_seconds()) + tstr = ( + bui.timestring(secs_till_open, centi=False) + if secs_till_open > 0 + else '' + ) + bui.textwidget(edit=self._time_string_text, text=tstr) + def _on_chest_info_response( - self, response: bacommon.cloud.BSChestInfoResponse | Exception + self, response: bacommon.bs.ChestInfoResponse | Exception ) -> None: assert self._action_in_flight # Should be us. self._action_in_flight = False @@ -151,10 +212,11 @@ def _on_chest_info_response( self._show_about_chest_slots() return - self.show_chest_actions(response.chest) + assert response.user_tokens is not None + self._show_chest_actions(response.user_tokens, response.chest) def _on_chest_action_response( - self, response: bacommon.cloud.BSChestActionResponse | Exception + self, response: bacommon.bs.ChestActionResponse | Exception ) -> None: assert self._action_in_flight # Should be us. self._action_in_flight = False @@ -171,71 +233,438 @@ def _on_chest_action_response( self._error(bui.Lstr(translate=('serverResponses', response.error))) return + # Show any bundled success message. + if response.success_msg is not None: + bui.screenmessage( + bui.Lstr(translate=('serverResponses', response.success_msg)), + color=(0, 1.0, 0), + ) + bui.getsound('cashRegister').play() + + # Show any bundled warning. + if response.warning is not None: + bui.screenmessage( + bui.Lstr(translate=('serverResponses', response.warning)), + color=(1, 0.5, 0), + ) + bui.getsound('error').play() + + # If we just paid for something, make a sound accordingly. + if bool(False): # Hmm maybe this feels odd. + if response.tokens_charged > 0: + bui.getsound('cashRegister').play() + # If there's contents listed in the response, show them. if response.contents is not None: - print('WOULD SHOW CONTENTS:', response.contents) + self._show_chest_contents(response) else: # Otherwise we're done here; just close out our UI. self.main_window_back() - def show_chest_actions( - self, chest: bacommon.cloud.BSChestInfoResponse.Chest + def _show_chest_actions( + self, user_tokens: int, chest: bacommon.bs.ChestInfoResponse.Chest ) -> None: """Show state for our chest.""" + # pylint: disable=too-many-locals # pylint: disable=cyclic-import - from baclassic import ClassicAppMode + from baclassic import ( + ClassicAppMode, + CHEST_APPEARANCE_DISPLAY_INFOS, + CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, + ) - # We expect to be run under classic. + plus = bui.app.plus + assert plus is not None + + # We expect to be run under classic app mode. mode = bui.app.mode if not isinstance(mode, ClassicAppMode): self._error('Classic app mode not active.') return - now = bui.utc_now_cloud() - secs_till_open = max(0.0, (chest.unlock_time - now).total_seconds()) - tstr = bui.timestring(secs_till_open, centi=False) + self._reset() + + self._chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get( + chest.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT + ) bui.textwidget( + edit=self._title_text, text=f'{chest.appearance.name} Chest' + ) + + imgsize = 145 + bui.imagewidget( parent=self._root_widget, - position=(self._width * 0.5, self._height - 120 + self._yoffs), + position=( + self._width * 0.5 - imgsize * 0.5, + self._height - 223 + self._yoffs, + ), + color=self._chestdisplayinfo.color, + size=(imgsize, imgsize), + texture=bui.gettexture(self._chestdisplayinfo.texclosed), + tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint), + tint_color=self._chestdisplayinfo.tint, + tint2_color=self._chestdisplayinfo.tint2, + ) + + # Store the prize-sets so we can display odds/etc. Sort them + # with largest weights first. + self._prizesets = sorted( + chest.prizesets, key=lambda s: s.weight, reverse=True + ) + + if chest.unlock_tokens > 0: + lsize = 30 + bui.imagewidget( + parent=self._root_widget, + position=( + self._width * 0.5 - imgsize * 0.4 - lsize * 0.5, + self._height - 223 + 27.0 + self._yoffs, + ), + size=(lsize, lsize), + texture=bui.gettexture('lock'), + ) + + # Time string. + self._time_string_text = bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 85 + self._yoffs), size=(0, 0), - text=tstr, + text='', maxwidth=700, - scale=0.7, - color=(0.6, 0.5, 0.6), + scale=0.6, + color=(0.6, 1.0, 0.6), h_align='center', v_align='center', ) + self._update_time_display(chest.unlock_time) + self._time_string_timer = bui.AppTimer( + 1.0, + repeat=True, + call=bui.WeakCall(self._update_time_display, chest.unlock_time), + ) + + # Allow watching an ad IF the server tells us we can AND we have + # an ad ready to show. + show_ad_button = ( + chest.unlock_tokens > 0 + and chest.ad_allow + and plus.have_incentivized_ad() + ) + + bwidth = 130 + bheight = 90 + bposy = -330 if chest.unlock_tokens == 0 else -340 + hspace = 20 + boffsx = (hspace * -0.5 - bwidth * 0.5) if show_ad_button else 0.0 + self._open_now_button = bui.buttonwidget( parent=self._root_widget, position=( - self._width * 0.5 - 200, - self._height - 250 + self._yoffs, + self._width * 0.5 - bwidth * 0.5 + boffsx, + self._height + bposy + self._yoffs, ), - size=(150, 100), - label=f'OPEN NOW FOR {chest.unlock_tokens} TOKENS', + size=(bwidth, bheight), + label='', button_type='square', autoselect=True, on_activate_call=bui.WeakCall( - self._open_now_press, chest.unlock_tokens + self._open_press, user_tokens, chest.unlock_tokens ), + enable_sound=False, ) + self._open_now_images = [] + self._open_now_texts = [] - self._watch_ad_button = bui.buttonwidget( + iconsize = 50 + if chest.unlock_tokens == 0: + self._open_now_texts.append( + bui.textwidget( + parent=self._root_widget, + text='Open', + position=( + self._width * 0.5 + boffsx, + self._height + bposy + self._yoffs + bheight * 0.5, + ), + color=(0, 1, 0), + draw_controller=self._open_now_button, + scale=0.7, + maxwidth=bwidth * 0.8, + size=(0, 0), + h_align='center', + v_align='center', + ) + ) + else: + self._open_now_texts.append( + bui.textwidget( + parent=self._root_widget, + text='Open Now', + position=( + self._width * 0.5 + boffsx, + self._height + bposy + self._yoffs + bheight * 1.15, + ), + maxwidth=bwidth * 0.8, + scale=0.7, + color=(0.7, 1, 0.7), + size=(0, 0), + h_align='center', + v_align='center', + ) + ) + self._open_now_images.append( + bui.imagewidget( + parent=self._root_widget, + size=(iconsize, iconsize), + position=( + self._width * 0.5 - iconsize * 0.5 + boffsx, + self._height + bposy + self._yoffs + bheight * 0.35, + ), + draw_controller=self._open_now_button, + texture=bui.gettexture('coin'), + ) + ) + self._open_now_texts.append( + bui.textwidget( + parent=self._root_widget, + text=bui.Lstr( + resource='tokens.numTokensText', + subs=[('${COUNT}', str(chest.unlock_tokens))], + ), + position=( + self._width * 0.5 + boffsx, + self._height + bposy + self._yoffs + bheight * 0.25, + ), + scale=0.65, + color=(0, 1, 0), + draw_controller=self._open_now_button, + maxwidth=bwidth * 0.8, + size=(0, 0), + h_align='center', + v_align='center', + ) + ) + self._open_now_spinner = bui.spinnerwidget( parent=self._root_widget, position=( - self._width * 0.5 + 50, - self._height - 250 + self._yoffs, + self._width * 0.5 + boffsx, + self._height + bposy + self._yoffs + 0.5 * bheight, ), - size=(150, 100), - label='WATCH AN AD TO REDUCE WAIT', - button_type='square', - autoselect=True, - on_activate_call=bui.WeakCall(self._watch_ad_press), + visible=False, + ) + + if show_ad_button: + bui.textwidget( + parent=self._root_widget, + text='Reduce Wait', + position=( + self._width * 0.5 + hspace * 0.5 + bwidth * 0.5, + self._height + bposy + self._yoffs + bheight * 1.15, + ), + maxwidth=bwidth * 0.8, + scale=0.7, + color=(0.7, 1, 0.7), + size=(0, 0), + h_align='center', + v_align='center', + ) + self._watch_ad_button = bui.buttonwidget( + parent=self._root_widget, + position=( + self._width * 0.5 + hspace * 0.5, + self._height + bposy + self._yoffs, + ), + size=(bwidth, bheight), + label='', + button_type='square', + autoselect=True, + on_activate_call=bui.WeakCall(self._watch_ad_press), + enable_sound=False, + ) + bui.imagewidget( + parent=self._root_widget, + size=(iconsize, iconsize), + position=( + self._width * 0.5 + + hspace * 0.5 + + bwidth * 0.5 + - iconsize * 0.5, + self._height + bposy + self._yoffs + bheight * 0.35, + ), + draw_controller=self._watch_ad_button, + color=(1.5, 1.0, 2.0), + texture=bui.gettexture('tv'), + ) + # Note to self: AdMob requires rewarded ad usage + # specifically says 'Ad' in it. + bui.textwidget( + parent=self._root_widget, + text=bui.Lstr(resource='watchAnAdText'), + position=( + self._width * 0.5 + hspace * 0.5 + bwidth * 0.5, + self._height + bposy + self._yoffs + bheight * 0.25, + ), + scale=0.65, + color=(0, 1, 0), + draw_controller=self._watch_ad_button, + maxwidth=bwidth * 0.8, + size=(0, 0), + h_align='center', + v_align='center', + ) + + self._show_odds(initial_highlighted_row=-1) + # bui.textwidget(edit=self._infotext, text='') + + def _highlight_odds_row(self, row: int, extra: bool = False) -> None: + + for rindex, imgs in self._prizesetimgs.items(): + opacity = ( + (0.9 if extra else 0.75) + if rindex == row + else (0.4 if extra else 0.5) + ) + for img in imgs: + if img: + bui.imagewidget(edit=img, opacity=opacity) + + for rindex, txts in self._prizesettxts.items(): + opacity = ( + (0.9 if extra else 0.75) + if rindex == row + else (0.4 if extra else 0.5) + ) + for txt in txts: + if txt: + bui.textwidget(edit=txt, color=(0.7, 0.65, 1, opacity)) + + def _show_odds( + self, + *, + initial_highlighted_row: int, + initial_highlighted_extra: bool = False, + ) -> None: + # pylint: disable=too-many-locals + xoffs = 110 + + totalweight = max(0.001, sum(t.weight for t in self._prizesets)) + + rowheight = 25 + totalheight = (len(self._prizesets) + 1) * rowheight + x = self._width * 0.5 + xoffs + y = self._height + self._yoffs - 150.0 + totalheight * 0.5 + + # Title. + bui.textwidget( + parent=self._root_widget, + text='Prize Odds', + color=(0.7, 0.65, 1, 0.5), + flatness=1.0, + shadow=1.0, + position=(x, y), + scale=0.55, + size=(0, 0), + h_align='left', + v_align='center', ) - bui.textwidget(edit=self._infotext, text='') + y -= 5.0 + + prizesettxts: list[bui.Widget] + prizesetimgs: list[bui.Widget] + + def _mkicon(img: str) -> None: + iconsize = 20.0 + nonlocal x + nonlocal prizesetimgs + prizesetimgs.append( + bui.imagewidget( + parent=self._root_widget, + size=(iconsize, iconsize), + position=(x, y - iconsize * 0.5), + texture=bui.gettexture(img), + opacity=0.4, + ) + ) + x += iconsize + + def _mktxt(txt: str, advance: bool = True) -> None: + tscale = 0.45 + nonlocal x + nonlocal prizesettxts + prizesettxts.append( + bui.textwidget( + parent=self._root_widget, + text=txt, + flatness=1.0, + shadow=1.0, + position=(x, y), + scale=tscale, + size=(0, 0), + h_align='left', + v_align='center', + ) + ) + if advance: + x += (bui.get_string_width(txt, suppress_warning=True)) * tscale + + self._prizesettxts = {} + self._prizesetimgs = {} + + for i, p in enumerate(self._prizesets): + prizesettxts = self._prizesettxts.setdefault(i, []) + prizesetimgs = self._prizesetimgs.setdefault(i, []) + x = self._width * 0.5 + xoffs + y -= rowheight + percent = 100.0 * p.weight / totalweight + + # Show decimals only if we get very small percentages (looks + # better than rounding as '0%'). + percenttxt = ( + f'{percent:.2f}' + if percent < 0.1 + else ( + f'{percent:.1f}' if percent < 1.0 else f'{round(percent)}%:' + ) + ) - def _open_now_press(self, token_payment: int) -> None: + # We advance manually here to keep values lined up + # (otherwise single digit percent rows don't line up with + # double digit ones). + _mktxt(percenttxt, advance=False) + x += 35.0 + + for item in p.contents: + x += 5.0 + if isinstance(item.item, bacommon.bs.TicketsDisplayItem): + _mktxt(str(item.item.count)) + _mkicon('tickets') + elif isinstance(item.item, bacommon.bs.TokensDisplayItem): + _mktxt(str(item.item.count)) + _mkicon('coin') + else: + # For other cases just fall back on text desc. + # + # Translate the wrapper description and apply any subs. + descfin = bui.Lstr( + translate=('serverResponses', item.description) + ).evaluate() + subs = ( + [] + if item.description_subs is None + else item.description_subs + ) + assert len(subs) % 2 == 0 # Should always be even. + for j in range(0, len(subs) - 1, 2): + descfin = descfin.replace(subs[j], subs[j + 1]) + _mktxt(descfin) + self._highlight_odds_row( + initial_highlighted_row, extra=initial_highlighted_extra + ) + + def _open_press(self, user_tokens: int, token_payment: int) -> None: + from bauiv1lib.gettokens import show_get_tokens_prompt + + bui.getsound('click01').play() # Allow only one in-flight action at once. if self._action_in_flight: @@ -252,12 +681,20 @@ def _open_now_press(self, token_payment: int) -> None: self._error(bui.Lstr(resource='notSignedInText')) return + # Offer to purchase tokens if they don't have enough. + if user_tokens < token_payment: + # Hack: We disable normal swish for the open button and it + # seems weird without a swish here, so explicitly do one. + bui.getsound('swish').play() + show_get_tokens_prompt() + return + self._action_in_flight = True with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.cloud.BSChestActionMessage( + bacommon.bs.ChestActionMessage( chest_id=str(self._index), - action=bacommon.cloud.BSChestActionMessage.Action.UNLOCK, + action=bacommon.bs.ChestActionMessage.Action.UNLOCK, token_payment=token_payment, ), on_response=bui.WeakCall(self._on_chest_action_response), @@ -265,10 +702,46 @@ def _open_now_press(self, token_payment: int) -> None: # Convey that something is in progress. if self._open_now_button: - bui.buttonwidget(edit=self._open_now_button, label='...') + # bui.buttonwidget(edit=self._open_now_button, + # color=(0.4, 1.0, 0.4)) + bui.spinnerwidget(edit=self._open_now_spinner, visible=True) + for twidget in self._open_now_texts: + bui.textwidget(edit=twidget, color=(1, 1, 1, 0.2)) + for iwidget in self._open_now_images: + bui.imagewidget(edit=iwidget, opacity=0.2) def _watch_ad_press(self) -> None: + bui.getsound('click01').play() + + # Allow only one in-flight action at once. + if self._action_in_flight: + bui.screenmessage( + bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0) + ) + bui.getsound('error').play() + return + + assert bui.app.classic is not None + + self._action_in_flight = True + bui.app.classic.ads.show_ad_2( + 'reduce_chest_wait', + on_completion_call=bui.WeakCall(self._watch_ad_complete), + ) + + # Convey that something is in progress. + if self._watch_ad_button: + bui.buttonwidget(edit=self._watch_ad_button, color=(0.4, 0.4, 0.4)) + + def _watch_ad_complete(self, actually_showed: bool) -> None: + + assert self._action_in_flight # Should be ad view. + self._action_in_flight = False + + if not actually_showed: + return + # Allow only one in-flight action at once. if self._action_in_flight: bui.screenmessage( @@ -287,23 +760,20 @@ def _watch_ad_press(self) -> None: self._action_in_flight = True with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.cloud.BSChestActionMessage( + bacommon.bs.ChestActionMessage( chest_id=str(self._index), - action=bacommon.cloud.BSChestActionMessage.Action.AD, + action=bacommon.bs.ChestActionMessage.Action.AD, token_payment=0, ), on_response=bui.WeakCall(self._on_chest_action_response), ) - # Convey that something is in progress. - if self._watch_ad_button: - bui.buttonwidget(edit=self._watch_ad_button, label='...') - def _reset(self) -> None: - """Clear all non-permanent widgets.""" + """Clear all non-permanent widgets and clear infotext.""" for widget in self._root_widget.get_children(): if widget not in self._core_widgets: widget.delete() + bui.textwidget(edit=self._infotext, text='', color=(1, 1, 1)) def _error(self, msg: str | bui.Lstr) -> None: """Put ourself in an error state with a visible error message.""" @@ -311,34 +781,310 @@ def _error(self, msg: str | bui.Lstr) -> None: bui.textwidget(edit=self._infotext, text=msg, color=(1, 0, 0)) def _show_about_chest_slots(self) -> None: + # No-op if our ui is dead. + if not self._root_widget: + return + self._reset() msg = ( - 'This empty slot can hold a treasure chest.\n' - 'Treasure chests are earned through gameplay.' + 'This slot can hold a treasure chest.\n\n' + 'Earn chests by beating campaing levels,\n' + 'placing in tournaments, and completing\n' + 'achievements.' ) bui.textwidget(edit=self._infotext, text=msg, color=(1, 1, 1)) - @override - def get_main_window_state(self) -> bui.MainWindowState: - # Support recreating our window for back/refresh purposes. - cls = type(self) + def _show_chest_contents( + self, response: bacommon.bs.ChestActionResponse + ) -> None: + # pylint: disable=too-many-locals - # Pull anything we need from self out here; if we do it in the - # lambda we keep self alive which is bad. - index = self._index + # No-op if our ui is dead. + if not self._root_widget: + return - return bui.BasicMainWindowState( - create_call=lambda transition, origin_widget: cls( - index=index, transition=transition, origin_widget=origin_widget + assert response.contents is not None + + tincr = 0.4 + tendoffs = tincr * 4.0 + toffs = 0.0 + + bui.getsound('revUp').play(volume=2.0) + + # Show nothing but the chest icon and animate it shaking. + self._reset() + imgsize = 145 + assert self._chestdisplayinfo is not None + img = bui.imagewidget( + parent=self._root_widget, + color=self._chestdisplayinfo.color, + texture=bui.gettexture(self._chestdisplayinfo.texclosed), + tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint), + tint_color=self._chestdisplayinfo.tint, + tint2_color=self._chestdisplayinfo.tint2, + ) + + def _set_img(x: float, scale: float) -> None: + if not img: + return + bui.imagewidget( + edit=img, + position=( + self._width * 0.5 - imgsize * scale * 0.5 + x, + self._height + - 223 + + self._yoffs + + imgsize * 0.5 + - imgsize * scale * 0.5, + ), + size=(imgsize * scale, imgsize * scale), + ) + + # Set initial place. + _set_img(0.0, 1.0) + + sign = 1.0 + while toffs < tendoffs: + toffs += 0.03 * random.uniform(0.5, 1.5) + sign = -sign + bui.apptimer( + toffs, + bui.Call( + _set_img, + x=( + 20.0 + * random.uniform(0.3, 1.0) + * math.pow(toffs / tendoffs, 2.0) + * sign + ), + scale=1.0 - 0.2 * math.pow(toffs / tendoffs, 2.0), + ), + ) + + xspacing = 150 + xoffs = -0.5 * (len(response.contents) - 1) * xspacing + bui.apptimer( + toffs - 0.2, lambda: bui.getsound('corkPop2').play(volume=4.0) + ) + # Play a variety of voice sounds. + + # We keep a global list of voice options which we randomly pull + # from and refill when empty. This ensures everything gets + # played somewhat frequently and minimizes annoying repeats. + global _g_open_voices # pylint: disable=global-statement + if not _g_open_voices: + _g_open_voices = [ + (0.3, 'woo3', 2.5), + (0.1, 'gasp', 1.3), + (0.2, 'woo2', 2.0), + (0.2, 'wow', 2.0), + (0.2, 'kronk2', 2.0), + (0.2, 'mel03', 2.0), + (0.2, 'aww', 2.0), + (0.4, 'nice', 2.0), + (0.3, 'yeah', 1.5), + (0.2, 'woo', 1.0), + (0.5, 'ooh', 0.8), + ] + + voicetimeoffs, voicename, volume = _g_open_voices.pop( + random.randrange(len(_g_open_voices)) + ) + bui.apptimer( + toffs + voicetimeoffs, + lambda: bui.getsound(voicename).play(volume=volume), + ) + + toffsopen = toffs + bui.apptimer(toffs, bui.WeakCall(self._show_chest_opening)) + toffs += tincr * 1.0 + width = xspacing * 0.75 + for obj in response.contents: + toffs += tincr + bui.apptimer( + toffs - 0.1, lambda: bui.getsound('cashRegister').play() + ) + bui.apptimer( + toffs, bui.WeakCall(self._show_chest_item, obj, xoffs, width) + ) + xoffs += xspacing + toffs += tincr + bui.apptimer(toffs, bui.WeakCall(self._show_done_button)) + + self._show_odds(initial_highlighted_row=-1) + + # Store this for later + self._prizeindex = response.prizeindex + + # The final result was already randomly selected on the server, + # but we want to give the illusion of randomness here, so cycle + # through highlighting our options and stop on the winner when + # the chest opens. To do this, we start at the end at the prize + # and work backwards setting timers. + if self._prizesets: + toffs2 = toffsopen - 0.01 + amt = 0.02 + i = self._prizeindex + while toffs2 > 0.0: + bui.apptimer( + toffs2, + bui.WeakCall(self._highlight_odds_row, i), + ) + toffs2 -= amt + amt *= 1.05 * random.uniform(0.9, 1.1) + i = (i - 1) % len(self._prizesets) + + def _show_chest_opening(self) -> None: + + # No-op if our ui is dead. + if not self._root_widget: + return + + self._reset() + imgsize = 145 + bui.getsound('hiss').play() + assert self._chestdisplayinfo is not None + img = bui.imagewidget( + parent=self._root_widget, + color=self._chestdisplayinfo.color, + texture=bui.gettexture(self._chestdisplayinfo.texopen), + tint_texture=bui.gettexture(self._chestdisplayinfo.texopentint), + tint_color=self._chestdisplayinfo.tint, + tint2_color=self._chestdisplayinfo.tint2, + ) + tincr = 0.8 + tendoffs = tincr * 2.0 + toffs = 0.0 + + def _set_img(x: float, scale: float) -> None: + if not img: + return + bui.imagewidget( + edit=img, + position=( + self._width * 0.5 - imgsize * scale * 0.5 + x, + self._height + - 223 + + self._yoffs + + imgsize * 0.5 + - imgsize * scale * 0.5, + ), + size=(imgsize * scale, imgsize * scale), + ) + + # Set initial place. + _set_img(0.0, 1.0) + + sign = 1.0 + while toffs < tendoffs: + toffs += 0.03 * random.uniform(0.5, 1.5) + sign = -sign + # Note: we speed x along here (multing toffs) so position + # comes to rest before scale. + bui.apptimer( + toffs, + bui.Call( + _set_img, + x=( + 1.0 + * random.uniform(0.3, 1.0) + * ( + 1.0 + - math.pow(min(1.0, 3.0 * toffs / tendoffs), 2.0) + ) + * sign + ), + scale=1.0 - 0.1 * math.pow(toffs / tendoffs, 0.5), + ), + ) + + self._show_odds( + initial_highlighted_row=self._prizeindex, + initial_highlighted_extra=True, + ) + + def _show_chest_item( + self, + itemwrapper: bacommon.bs.DisplayItemWrapper, + xoffs: float, + width: float, + ) -> None: + # No-op if our ui is dead. + if not self._root_widget: + return + + img: str | None = None + if isinstance(itemwrapper.item, bacommon.bs.TicketsDisplayItem): + img = 'tickets' + elif isinstance(itemwrapper.item, bacommon.bs.TokensDisplayItem): + img = 'coin' + + # Translate the wrapper description and apply any subs. + descfin = bui.Lstr( + translate=('serverResponses', itemwrapper.description) + ).evaluate() + subs = ( + [] + if itemwrapper.description_subs is None + else itemwrapper.description_subs + ) + assert len(subs) % 2 == 0 # Should always be even. + for j in range(0, len(subs) - 1, 2): + descfin = descfin.replace(subs[j], subs[j + 1]) + + imgsize = 34 + if img is not None: + bui.imagewidget( + parent=self._root_widget, + position=( + self._width * 0.5 + xoffs - imgsize * 0.5, + self._height - 252 + 14.0 + self._yoffs - imgsize * 0.5, + ), + size=(imgsize, imgsize), + texture=bui.gettexture(img), ) + bui.textwidget( + parent=self._root_widget, + position=( + self._width * 0.5 + xoffs, + self._height - 252 - 14.0 + self._yoffs, + ), + scale=0.65, + size=(0, 0), + text=f'+ {descfin}', + maxwidth=width, + color=(0.0, 1.0, 0.0), + h_align='center', + v_align='center', + ) + + def _show_done_button(self) -> None: + # No-op if our ui is dead. + if not self._root_widget: + return + + bwidth = 200 + bheight = 60 + + btn = bui.buttonwidget( + parent=self._root_widget, + position=( + self._width * 0.5 - bwidth * 0.5, + self._height - 350 + self._yoffs, + ), + size=(bwidth, bheight), + label=bui.Lstr(resource='doneText'), + autoselect=True, + on_activate_call=self.main_window_back, ) + bui.containerwidget(edit=self._root_widget, start_button=btn) -# Slight hack: we define different classes for our different chest slots -# so that the default UI behavior is to replace each other when -# different ones are pressed. If they are all the same class then the -# default behavior for such presses is to toggle the existing one back -# off. +# Slight hack: we define window different classes for our different +# chest slots so that the default UI behavior is to replace each other +# when different ones are pressed. If they are all the same window class +# then the default behavior for such presses is to toggle the existing +# one back off. class ChestWindow0(ChestWindow): diff --git a/src/assets/ba_data/python/bauiv1lib/connectivity.py b/src/assets/ba_data/python/bauiv1lib/connectivity.py index 8f76fc624..0b62315ad 100644 --- a/src/assets/ba_data/python/bauiv1lib/connectivity.py +++ b/src/assets/ba_data/python/bauiv1lib/connectivity.py @@ -57,7 +57,7 @@ def __init__( ) bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height * 0.65), + position=(self._width * 0.5, self._height * 0.7), size=(0, 0), scale=1.2, h_align='center', @@ -65,9 +65,15 @@ def __init__( text=bui.Lstr(resource='internal.connectingToPartyText'), maxwidth=self._width * 0.9, ) + + self._spinner = bui.spinnerwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.54), + ) + self._info_text = bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height * 0.45), + position=(self._width * 0.5, self._height * 0.4), size=(0, 0), color=(0.6, 0.5, 0.6), flatness=1.0, @@ -115,6 +121,15 @@ def _update(self) -> None: def _connected(self) -> None: if not self._root_widget or self._root_widget.transitioning_out: return + + # Show 'connected.' and kill the spinner for the brief moment + # we're visible on our way out. + bui.textwidget( + edit=self._info_text, text=bui.Lstr(resource='remote_app.connected') + ) + if self._spinner: + self._spinner.delete() + bui.containerwidget( edit=self._root_widget, transition=('out_scale'), diff --git a/src/assets/ba_data/python/bauiv1lib/coop/browser.py b/src/assets/ba_data/python/bauiv1lib/coop/browser.py index c73137dad..ca6946567 100644 --- a/src/assets/ba_data/python/bauiv1lib/coop/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/coop/browser.py @@ -231,11 +231,11 @@ def __init__( # Don't want initial construction affecting our last-selected. self._do_selection_callbacks = False v = self._height - 95 - txt = bui.textwidget( + bui.textwidget( parent=self._root_widget, position=( self._width * 0.5, - v + 40 - (0 if uiscale is bui.UIScale.SMALL else 0), + v + 40 - (25 if uiscale is bui.UIScale.SMALL else 0), ), size=(0, 0), text=bui.Lstr( @@ -244,14 +244,11 @@ def __init__( ), h_align='center', color=app.ui_v1.title_color, - scale=1.5, - maxwidth=500, + scale=0.85 if uiscale is bui.UIScale.SMALL else 1.5, + maxwidth=280 if uiscale is bui.UIScale.SMALL else 500, v_align='center', ) - if uiscale is bui.UIScale.SMALL: - bui.textwidget(edit=txt, text='') - self._selected_row = cfg.get('Selected Coop Row', None) self._scroll_width = self._width - (130 + 2 * x_inset) diff --git a/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py b/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py index 549f5b908..a7584685b 100644 --- a/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py +++ b/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py @@ -12,6 +12,11 @@ if TYPE_CHECKING: from typing import Any, Callable +# As of 1.7.37, no longer charging entry fees for tourneys (tourneys now +# reward chests and the game now makes its money from tokens/ads used to +# speed up chest openings). +USE_ENTRY_FEES = False + class TournamentButton: """Button showing a tournament in coop window.""" @@ -25,6 +30,7 @@ def __init__( on_pressed: Callable[[TournamentButton], None], ) -> None: # pylint: disable=too-many-positional-arguments + # pylint: disable=too-many-statements self._r = 'coopSelectWindow' sclx = 300 scly = 195.0 @@ -37,6 +43,7 @@ def __init__( self.has_time_remaining: bool = False self.leader: Any = None self.required_league: str | None = None + self._base_x_offs = 0 if USE_ENTRY_FEES else -45.0 self.button = btn = bui.buttonwidget( parent=parent, position=(x + 23, y + 4), @@ -96,69 +103,72 @@ def __init__( header_color = (0.43, 0.4, 0.5, 1) value_color = (0.6, 0.6, 0.6, 1) - x_offs = 0 - bui.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 360, y + scly - 20), - size=(0, 0), - h_align='center', - text=bui.Lstr(resource=f'{self._r}.entryFeeText'), - v_align='center', - maxwidth=100, - scale=0.9, - color=header_color, - flatness=1.0, - ) + x_offs = self._base_x_offs - self.entry_fee_text_top = bui.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 360, y + scly - 60), - size=(0, 0), - h_align='center', - text='-', - v_align='center', - maxwidth=60, - scale=1.3, - color=value_color, - flatness=1.0, - ) - self.entry_fee_text_or = bui.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 360, y + scly - 90), - size=(0, 0), - h_align='center', - text='', - v_align='center', - maxwidth=60, - scale=0.5, - color=value_color, - flatness=1.0, - ) - self.entry_fee_text_remaining = bui.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 360, y + scly - 90), - size=(0, 0), - h_align='center', - text='', - v_align='center', - maxwidth=60, - scale=0.5, - color=value_color, - flatness=1.0, - ) + # No longer using entry fees. + if USE_ENTRY_FEES: + bui.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 360, y + scly - 20), + size=(0, 0), + h_align='center', + text=bui.Lstr(resource=f'{self._r}.entryFeeText'), + v_align='center', + maxwidth=100, + scale=0.9, + color=header_color, + flatness=1.0, + ) - self.entry_fee_ad_image = bui.imagewidget( - parent=parent, - size=(40, 40), - draw_controller=btn, - position=(x + 360 - 20, y + scly - 140), - opacity=0.0, - texture=bui.gettexture('tv'), - ) + self.entry_fee_text_top = bui.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 360, y + scly - 60), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=60, + scale=1.3, + color=value_color, + flatness=1.0, + ) + self.entry_fee_text_or = bui.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 360, y + scly - 90), + size=(0, 0), + h_align='center', + text='', + v_align='center', + maxwidth=60, + scale=0.5, + color=value_color, + flatness=1.0, + ) + self.entry_fee_text_remaining = bui.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 360, y + scly - 90), + size=(0, 0), + h_align='center', + text='', + v_align='center', + maxwidth=60, + scale=0.5, + color=value_color, + flatness=1.0, + ) + + self.entry_fee_ad_image = bui.imagewidget( + parent=parent, + size=(40, 40), + draw_controller=btn, + position=(x + 360 - 20, y + scly - 140), + opacity=0.0, + texture=bui.gettexture('tv'), + ) x_offs += 50 @@ -180,8 +190,8 @@ def __init__( self.button_y = y self.button_scale_y = scly - xo2 = 0 - prize_value_scale = 1.5 + # Offset for prize range/values. + xo2 = 0.0 self.prize_range_1_text = bui.textwidget( parent=parent, @@ -191,7 +201,7 @@ def __init__( h_align='right', v_align='center', maxwidth=50, - text='-', + text='', scale=0.8, color=header_color, flatness=1.0, @@ -202,13 +212,21 @@ def __init__( position=(x + 380 + xo2 + x_offs, y + scly - 93), size=(0, 0), h_align='left', - text='-', + text='', v_align='center', maxwidth=100, - scale=prize_value_scale, color=value_color, flatness=1.0, ) + self._chestsz = 50 + self.prize_chest_1_image = bui.imagewidget( + parent=parent, + draw_controller=btn, + texture=bui.gettexture('white'), + position=(x + 380 + xo2 + x_offs, y + scly - 93), + size=(self._chestsz, self._chestsz), + opacity=0.0, + ) self.prize_range_2_text = bui.textwidget( parent=parent, @@ -216,6 +234,7 @@ def __init__( position=(x + 355 + xo2 + x_offs, y + scly - 93), size=(0, 0), h_align='right', + text='', v_align='center', maxwidth=50, scale=0.8, @@ -231,10 +250,17 @@ def __init__( text='', v_align='center', maxwidth=100, - scale=prize_value_scale, color=value_color, flatness=1.0, ) + self.prize_chest_2_image = bui.imagewidget( + parent=parent, + draw_controller=btn, + texture=bui.gettexture('white'), + position=(x + 380 + xo2 + x_offs, y + scly - 93), + size=(self._chestsz, self._chestsz), + opacity=0.0, + ) self.prize_range_3_text = bui.textwidget( parent=parent, @@ -242,6 +268,7 @@ def __init__( position=(x + 355 + xo2 + x_offs, y + scly - 93), size=(0, 0), h_align='right', + text='', v_align='center', maxwidth=50, scale=0.8, @@ -257,15 +284,22 @@ def __init__( text='', v_align='center', maxwidth=100, - scale=prize_value_scale, color=value_color, flatness=1.0, ) + self.prize_chest_3_image = bui.imagewidget( + parent=parent, + draw_controller=btn, + texture=bui.gettexture('white'), + position=(x + 380 + xo2 + x_offs, y + scly - 93), + size=(self._chestsz, self._chestsz), + opacity=0.0, + ) bui.textwidget( parent=parent, draw_controller=btn, - position=(x + 620 + x_offs, y + scly - 20), + position=(x + 625 + x_offs, y + scly - 20), size=(0, 0), h_align='center', text=bui.Lstr(resource=f'{self._r}.currentBestText'), @@ -279,7 +313,7 @@ def __init__( parent=parent, draw_controller=btn, position=( - x + 620 + x_offs - (170 / 1.4) * 0.5, + x + 625 + x_offs - (170 / 1.4) * 0.5, y + scly - 60 - 40 * 0.5, ), selectable=True, @@ -299,7 +333,7 @@ def __init__( self.current_leader_score_text = bui.textwidget( parent=parent, draw_controller=btn, - position=(x + 620 + x_offs, y + scly - 113 + 10), + position=(x + 625 + x_offs, y + scly - 113 + 10), size=(0, 0), h_align='center', text='-', @@ -312,7 +346,7 @@ def __init__( self.more_scores_button = bui.buttonwidget( parent=parent, - position=(x + 620 + x_offs - 60, y + scly - 50 - 125), + position=(x + 625 + x_offs - 60, y + scly - 50 - 125), color=(0.5, 0.5, 0.6), textcolor=(0.7, 0.7, 0.8), label='-', @@ -330,7 +364,7 @@ def __init__( bui.textwidget( parent=parent, draw_controller=btn, - position=(x + 820 + x_offs, y + scly - 20), + position=(x + 840 + x_offs, y + scly - 20), size=(0, 0), h_align='center', text=bui.Lstr(resource=f'{self._r}.timeRemainingText'), @@ -343,7 +377,7 @@ def __init__( self.time_remaining_value_text = bui.textwidget( parent=parent, draw_controller=btn, - position=(x + 820 + x_offs, y + scly - 68), + position=(x + 840 + x_offs, y + scly - 68), size=(0, 0), h_align='center', text='-', @@ -356,7 +390,7 @@ def __init__( self.time_remaining_out_of_text = bui.textwidget( parent=parent, draw_controller=btn, - position=(x + 820 + x_offs, y + scly - 110), + position=(x + 840 + x_offs, y + scly - 110), size=(0, 0), h_align='center', text='-', @@ -415,26 +449,26 @@ def update_for_data(self, entry: dict[str, Any]) -> None: plus = bui.app.plus assert plus is not None - assert bui.app.classic is not None + classic = bui.app.classic + assert classic is not None + prize_y_offs = ( 34 if 'prizeRange3' in entry else 20 if 'prizeRange2' in entry else 12 ) - x_offs = 90 - - # pylint: disable=useless-suppression - # pylint: disable=unbalanced-tuple-unpacking - ( - pr1, - pv1, - pr2, - pv2, - pr3, - pv3, - ) = bui.app.classic.get_tournament_prize_strings(entry) - # pylint: enable=unbalanced-tuple-unpacking - # pylint: enable=useless-suppression + x_offs = self._base_x_offs + 90 + + # Special offset for prize ranges/vals. + x_offs2 = x_offs - 20.0 + + # Special offset for prize chests. + x_offs2c = x_offs2 + 50 + + # Fetch prize range and trophy strings. + (pr1, pv1, pr2, pv2, pr3, pv3) = classic.get_tournament_prize_strings( + entry, include_tickets=False + ) enabled = 'requiredLeague' not in entry bui.buttonwidget( @@ -446,74 +480,91 @@ def update_for_data(self, entry: dict[str, Any]) -> None: edit=self.prize_range_1_text, text='-' if pr1 == '' else pr1, position=( - self.button_x + 365 + x_offs, + self.button_x + 365 + x_offs2, self.button_y + self.button_scale_y - 93 + prize_y_offs, ), ) - # We want to draw values containing tickets a bit smaller - # (scratch that; we now draw medals a bit bigger). - ticket_char = bui.charstr(bui.SpecialChar.TICKET_BACKING) - prize_value_scale_large = 1.0 - prize_value_scale_small = 1.0 - bui.textwidget( edit=self.prize_value_1_text, text='-' if pv1 == '' else pv1, - scale=( - prize_value_scale_large - if ticket_char not in pv1 - else prize_value_scale_small - ), position=( - self.button_x + 380 + x_offs, + self.button_x + 380 + x_offs2, self.button_y + self.button_scale_y - 93 + prize_y_offs, ), ) + bui.imagewidget( + edit=self.prize_chest_1_image, + position=( + self.button_x + 380 + x_offs2c, + self.button_y + + self.button_scale_y + - 93 + + prize_y_offs + - 0.5 * self._chestsz, + ), + ) + classic.set_tournament_prize_image(entry, 0, self.prize_chest_1_image) bui.textwidget( edit=self.prize_range_2_text, text=pr2, position=( - self.button_x + 365 + x_offs, + self.button_x + 365 + x_offs2, self.button_y + self.button_scale_y - 93 - 45 + prize_y_offs, ), ) bui.textwidget( edit=self.prize_value_2_text, text=pv2, - scale=( - prize_value_scale_large - if ticket_char not in pv2 - else prize_value_scale_small - ), position=( - self.button_x + 380 + x_offs, + self.button_x + 380 + x_offs2, self.button_y + self.button_scale_y - 93 - 45 + prize_y_offs, ), ) + bui.imagewidget( + edit=self.prize_chest_2_image, + position=( + self.button_x + 380 + x_offs2c, + self.button_y + + self.button_scale_y + - 93 + - 45 + + prize_y_offs + - 0.5 * self._chestsz, + ), + ) + classic.set_tournament_prize_image(entry, 1, self.prize_chest_2_image) bui.textwidget( edit=self.prize_range_3_text, text=pr3, position=( - self.button_x + 365 + x_offs, + self.button_x + 365 + x_offs2, self.button_y + self.button_scale_y - 93 - 90 + prize_y_offs, ), ) bui.textwidget( edit=self.prize_value_3_text, text=pv3, - scale=( - prize_value_scale_large - if ticket_char not in pv3 - else prize_value_scale_small - ), position=( - self.button_x + 380 + x_offs, + self.button_x + 380 + x_offs2, self.button_y + self.button_scale_y - 93 - 90 + prize_y_offs, ), ) + bui.imagewidget( + edit=self.prize_chest_3_image, + position=( + self.button_x + 380 + x_offs2c, + self.button_y + + self.button_scale_y + - 93 + - 90 + + prize_y_offs + - 0.5 * self._chestsz, + ), + ) + classic.set_tournament_prize_image(entry, 2, self.prize_chest_3_image) leader_name = '-' leader_score: str | bui.Lstr = '-' @@ -599,6 +650,7 @@ def update_for_data(self, entry: dict[str, Any]) -> None: ) fee = entry['fee'] + assert isinstance(fee, int | None) if fee is None: fee_var = None @@ -610,18 +662,23 @@ def update_for_data(self, entry: dict[str, Any]) -> None: fee_var = 'price.tournament_entry_2' elif fee == 1: fee_var = 'price.tournament_entry_1' + elif fee == -1: + fee_var = None else: if fee != 0: print('Unknown fee value:', fee) fee_var = 'price.tournament_entry_0' - self.allow_ads = allow_ads = entry['allowAds'] + self.allow_ads = allow_ads = ( + entry['allowAds'] if USE_ENTRY_FEES else False + ) - final_fee: int | None = ( + final_fee = ( None if fee_var is None else plus.get_v1_account_misc_read_val(fee_var, '?') ) + assert isinstance(final_fee, int | None) final_fee_str: str | bui.Lstr if fee_var is None: @@ -638,72 +695,77 @@ def update_for_data(self, entry: dict[str, Any]) -> None: ad_tries_remaining = bui.app.classic.accounts.tournament_info[ self.tournament_id ]['adTriesRemaining'] + assert isinstance(ad_tries_remaining, int | None) free_tries_remaining = bui.app.classic.accounts.tournament_info[ self.tournament_id ]['freeTriesRemaining'] + assert isinstance(free_tries_remaining, int | None) # Now, if this fee allows ads and we support video ads, show # the 'or ad' version. - if allow_ads and plus.has_video_ads(): - ads_enabled = plus.have_incentivized_ad() - bui.imagewidget( - edit=self.entry_fee_ad_image, - opacity=1.0 if ads_enabled else 0.25, - ) - or_text = ( - bui.Lstr(resource='orText', subs=[('${A}', ''), ('${B}', '')]) - .evaluate() - .strip() - ) - bui.textwidget(edit=self.entry_fee_text_or, text=or_text) - bui.textwidget( - edit=self.entry_fee_text_top, - position=( - self.button_x + 360, - self.button_y + self.button_scale_y - 60, - ), - scale=1.3, - text=final_fee_str, - ) - - # Possibly show number of ad-plays remaining. - bui.textwidget( - edit=self.entry_fee_text_remaining, - position=( - self.button_x + 360, - self.button_y + self.button_scale_y - 146, - ), - text=( - '' - if ad_tries_remaining in [None, 0] - else ('' + str(ad_tries_remaining)) - ), - color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2), - ) - else: - bui.imagewidget(edit=self.entry_fee_ad_image, opacity=0.0) - bui.textwidget(edit=self.entry_fee_text_or, text='') - bui.textwidget( - edit=self.entry_fee_text_top, - position=( - self.button_x + 360, - self.button_y + self.button_scale_y - 80, - ), - scale=1.3, - text=final_fee_str, - ) - - # Possibly show number of free-plays remaining. - bui.textwidget( - edit=self.entry_fee_text_remaining, - position=( - self.button_x + 360, - self.button_y + self.button_scale_y - 100, - ), - text=( - '' - if (free_tries_remaining in [None, 0] or final_fee != 0) - else ('' + str(free_tries_remaining)) - ), - color=(0.6, 0.6, 0.6, 1), - ) + if USE_ENTRY_FEES: + if allow_ads and plus.has_video_ads(): + ads_enabled = plus.have_incentivized_ad() + bui.imagewidget( + edit=self.entry_fee_ad_image, + opacity=1.0 if ads_enabled else 0.25, + ) + or_text = ( + bui.Lstr( + resource='orText', subs=[('${A}', ''), ('${B}', '')] + ) + .evaluate() + .strip() + ) + bui.textwidget(edit=self.entry_fee_text_or, text=or_text) + bui.textwidget( + edit=self.entry_fee_text_top, + position=( + self.button_x + 360, + self.button_y + self.button_scale_y - 60, + ), + scale=1.3, + text=final_fee_str, + ) + + # Possibly show number of ad-plays remaining. + bui.textwidget( + edit=self.entry_fee_text_remaining, + position=( + self.button_x + 360, + self.button_y + self.button_scale_y - 146, + ), + text=( + '' + if ad_tries_remaining in [None, 0] + else ('' + str(ad_tries_remaining)) + ), + color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2), + ) + else: + bui.imagewidget(edit=self.entry_fee_ad_image, opacity=0.0) + bui.textwidget(edit=self.entry_fee_text_or, text='') + bui.textwidget( + edit=self.entry_fee_text_top, + position=( + self.button_x + 360, + self.button_y + self.button_scale_y - 80, + ), + scale=1.3, + text=final_fee_str, + ) + + # Possibly show number of free-plays remaining. + bui.textwidget( + edit=self.entry_fee_text_remaining, + position=( + self.button_x + 360, + self.button_y + self.button_scale_y - 100, + ), + text=( + '' + if (free_tries_remaining in [None, 0] or final_fee != 0) + else ('' + str(free_tries_remaining)) + ), + color=(0.6, 0.6, 0.6, 1), + ) diff --git a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py index dc185f2d6..a2744290c 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py @@ -62,7 +62,7 @@ def __init__(self, window: GatherWindow) -> None: self._state: State = State() self._last_datacode_refresh_time: float | None = None self._hostingstate = PrivateHostingState() - self._v2state: bacommon.cloud.BSPrivatePartyResponse | None = None + self._v2state: bacommon.bs.PrivatePartyResponse | None = None self._join_sub_tab_text: bui.Widget | None = None self._host_sub_tab_text: bui.Widget | None = None self._update_timer: bui.AppTimer | None = None @@ -339,7 +339,7 @@ def _update(self) -> None: if plus.accounts.primary is not None: with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.cloud.BSPrivatePartyMessage( + bacommon.bs.PrivatePartyMessage( need_datacode=( self._last_datacode_refresh_time is None or time.monotonic() @@ -355,7 +355,7 @@ def _update(self) -> None: self._last_v2_state_query_time = now def _on_private_party_query_response( - self, response: bacommon.cloud.BSPrivatePartyResponse | Exception + self, response: bacommon.bs.PrivatePartyResponse | Exception ) -> None: if isinstance(response, Exception): self._debug_server_comm('got pp v2 state response (err)') diff --git a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py index eb6804caa..25d666ff5 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py @@ -367,6 +367,7 @@ def __init__(self, window: GatherWindow) -> None: self._last_server_list_query_time: float | None = None self._join_list_column: bui.Widget | None = None self._join_status_text: bui.Widget | None = None + self._join_status_spinner: bui.Widget | None = None self._no_servers_found_text: bui.Widget | None = None self._host_max_party_size_value: bui.Widget | None = None self._host_max_party_size_minus_button: bui.Widget | None = None @@ -665,6 +666,9 @@ def _build_join_tab( size=(400, 400), claims_left_right=True, ) + + # Create join status text and join spinner. Always make sure to + # update both of these together. self._join_status_text = bui.textwidget( parent=self._container, text='', @@ -678,6 +682,10 @@ def _build_join_tab( color=(0.6, 0.6, 0.6), position=(c_width * 0.5, c_height * 0.5), ) + self._join_status_spinner = bui.spinnerwidget( + parent=self._container, position=(c_width * 0.5, c_height * 0.5) + ) + self._no_servers_found_text = bui.textwidget( parent=self._container, text='', @@ -944,37 +952,51 @@ def _update(self) -> None: name = cast(str, bui.textwidget(query=self._host_name_text)) bs.set_public_party_name(name) - # Update status text. - status_text = self._join_status_text - if status_text: + # Update status text and loading spinner. + if self._join_status_text: + assert self._join_status_spinner if not signed_in: bui.textwidget( - edit=status_text, text=bui.Lstr(resource='notSignedInText') + edit=self._join_status_text, + text=bui.Lstr(resource='notSignedInText'), ) + bui.spinnerwidget(edit=self._join_status_spinner, visible=False) else: # If we have a valid list, show no status; just the list. # Otherwise show either 'loading...' or 'error' depending # on whether this is our first go-round. if self._have_valid_server_list: - bui.textwidget(edit=status_text, text='') + bui.textwidget(edit=self._join_status_text, text='') + bui.spinnerwidget( + edit=self._join_status_spinner, visible=False + ) else: if self._have_server_list_response: bui.textwidget( - edit=status_text, + edit=self._join_status_text, text=bui.Lstr(resource='errorText'), ) + bui.spinnerwidget( + edit=self._join_status_spinner, visible=False + ) else: - bui.textwidget( - edit=status_text, - text=bui.Lstr( - value='${A}...', - subs=[ - ( - '${A}', - bui.Lstr(resource='store.loadingText'), - ) - ], - ), + # Show our loading spinner. + bui.textwidget(edit=self._join_status_text, text='') + # bui.textwidget( + # edit=self._join_status_text, + # text=bui.Lstr( + # value='${A}...', + # subs=[ + # ( + # '${A}', + # + # bui.Lstr(resource='store.loadingText'), + # ) + # ], + # ), + # ) + bui.spinnerwidget( + edit=self._join_status_spinner, visible=True ) self._update_party_rows() @@ -1005,16 +1027,11 @@ def _update_party_rows(self) -> None: self._ui_rows = self._ui_rows[:-clipcount] # If we have no parties to show, we're done. - if not self._parties_displayed: - text = self._join_status_text - if ( - plus.get_v1_account_state() == 'signed_in' - and cast(str, bui.textwidget(query=text)) == '' - ): - bui.textwidget( - edit=self._no_servers_found_text, - text=bui.Lstr(resource='noServersFoundText'), - ) + if self._have_valid_server_list and not self._parties_displayed: + bui.textwidget( + edit=self._no_servers_found_text, + text=bui.Lstr(resource='noServersFoundText'), + ) return sub_scroll_width = 830 diff --git a/src/assets/ba_data/python/bauiv1lib/gettokens.py b/src/assets/ba_data/python/bauiv1lib/gettokens.py index c923ae182..a29971c71 100644 --- a/src/assets/ba_data/python/bauiv1lib/gettokens.py +++ b/src/assets/ba_data/python/bauiv1lib/gettokens.py @@ -863,7 +863,7 @@ def show_get_tokens_prompt() -> None: if bool(True): ConfirmWindow( bui.Lstr(resource='tokens.notEnoughTokensText'), - GetTokensWindow, + _show_get_tokens, ok_text=bui.Lstr(resource='tokens.getTokensText'), width=460, height=130, @@ -875,3 +875,30 @@ def show_get_tokens_prompt() -> None: width=460, height=130, ) + + +def _show_get_tokens() -> None: + + # NOTE TO USERS: The code below is not the proper way to do things; + # whenever possible one should use a MainWindow's + # main_window_replace() or main_window_back() methods. We just need + # to do things a bit more manually in this case. + + prev_main_window = bui.app.ui_v1.get_main_window() + + # Special-case: If it seems we're already in the account window, do + # nothing. + if isinstance(prev_main_window, GetTokensWindow): + return + + # Set our new main window. + bui.app.ui_v1.set_main_window( + GetTokensWindow(), + from_window=False, + is_auxiliary=True, + suppress_warning=True, + ) + + # Transition out any previous main window. + if prev_main_window is not None: + prev_main_window.main_window_close() diff --git a/src/assets/ba_data/python/bauiv1lib/inbox.py b/src/assets/ba_data/python/bauiv1lib/inbox.py index 6996bb299..2783208f9 100644 --- a/src/assets/ba_data/python/bauiv1lib/inbox.py +++ b/src/assets/ba_data/python/bauiv1lib/inbox.py @@ -6,30 +6,100 @@ import weakref from dataclasses import dataclass -from typing import override +from typing import override, assert_never from efro.error import CommunicationError -import bacommon.cloud +import bacommon.bs import bauiv1 as bui -# Messages with format versions higher than this will show up as -# 'app needs to be updated to view this' -SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION = 1 + +class _Section: + def get_height(self) -> float: + """Return section height.""" + raise NotImplementedError() + + def draw(self, subcontainer: bui.Widget, y: float) -> None: + """Draw the section.""" + + +class _TextSection(_Section): + + def __init__( + self, + sub_width: float, + text: str, + *, + subs: list[str], + spacing_top: float = 0.0, + spacing_bottom: float = 0.0, + scale: float = 0.6, + color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0), + ) -> None: + self.sub_width = sub_width + self.spacing_top = spacing_top + self.spacing_bottom = spacing_bottom + self.color = color + + self.textfin = bui.Lstr(translate=('serverResponses', text)).evaluate() + assert len(subs) % 2 == 0 # Should always be even. + for j in range(0, len(subs) - 1, 2): + self.textfin = self.textfin.replace(subs[j], subs[j + 1]) + + # Calc scale to fit width and then see what height we need at + # that scale. + t_width = max( + 10.0, + bui.get_string_width(self.textfin, suppress_warning=True) * scale, + ) + self.text_scale = scale * min(1.0, (sub_width * 0.9) / t_width) + + self.text_height = ( + 0.0 + if not self.textfin + else bui.get_string_height(self.textfin, suppress_warning=True) + ) * self.text_scale + + self.full_height = self.text_height + spacing_top + spacing_bottom + + @override + def get_height(self) -> float: + return self.full_height + + @override + def draw(self, subcontainer: bui.Widget, y: float) -> None: + bui.textwidget( + parent=subcontainer, + position=( + self.sub_width * 0.5, + y - self.spacing_top - self.text_height * 0.5, + # y - self.height * 0.5 - 23.0, + ), + color=self.color, + scale=self.text_scale, + flatness=1.0, + shadow=0.0, + text=self.textfin, + size=(0, 0), + h_align='center', + v_align='center', + ) @dataclass -class _MessageEntry: - type: bacommon.cloud.BSInboxEntryType +class _EntryDisplay: + interaction_style: bacommon.bs.BasicClientUI.InteractionStyle + button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel + button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel + sections: list[_Section] id: str - height: float - text_height: float - scale: float - text: str + total_height: float color: tuple[float, float, float] backing: bui.Widget | None = None button_positive: bui.Widget | None = None + button_spinner_positive: bui.Widget | None = None button_negative: bui.Widget | None = None - message_text: bui.Widget | None = None + button_spinner_negative: bui.Widget | None = None + # message_text: bui.Widget | None = None processing_complete: bool = False @@ -45,15 +115,15 @@ def __init__( assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - self._message_entries: list[_MessageEntry] = [] + self._entry_displays: list[_EntryDisplay] = [] - self._width = 600 if uiscale is bui.UIScale.SMALL else 450 + self._width = 800 if uiscale is bui.UIScale.SMALL else 500 self._height = ( - 375 + 455 if uiscale is bui.UIScale.SMALL else 370 if uiscale is bui.UIScale.MEDIUM else 450 ) - yoffs = -47 if uiscale is bui.UIScale.SMALL else 0 + yoffs = -42 if uiscale is bui.UIScale.SMALL else 0 super().__init__( root_widget=bui.containerwidget( @@ -62,9 +132,9 @@ def __init__( 'menu_full' if uiscale is bui.UIScale.SMALL else 'menu_full' ), scale=( - 2.3 + 1.7 if uiscale is bui.UIScale.SMALL - else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 + else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.15 ), stack_offset=( (0, 0) @@ -101,7 +171,7 @@ def __init__( position=( self._width * 0.5, self._height - - (27 if uiscale is bui.UIScale.SMALL else 20) + - (24 if uiscale is bui.UIScale.SMALL else 20) + yoffs, ), size=(0, 0), @@ -122,11 +192,15 @@ def __init__( flatness=1.0, color=(0.4, 0.4, 0.5), shadow=0.0, - text=bui.Lstr(resource='loadingText'), + text='', size=(0, 0), h_align='center', v_align='center', ) + self._loading_spinner = bui.spinnerwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + ) self._scrollwidget = bui.scrollwidget( parent=self._root_widget, size=( @@ -141,6 +215,7 @@ def __init__( simple_culling_v=200, claims_left_right=True, claims_up_down=True, + center_small_content_horizontally=True, ) bui.widget(edit=self._scrollwidget, autoselect=True) if uiscale is bui.UIScale.SMALL: @@ -163,7 +238,7 @@ def __init__( with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.cloud.BSInboxRequestMessage(), + bacommon.bs.InboxRequestMessage(), on_response=bui.WeakCall(self._on_inbox_request_response), ) @@ -179,26 +254,33 @@ def get_main_window_state(self) -> bui.MainWindowState: def _error(self, errmsg: bui.Lstr | str) -> None: """Put ourself in a permanent error state.""" + bui.spinnerwidget(edit=self._loading_spinner, visible=False) bui.textwidget( edit=self._infotext, color=(1, 0, 0), text=errmsg, ) - def _on_message_entry_press( + def _on_entry_display_press( self, - entry_weak: weakref.ReferenceType[_MessageEntry], - process_type: bacommon.cloud.BSInboxEntryProcessType, + display_weak: weakref.ReferenceType[_EntryDisplay], + action: bacommon.bs.ClientUIAction, ) -> None: - entry = entry_weak() - if entry is None: + display = display_weak() + if display is None: return - self._neuter_message_entry(entry) + bui.getsound('click01').play() - # We don't do anything for invalid messages. - if entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN: - entry.processing_complete = True + self._neuter_entry_display(display) + + # We currently only recognize basic entries and their possible + # interaction types. + if ( + display.interaction_style + is bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN + ): + display.processing_complete = True self._close_soon_if_all_processed() return @@ -211,38 +293,43 @@ def _on_message_entry_press( bui.getsound('error').play() return - # Message the master-server to process the entry. + # Ask the master-server to run our action. with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.cloud.BSInboxEntryProcessMessage( - entry.id, process_type - ), + bacommon.bs.ClientUIActionMessage(display.id, action), on_response=bui.WeakCall( - self._on_inbox_entry_process_response, - entry_weak, - process_type, + self._on_client_ui_action_response, + display_weak, + action, ), ) - # Tweak the button to show this is in progress. + # Tweak the UI to show that things are in motion. button = ( - entry.button_positive - if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE - else entry.button_negative + display.button_positive + if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE + else display.button_negative + ) + button_spinner = ( + display.button_spinner_positive + if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE + else display.button_spinner_negative ) if button is not None: - bui.buttonwidget(edit=button, label='...') + bui.buttonwidget(edit=button, label='') + if button_spinner is not None: + bui.spinnerwidget(edit=button_spinner, visible=True) def _close_soon_if_all_processed(self) -> None: bui.apptimer(0.25, bui.WeakCall(self._close_if_all_processed)) def _close_if_all_processed(self) -> None: - if not all(m.processing_complete for m in self._message_entries): + if not all(m.processing_complete for m in self._entry_displays): return self.main_window_back() - def _neuter_message_entry(self, entry: _MessageEntry) -> None: + def _neuter_entry_display(self, entry: _EntryDisplay) -> None: errsound = bui.getsound('error') if entry.button_positive is not None: bui.buttonwidget( @@ -260,22 +347,20 @@ def _neuter_message_entry(self, entry: _MessageEntry) -> None: ) if entry.backing is not None: bui.imagewidget(edit=entry.backing, color=(0.4, 0.4, 0.4)) - if entry.message_text is not None: - bui.textwidget(edit=entry.message_text, color=(0.5, 0.5, 0.5, 0.5)) - def _on_inbox_entry_process_response( + def _on_client_ui_action_response( self, - entry_weak: weakref.ReferenceType[_MessageEntry], - process_type: bacommon.cloud.BSInboxEntryProcessType, - response: bacommon.cloud.BSInboxEntryProcessResponse | Exception, + display_weak: weakref.ReferenceType[_EntryDisplay], + action: bacommon.bs.ClientUIAction, + response: bacommon.bs.ClientUIActionResponse | Exception, ) -> None: # pylint: disable=too-many-branches - entry = entry_weak() - if entry is None: + display = display_weak() + if display is None: return - assert not entry.processing_complete - entry.processing_complete = True + assert not display.processing_complete + display.processing_complete = True self._close_soon_if_all_processed() # No-op if our UI is dead or on its way out. @@ -284,10 +369,18 @@ def _on_inbox_entry_process_response( # Tweak the button to show results. button = ( - entry.button_positive - if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE - else entry.button_negative + display.button_positive + if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE + else display.button_negative ) + button_spinner = ( + display.button_spinner_positive + if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE + else display.button_spinner_negative + ) + # Always hide spinner at this point. + if button_spinner is not None: + bui.spinnerwidget(edit=button_spinner, visible=False) # See if we should show an error message. if isinstance(response, Exception): @@ -297,9 +390,11 @@ def _on_inbox_entry_process_response( ) else: error_message = bui.Lstr(resource='errorText') - elif response.error is not None: + elif response.error_type is not None: + # If error_type is set, error should be also. + assert response.error_message is not None error_message = bui.Lstr( - translate=('serverResponses', response.error) + translate=('serverResponses', response.error_message) ) else: error_message = None @@ -314,6 +409,13 @@ def _on_inbox_entry_process_response( ) return + # Success! + assert not isinstance(response, Exception) + + # Run any bundled effects. + assert bui.app.classic is not None + bui.app.classic.run_bs_client_effects(response.effects) + # Whee; no error. Mark as done. if button is not None: # If we have full unicode, just show a checkmark in all cases. @@ -321,24 +423,11 @@ def _on_inbox_entry_process_response( if bui.supports_unicode_display(): label = '✓' else: - # For positive claim buttons, say 'success'. - # Otherwise default to 'done.' - if ( - entry.type - in { - bacommon.cloud.BSInboxEntryType.CLAIM, - bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD, - } - and process_type - is bacommon.cloud.BSInboxEntryProcessType.POSITIVE - ): - label = bui.Lstr(resource='successText') - else: - label = bui.Lstr(resource='doneText') + label = bui.Lstr(resource='doneText') bui.buttonwidget(edit=button, label=label) def _on_inbox_request_response( - self, response: bacommon.cloud.BSInboxRequestResponse | Exception + self, response: bacommon.bs.InboxRequestResponse | Exception ) -> None: # pylint: disable=too-many-locals # pylint: disable=too-many-statements @@ -364,11 +453,12 @@ def _on_inbox_request_response( self._error(errmsg) return - assert isinstance(response, bacommon.cloud.BSInboxRequestResponse) + assert isinstance(response, bacommon.bs.InboxRequestResponse) # If we got no messages, don't touch anything. This keeps # keyboard control working in the empty case. - if not response.entries: + if not response.wrappers: + bui.spinnerwidget(edit=self._loading_spinner, visible=False) bui.textwidget( edit=self._infotext, color=(0.4, 0.4, 0.5), @@ -376,63 +466,96 @@ def _on_inbox_request_response( ) return + bui.spinnerwidget(edit=self._loading_spinner, visible=False) bui.textwidget(edit=self._infotext, text='') - sub_width = self._width - 90 + # Even though our window size varies with uiscale, we want + # notifications to target a fixed width. + sub_width = 400.0 sub_height = 0.0 - # Run the math on row heights/etc. - for i, entry in enumerate(response.entries): + # Construct entries for everything we'll display. + for i, wrapper in enumerate(response.wrappers): + # We need to flatten text here so we can measure it. - textfin: str + # textfin: str color: tuple[float, float, float] - # Messages with either newer formatting or unrecognized - # types show up as 'upgrade your app to see this'. + interaction_style: bacommon.bs.BasicClientUI.InteractionStyle + button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel + button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel + + sections: list[_Section] = [] + total_height = 90.0 + + # Display only entries where we recognize all style/label + # values and ui component types. if ( - entry.format_version > SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION - or entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN + isinstance(wrapper.ui, bacommon.bs.BasicClientUI) + and not wrapper.ui.contains_unknown_elements() ): - textfin = bui.Lstr( - translate=( - 'serverResponses', - 'You must update the app to view this.', - ) - ).evaluate() - color = (0.6, 0.6, 0.6) - else: - # Translate raw response and apply any replacements. - textfin = bui.Lstr( - translate=('serverResponses', entry.message) - ).evaluate() - assert len(entry.subs) % 2 == 0 # Should always be even. - for j in range(0, len(entry.subs) - 1, 2): - textfin = textfin.replace(entry.subs[j], entry.subs[j + 1]) color = (0.55, 0.5, 0.7) + interaction_style = wrapper.ui.interaction_style + button_label_positive = wrapper.ui.button_label_positive + button_label_negative = wrapper.ui.button_label_negative + + idcls = bacommon.bs.BasicClientUIComponentTypeID + for component in wrapper.ui.components: + ctypeid = component.get_type_id() + if ctypeid is idcls.TEXT: + assert isinstance( + component, bacommon.bs.BasicClientUIComponentText + ) + section = _TextSection( + sub_width=sub_width, + text=component.text, + subs=component.subs, + color=component.color, + scale=component.scale, + spacing_top=component.spacing_top, + spacing_bottom=component.spacing_bottom, + ) + total_height += section.get_height() + sections.append(section) + + elif ctypeid is idcls.UNKNOWN: + raise RuntimeError('Should not get here.') + else: + # Make sure we handle all types. + assert_never(ctypeid) + else: - # Calc scale to fit width and then see what height we need - # at that scale. - t_width = max( - 10.0, bui.get_string_width(textfin, suppress_warning=True) - ) - scale = min(0.6, (sub_width * 0.9) / t_width) - t_height = ( - max(10.0, bui.get_string_height(textfin, suppress_warning=True)) - * scale - ) - entry_height = 90.0 + t_height - self._message_entries.append( - _MessageEntry( - type=entry.type, - id=entry.id, - height=entry_height, - text_height=t_height, - scale=scale, - text=textfin, + # Display anything with unknown components as an + # 'upgrade your app to see this' message. + color = (0.6, 0.6, 0.6) + interaction_style = ( + bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN + ) + button_label_positive = bacommon.bs.BasicClientUI.ButtonLabel.OK + button_label_negative = ( + bacommon.bs.BasicClientUI.ButtonLabel.CANCEL + ) + + section = _TextSection( + sub_width=sub_width, + text='You must update the app to view this.', + subs=[], + ) + total_height += section.get_height() + sections.append(section) + + self._entry_displays.append( + _EntryDisplay( + interaction_style=interaction_style, + button_label_positive=button_label_positive, + button_label_negative=button_label_negative, + id=wrapper.id, + sections=sections, + total_height=total_height, color=color, ) ) - sub_height += entry_height + sub_height += total_height subcontainer = bui.containerwidget( id='inboxsub', @@ -446,98 +569,116 @@ def _on_inbox_request_response( backing_tex = bui.gettexture('buttonSquareWide') + assert bui.app.classic is not None + buttonrows: list[list[bui.Widget]] = [] y = sub_height - for i, _entry in enumerate(response.entries): - message_entry = self._message_entries[i] - message_entry_weak = weakref.ref(message_entry) + for i, _wrapper in enumerate(response.wrappers): + entry_display = self._entry_displays[i] + entry_display_weak = weakref.ref(entry_display) bwidth = 140 bheight = 40 + ysection = y - 23.0 + # Backing. - message_entry.backing = img = bui.imagewidget( + entry_display.backing = img = bui.imagewidget( parent=subcontainer, - position=(-0.022 * sub_width, y - message_entry.height * 1.09), + position=( + -0.022 * sub_width, + y - entry_display.total_height * 1.09, + ), texture=backing_tex, - size=(sub_width * 1.07, message_entry.height * 1.15), - color=message_entry.color, + size=(sub_width * 1.07, entry_display.total_height * 1.15), + color=entry_display.color, opacity=0.9, ) bui.widget(edit=img, depth_range=(0, 0.1)) + # Section contents. + for sec in entry_display.sections: + sec.draw(subcontainer, ysection) + ysection -= sec.get_height() + buttonrow: list[bui.Widget] = [] have_negative_button = ( - message_entry.type - is bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD + entry_display.interaction_style + is ( + bacommon.bs.BasicClientUI + ).InteractionStyle.BUTTON_POSITIVE_NEGATIVE ) - message_entry.button_positive = btn = bui.buttonwidget( - parent=subcontainer, - position=( - ( - (sub_width - bwidth - 25) - if have_negative_button - else ((sub_width - bwidth) * 0.5) - ), - y - message_entry.height + 15.0, + bpos = ( + ( + (sub_width - bwidth - 25) + if have_negative_button + else ((sub_width - bwidth) * 0.5) ), + y - entry_display.total_height + 15.0, + ) + entry_display.button_positive = btn = bui.buttonwidget( + parent=subcontainer, + position=bpos, size=(bwidth, bheight), - label=bui.Lstr( - resource=( - 'claimText' - if message_entry.type - in { - bacommon.cloud.BSInboxEntryType.CLAIM, - bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD, - } - else 'okText' - ) + label=bui.app.classic.basic_client_ui_button_label_str( + entry_display.button_label_positive ), - color=message_entry.color, + color=entry_display.color, textcolor=(0, 1, 0), on_activate_call=bui.WeakCall( - self._on_message_entry_press, - message_entry_weak, - bacommon.cloud.BSInboxEntryProcessType.POSITIVE, + self._on_entry_display_press, + entry_display_weak, + bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE, ), + enable_sound=False, ) bui.widget(edit=btn, depth_range=(0.1, 1.0)) buttonrow.append(btn) + spinner = entry_display.button_spinner_positive = bui.spinnerwidget( + parent=subcontainer, + position=( + bpos[0] + 0.5 * bwidth, + bpos[1] + 0.5 * bheight, + ), + visible=False, + ) + bui.widget(edit=spinner, depth_range=(0.1, 1.0)) if have_negative_button: - message_entry.button_negative = btn2 = bui.buttonwidget( + bpos = (25, y - entry_display.total_height + 15.0) + entry_display.button_negative = btn2 = bui.buttonwidget( parent=subcontainer, - position=(25, y - message_entry.height + 15.0), + position=bpos, size=(bwidth, bheight), - label=bui.Lstr(resource='discardText'), + label=bui.app.classic.basic_client_ui_button_label_str( + entry_display.button_label_negative + ), color=(0.85, 0.5, 0.7), textcolor=(1, 0.4, 0.4), on_activate_call=bui.WeakCall( - self._on_message_entry_press, - message_entry_weak, - bacommon.cloud.BSInboxEntryProcessType.NEGATIVE, + self._on_entry_display_press, + entry_display_weak, + (bacommon.bs.ClientUIAction).BUTTON_PRESS_NEGATIVE, ), + enable_sound=False, ) bui.widget(edit=btn2, depth_range=(0.1, 1.0)) buttonrow.append(btn2) + spinner = entry_display.button_spinner_negative = ( + bui.spinnerwidget( + parent=subcontainer, + position=( + bpos[0] + 0.5 * bwidth, + bpos[1] + 0.5 * bheight, + ), + visible=False, + ) + ) + bui.widget(edit=spinner, depth_range=(0.1, 1.0)) buttonrows.append(buttonrow) - message_entry.message_text = bui.textwidget( - parent=subcontainer, - position=( - sub_width * 0.5, - y - message_entry.text_height * 0.5 - 23.0, - ), - scale=message_entry.scale, - flatness=1.0, - shadow=0.0, - text=message_entry.text, - size=(0, 0), - h_align='center', - v_align='center', - ) - y -= message_entry.height + y -= entry_display.total_height uiscale = bui.app.ui_v1.uiscale above_widget = ( diff --git a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py index 0666147f7..cfa21aef7 100644 --- a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py +++ b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py @@ -39,6 +39,7 @@ def __init__( self._league_text: bui.Widget | None = None self._league_number_text: bui.Widget | None = None self._your_power_ranking_text: bui.Widget | None = None + self._loading_spinner: bui.Widget | None = None self._season_ends_text: bui.Widget | None = None self._power_ranking_rank_text: bui.Widget | None = None self._to_ranked_text: bui.Widget | None = None @@ -150,7 +151,7 @@ def __init__( self._doing_power_ranking_query = False self._subcontainer: bui.Widget | None = None - self._subcontainerwidth = 800 + self._subcontainerwidth = max(800, self._scroll_width) self._subcontainerheight = 483 self._power_ranking_score_widgets: list[bui.Widget] = [] @@ -330,13 +331,8 @@ def _update(self, show: bool = False) -> None: bui.textwidget(edit=self._league_title_text, text='') bui.textwidget(edit=self._league_text, text='') bui.textwidget(edit=self._league_number_text, text='') - bui.textwidget( - edit=self._your_power_ranking_text, - text=bui.Lstr( - value='${A}...', - subs=[('${A}', bui.Lstr(resource='loadingText'))], - ), - ) + bui.textwidget(edit=self._your_power_ranking_text, text='') + bui.spinnerwidget(edit=self._loading_spinner, visible=True) bui.textwidget(edit=self._to_ranked_text, text='') bui.textwidget(edit=self._power_ranking_rank_text, text='') bui.textwidget(edit=self._season_ends_text, text='') @@ -618,6 +614,14 @@ def _refresh(self) -> None: flatness=1.0, ) + self._loading_spinner = bui.spinnerwidget( + parent=w_parent, + position=( + self._subcontainerwidth * 0.5, + self._subcontainerheight * 0.5, + ), + size=64, + ) self._your_power_ranking_text = bui.textwidget( parent=w_parent, position=(self._xoffs + 470, v - 142 - 70), @@ -968,6 +972,7 @@ def _update_for_league_rank_data(self, data: dict[str, Any] | None) -> None: else '' ), ) + bui.spinnerwidget(edit=self._loading_spinner, visible=False) bui.textwidget( edit=self._power_ranking_rank_text, diff --git a/src/assets/ba_data/python/bauiv1lib/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py index 8495903ac..0f6152d54 100644 --- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py +++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py @@ -431,7 +431,6 @@ def _refresh(self) -> None: ) # Credits button. - # self._tdelay += self._t_delay_inc thistdelay = self._tdelay + td5 * self._t_delay_inc h += side_button_width * side_button_scale * 0.5 + hspace2 @@ -454,15 +453,16 @@ def _refresh(self) -> None: transition_delay=thistdelay, on_activate_call=self._credits, ) - # self._tdelay += self._t_delay_inc self._quit_button: bui.Widget | None if self._have_quit_button: v -= 1.1 * side_button_2_height * side_button_2_scale + # Nudge this a tiny bit right so we can press right from the + # credits button to get to it. self._quit_button = quit_button = bui.buttonwidget( parent=self._root_widget, autoselect=self._use_autoselect, - position=(h, v), + position=(h + 4.0, v), size=(side_button_2_width, side_button_2_height), scale=side_button_2_scale, label=bui.Lstr( diff --git a/src/assets/ba_data/python/bauiv1lib/partyqueue.py b/src/assets/ba_data/python/bauiv1lib/partyqueue.py index 3c4d10be7..dbcfd7adb 100644 --- a/src/assets/ba_data/python/bauiv1lib/partyqueue.py +++ b/src/assets/ba_data/python/bauiv1lib/partyqueue.py @@ -579,6 +579,10 @@ def on_boost_press(self) -> None: if plus.get_v1_account_ticket_count() < self._boost_tickets: bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='notEnoughTicketsText'), + color=(1, 0, 0), + ) # gettickets.show_get_tickets_prompt() return diff --git a/src/assets/ba_data/python/bauiv1lib/play.py b/src/assets/ba_data/python/bauiv1lib/play.py index 27b05a407..961113f38 100644 --- a/src/assets/ba_data/python/bauiv1lib/play.py +++ b/src/assets/ba_data/python/bauiv1lib/play.py @@ -42,9 +42,9 @@ def __init__( self._playlist_select_context = playlist_select_context uiscale = bui.app.ui_v1.uiscale - width = 1100 if uiscale is bui.UIScale.SMALL else 800 - x_offs = 150 if uiscale is bui.UIScale.SMALL else 0 - y_offs = -60 if uiscale is bui.UIScale.SMALL else 0 + width = 1100 if uiscale is bui.UIScale.SMALL else 1000 + x_offs = 150 if uiscale is bui.UIScale.SMALL else 90 + y_offs = -60 if uiscale is bui.UIScale.SMALL else 45 height = 650 if uiscale is bui.UIScale.SMALL else 550 button_width = 400 @@ -89,7 +89,7 @@ def __init__( else: self._back_button = bui.buttonwidget( parent=self._root_widget, - position=(55 + x_offs, height - 132 + y_offs), + position=(5 + x_offs, height - 162 + y_offs), size=(60, 60), scale=1.1, text_res_scale=1.5, @@ -103,11 +103,12 @@ def __init__( edit=self._root_widget, cancel_button=self._back_button ) - txt = bui.textwidget( + bui.textwidget( parent=self._root_widget, - position=(width * 0.5, height - 101 + y_offs), - # position=(width * 0.5, height - - # (101 if main_menu else 61)), + position=( + width * 0.5, + height - (83 if uiscale is bui.UIScale.SMALL else 131) + y_offs, + ), size=(0, 0), text=bui.Lstr( resource=( @@ -116,17 +117,13 @@ def __init__( else 'playlistsText' ) ), - scale=1.7, + scale=1.2 if uiscale is bui.UIScale.SMALL else 1.7, res_scale=2.0, maxwidth=400, color=bui.app.ui_v1.heading_color, h_align='center', v_align='center', ) - - if uiscale is bui.UIScale.SMALL: - bui.textwidget(edit=txt, text='') - v = ( height - (110 if self._playlist_select_context is None else 90) @@ -134,14 +131,14 @@ def __init__( ) v -= 100 clr = (0.6, 0.7, 0.6, 1.0) - v -= 280 if self._playlist_select_context is None else 180 - v += 30 if uiscale is bui.UIScale.SMALL else 0 + v -= 270 if self._playlist_select_context is None else 280 + v += 65 if uiscale is bui.UIScale.SMALL else 0 hoffs = ( - x_offs + 80 + x_offs - 45 if self._playlist_select_context is None else x_offs - 100 ) - scl = 1.13 if self._playlist_select_context is None else 0.68 + scl = 0.75 if self._playlist_select_context is None else 0.68 self._lineup_tex = bui.gettexture('playerLineup') angry_computer_transparent_mesh = bui.getmesh( @@ -167,16 +164,15 @@ def __init__( if self._playlist_select_context is None: self._coop_button = btn = bui.buttonwidget( parent=self._root_widget, - position=(hoffs, v + (scl * 15)), + position=(hoffs, v), size=( scl * button_width, - scl * 300, + scl * 360, ), extra_touch_border_scale=0.1, autoselect=True, label='', button_type='square', - text_scale=1.13, on_activate_call=self._coop, ) @@ -247,7 +243,7 @@ def __init__( h_align='center', v_align='center', color=(0.7, 0.9, 0.7, 1.0), - scale=scl * 2.3, + scale=scl * 1.5, ) bui.textwidget( @@ -264,34 +260,29 @@ def __init__( color=clr, ) - scl = 0.5 if self._playlist_select_context is None else 0.68 - hoffs += 440 if self._playlist_select_context is None else 216 - v += 180 if self._playlist_select_context is None else -68 + scl = 0.75 if self._playlist_select_context is None else 0.68 + hoffs += 300 if self._playlist_select_context is None else 216 + # v += 0 if self._playlist_select_context is None else -68 self._teams_button = btn = bui.buttonwidget( parent=self._root_widget, position=( hoffs, - v + (scl * 15 if self._playlist_select_context is None else 0), + v, + # v + (scl * 15 if + # self._playlist_select_context is None else 0), ), size=( scl * button_width, - scl * (300 if self._playlist_select_context is None else 360), + scl * (360 if self._playlist_select_context is None else 360), ), extra_touch_border_scale=0.1, autoselect=True, label='', button_type='square', - text_scale=1.13, on_activate_call=self._team_tourney, ) - bui.widget( - edit=btn, - up_widget=bui.get_special_widget('get_tokens_button'), - right_widget=bui.get_special_widget('squad_button'), - ) - xxx = -14 self._draw_dude( 2, @@ -381,7 +372,7 @@ def __init__( h_align='center', v_align='center', color=(0.7, 0.9, 0.7, 1.0), - scale=scl * 2.3, + scale=scl * 1.5, ) bui.textwidget( parent=self._root_widget, @@ -392,29 +383,30 @@ def __init__( h_align='center', v_align='center', res_scale=1.5, - scale=0.9 * scl, + scale=0.83 * scl, flatness=1.0, maxwidth=scl * button_width * 0.7, color=clr, ) - hoffs += 0 if self._playlist_select_context is None else 300 - v -= 155 if self._playlist_select_context is None else 0 + hoffs += 300 if self._playlist_select_context is None else 300 + # v -= 0 if self._playlist_select_context is None else 0 self._free_for_all_button = btn = bui.buttonwidget( parent=self._root_widget, position=( hoffs, - v + (scl * 15 if self._playlist_select_context is None else 0), + v, + # v + (scl * 15 + # if self._playlist_select_context is None else 0), ), size=( scl * button_width, - scl * (300 if self._playlist_select_context is None else 360), + scl * (360 if self._playlist_select_context is None else 360), ), extra_touch_border_scale=0.1, autoselect=True, label='', button_type='square', - text_scale=1.13, on_activate_call=self._free_for_all, ) @@ -505,7 +497,7 @@ def __init__( h_align='center', v_align='center', color=(0.7, 0.9, 0.7, 1.0), - scale=scl * 1.9, + scale=scl * 1.5, ) bui.textwidget( parent=self._root_widget, @@ -515,7 +507,7 @@ def __init__( text=bui.Lstr(resource=f'{self._r}.twoToEightPlayersText'), h_align='center', v_align='center', - scale=0.9 * scl, + scale=0.83 * scl, flatness=1.0, maxwidth=scl * button_width * 0.7, color=clr, diff --git a/src/assets/ba_data/python/bauiv1lib/playoptions.py b/src/assets/ba_data/python/bauiv1lib/playoptions.py index fc23723ea..0bf2028a1 100644 --- a/src/assets/ba_data/python/bauiv1lib/playoptions.py +++ b/src/assets/ba_data/python/bauiv1lib/playoptions.py @@ -17,6 +17,8 @@ from bauiv1lib.play import PlaylistSelectContext +REQUIRE_PRO = False + class PlayOptionsWindow(PopupWindow): """A popup window for configuring play options.""" @@ -316,7 +318,7 @@ def __init__( label=bui.Lstr(resource='teamNamesColorText'), ) 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, size=(30, 30), @@ -440,7 +442,7 @@ def _custom_colors_names_press(self) -> None: assert plus 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(): if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: diff --git a/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py b/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py index a9139a3cc..7fdc3a2a3 100644 --- a/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py +++ b/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py @@ -205,7 +205,10 @@ def _on_upgrade_press(self) -> None: tickets = plus.get_v1_account_ticket_count() if tickets < self._cost: bui.getsound('error').play() - print('FIXME - show not-enough-tickets msg.') + bui.screenmessage( + bui.Lstr(resource='notEnoughTicketsText'), + color=(1, 0, 0), + ) # gettickets.show_get_tickets_prompt() return bui.screenmessage( diff --git a/src/assets/ba_data/python/bauiv1lib/purchase.py b/src/assets/ba_data/python/bauiv1lib/purchase.py index 51094091d..9d20efeba 100644 --- a/src/assets/ba_data/python/bauiv1lib/purchase.py +++ b/src/assets/ba_data/python/bauiv1lib/purchase.py @@ -162,7 +162,6 @@ def _update(self) -> None: bui.containerwidget(edit=self._root_widget, transition='out_left') def _purchase(self) -> None: - # from bauiv1lib import gettickets plus = bui.app.plus assert plus is not None @@ -176,9 +175,12 @@ def _purchase(self) -> None: except Exception: ticket_count = None if ticket_count is not None and ticket_count < self._price: - # gettickets.show_get_tickets_prompt() - print('FIXME - show not-enough-tickets msg') bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='notEnoughTicketsText'), + color=(1, 0, 0), + ) + # gettickets.show_get_tickets_prompt() return def do_it() -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py b/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py index 139ff2402..b701805f4 100644 --- a/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py +++ b/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py @@ -53,24 +53,25 @@ def __init__( iconscale=1.2, ) - yoffs = self._height - 150 + yoffs = self._height - 145 if resource_type == 'tickets': rdesc = ( 'Tickets can be used to unlock characters,\n' 'maps, minigames, and more in the store.\n' '\n' - 'Earn tickets by completing achievements\n' - 'or by opening chests won in the game.' + 'Tickets can be found in chests won through\n' + 'campaigns, tournaments, and achievements.' ) texname = 'tickets' elif resource_type == 'tokens': rdesc = ( - 'Tokens have various uses in the game such as\n' - 'speeding up chest unlocks.\n' + 'Tokens are used to speed up chest unlocks\n' + 'and for other game and account features.\n' '\n' - 'You can buy packs of tokens or you can buy a\n' - 'Gold Pass to get unlimited tokens.\n' + 'You can win tokens in the game or buy them\n' + 'in packs. Or buy a Gold Pass to get infinite\n' + 'tokens forever and never hear of them again.' ) texname = 'coin' elif resource_type == 'trophies': diff --git a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py index badb77e5c..375502424 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py @@ -10,7 +10,7 @@ import bauiv1 as bui if TYPE_CHECKING: - pass + from typing import Callable class AllSettingsWindow(bui.MainWindow): @@ -21,7 +21,6 @@ def __init__( transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): - # pylint: disable=too-many-statements # pylint: disable=too-many-locals # Preload some modules we use in a background thread so we won't @@ -31,12 +30,12 @@ def __init__( bui.set_analytics_screen('Settings Window') assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - width = 1000 if uiscale is bui.UIScale.SMALL else 580 + width = 1000 if uiscale is bui.UIScale.SMALL else 900 x_inset = 125 if uiscale is bui.UIScale.SMALL else 0 - height = 500 if uiscale is bui.UIScale.SMALL else 435 + height = 500 if uiscale is bui.UIScale.SMALL else 450 self._r = 'settingsWindow' top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 - yoffs = -30 if uiscale is bui.UIScale.SMALL else 0 + yoffs = -30 if uiscale is bui.UIScale.SMALL else -30 uiscale = bui.app.ui_v1.uiscale super().__init__( @@ -50,10 +49,7 @@ def __init__( scale=( 1.5 if uiscale is bui.UIScale.SMALL - else 1.25 if uiscale is bui.UIScale.MEDIUM else 1.0 - ), - stack_offset=( - (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0) + else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.8 ), ), transition=transition, @@ -69,12 +65,12 @@ def __init__( self._back_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, - position=(40 + x_inset, height - 55 + yoffs), - size=(130, 60), + position=(40 + x_inset, height - 60 + yoffs), + size=(70, 70), scale=0.8, text_scale=1.2, - label=bui.Lstr(resource='backText'), - button_type='back', + label=bui.charstr(bui.SpecialChar.BACK), + button_type='backSmall', on_activate_call=self.main_window_back, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) @@ -87,131 +83,116 @@ def __init__( color=bui.app.ui_v1.title_color, h_align='center', v_align='center', + scale=1.1, maxwidth=130, ) - if self._back_button is not None: - bui.buttonwidget( - edit=self._back_button, - button_type='backSmall', - size=(60, 60), - label=bui.charstr(bui.SpecialChar.BACK), + bwidth = 200 + bheight = 230 + margin = 1 + all_buttons_width = 4.0 * bwidth + 3.0 * margin + + x = width * 0.5 - all_buttons_width * 0.5 + y = height + yoffs - 320.0 + + def _button( + position: tuple[float, float], + label: bui.Lstr, + call: Callable[[], None], + texture: bui.Texture, + imgsize: float, + *, + color: tuple[float, float, float] = (1.0, 1.0, 1.0), + imgoffs: tuple[float, float] = (0.0, 0.0), + ) -> bui.Widget: + x, y = position + btn = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(x, y), + size=(bwidth, bheight), + button_type='square', + label='', + on_activate_call=call, ) - - v = height - 80 + yoffs - v -= 145 - - basew = 280 if uiscale is bui.UIScale.SMALL else 230 - baseh = 170 - x_offs = ( - x_inset + (105 if uiscale is bui.UIScale.SMALL else 72) - basew - ) # now unused - x_offs2 = x_offs + basew - 7 - x_offs3 = x_offs + 2 * (basew - 7) - x_offs4 = x_offs2 - x_offs5 = x_offs3 - - def _b_title( - x: float, y: float, button: bui.Widget, text: str | bui.Lstr - ) -> None: bui.textwidget( parent=self._root_widget, - text=text, - position=(x + basew * 0.47, y + baseh * 0.22), - maxwidth=basew * 0.7, + text=label, + position=(x + bwidth * 0.5, y + bheight * 0.25), + maxwidth=bwidth * 0.7, size=(0, 0), h_align='center', v_align='center', - draw_controller=button, + draw_controller=btn, color=(0.7, 0.9, 0.7, 1.0), ) + bui.imagewidget( + parent=self._root_widget, + position=( + x + bwidth * 0.5 - imgsize * 0.5 + imgoffs[0], + y + bheight * 0.56 - imgsize * 0.5 + imgoffs[1], + ), + size=(imgsize, imgsize), + texture=texture, + draw_controller=btn, + color=color, + ) + return btn - ctb = self._controllers_button = bui.buttonwidget( - parent=self._root_widget, - autoselect=True, - position=(x_offs2, v), - size=(basew, baseh), - button_type='square', - label='', - on_activate_call=self._do_controllers, - ) - if self._back_button is None: - bbtn = bui.get_special_widget('back_button') - bui.widget(edit=ctb, left_widget=bbtn) - _b_title( - x_offs2, v, ctb, bui.Lstr(resource=f'{self._r}.controllersText') - ) - imgw = imgh = 130 - bui.imagewidget( - parent=self._root_widget, - position=(x_offs2 + basew * 0.49 - imgw * 0.5, v + 35), - size=(imgw, imgh), + self._controllers_button = _button( + position=(x, y), + label=bui.Lstr(resource=f'{self._r}.controllersText'), + call=self._do_controllers, texture=bui.gettexture('controllerIcon'), - draw_controller=ctb, + imgsize=150, + imgoffs=(-2.0, 2.0), ) + x += bwidth + margin - gfxb = self._graphics_button = bui.buttonwidget( - parent=self._root_widget, - autoselect=True, - position=(x_offs3, v), - size=(basew, baseh), - button_type='square', - label='', - on_activate_call=self._do_graphics, - ) - pbtn = bui.get_special_widget('squad_button') - bui.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn) - _b_title(x_offs3, v, gfxb, bui.Lstr(resource=f'{self._r}.graphicsText')) - imgw = imgh = 110 - bui.imagewidget( - parent=self._root_widget, - position=(x_offs3 + basew * 0.49 - imgw * 0.5, v + 42), - size=(imgw, imgh), + self._graphics_button = _button( + position=(x, y), + label=bui.Lstr(resource=f'{self._r}.graphicsText'), + call=self._do_graphics, texture=bui.gettexture('graphicsIcon'), - draw_controller=gfxb, + imgsize=135, + imgoffs=(0, 4.0), ) + x += bwidth + margin - v -= baseh - 5 - - abtn = self._audio_button = bui.buttonwidget( - parent=self._root_widget, - autoselect=True, - position=(x_offs4, v), - size=(basew, baseh), - button_type='square', - label='', - on_activate_call=self._do_audio, - ) - _b_title(x_offs4, v, abtn, bui.Lstr(resource=f'{self._r}.audioText')) - imgw = imgh = 120 - bui.imagewidget( - parent=self._root_widget, - position=(x_offs4 + basew * 0.49 - imgw * 0.5 + 5, v + 35), - size=(imgw, imgh), - color=(1, 1, 0), + self._audio_button = _button( + position=(x, y), + label=bui.Lstr(resource=f'{self._r}.audioText'), + call=self._do_audio, texture=bui.gettexture('audioIcon'), - draw_controller=abtn, + imgsize=150, + color=(1, 1, 0), ) + x += bwidth + margin - avb = self._advanced_button = bui.buttonwidget( - parent=self._root_widget, - autoselect=True, - position=(x_offs5, v), - size=(basew, baseh), - button_type='square', - label='', - on_activate_call=self._do_advanced, - ) - _b_title(x_offs5, v, avb, bui.Lstr(resource=f'{self._r}.advancedText')) - imgw = imgh = 120 - bui.imagewidget( - parent=self._root_widget, - position=(x_offs5 + basew * 0.49 - imgw * 0.5 + 5, v + 35), - size=(imgw, imgh), - color=(0.8, 0.95, 1), + self._advanced_button = _button( + position=(x, y), + label=bui.Lstr(resource=f'{self._r}.advancedText'), + call=self._do_advanced, texture=bui.gettexture('advancedIcon'), - draw_controller=avb, + imgsize=150, + color=(0.8, 0.95, 1), + imgoffs=(0, 5.0), ) + + # Hmm; we're now wide enough that being limited to pressing up + # might be ok. + if bool(False): + # Left from our leftmost button should go to back button. + if self._back_button is None: + bbtn = bui.get_special_widget('back_button') + bui.widget(edit=self._controllers_button, left_widget=bbtn) + + # Right from our rightmost widget should go to squad button. + bui.widget( + edit=self._advanced_button, + right_widget=bui.get_special_widget('squad_button'), + ) + self._restore_state() @override diff --git a/src/assets/ba_data/python/bauiv1lib/settings/audio.py b/src/assets/ba_data/python/bauiv1lib/settings/audio.py index 323bb2708..1a7251e41 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/audio.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/audio.py @@ -34,7 +34,10 @@ def __init__( spacing = 50.0 width = 460.0 - height = 210.0 + height = 240.0 + uiscale = bui.app.ui_v1.uiscale + + yoffs = -5.0 # Update: hard-coding head-relative audio to true now, # so not showing options. @@ -49,11 +52,10 @@ def __init__( show_soundtracks = True height += spacing * 2.0 - uiscale = bui.app.ui_v1.uiscale base_scale = ( - 2.05 + 1.9 if uiscale is bui.UIScale.SMALL - else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0 + else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ) popup_menu_scale = base_scale * 1.2 @@ -61,9 +63,6 @@ def __init__( root_widget=bui.containerwidget( size=(width, height), scale=base_scale, - stack_offset=( - (0, -20) if uiscale is bui.UIScale.SMALL else (0, 0) - ), toolbar_visibility=( None if uiscale is bui.UIScale.SMALL else 'menu_full' ), @@ -74,21 +73,20 @@ def __init__( self._back_button = back_button = btn = bui.buttonwidget( parent=self._root_widget, - position=(35, height - 55), - size=(120, 60), + position=(35, height + yoffs - 55), + size=(60, 60), scale=0.8, text_scale=1.2, - label=bui.Lstr(resource='backText'), - button_type='back', + label=bui.charstr(bui.SpecialChar.BACK), + button_type='backSmall', on_activate_call=self.main_window_back, autoselect=True, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) - v = height - 60 - v -= spacing * 1.0 + bui.textwidget( parent=self._root_widget, - position=(width * 0.5, height - 32), + position=(width * 0.5, height + yoffs - 32), size=(0, 0), text=bui.Lstr(resource=f'{self._r}.titleText'), color=bui.app.ui_v1.title_color, @@ -97,12 +95,8 @@ def __init__( v_align='center', ) - bui.buttonwidget( - edit=self._back_button, - button_type='backSmall', - size=(60, 60), - label=bui.charstr(bui.SpecialChar.BACK), - ) + v = height + yoffs - 60 + v -= spacing * 1.0 self._sound_volume_numedit = svne = ConfigNumberEdit( parent=self._root_widget, diff --git a/src/assets/ba_data/python/bauiv1lib/store/browser.py b/src/assets/ba_data/python/bauiv1lib/store/browser.py index 575e61e76..ee41bfcff 100644 --- a/src/assets/ba_data/python/bauiv1lib/store/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/store/browser.py @@ -141,11 +141,11 @@ def __init__( parent=self._root_widget, position=( self._width * 0.5, - self._height - (53 if uiscale is bui.UIScale.SMALL else 44), + self._height - (55 if uiscale is bui.UIScale.SMALL else 44), ), size=(0, 0), color=app.ui_v1.title_color, - scale=1.5, + scale=1.1 if uiscale is bui.UIScale.SMALL else 1.5, h_align='center', v_align='center', text=bui.Lstr(resource='storeText'), @@ -536,7 +536,10 @@ def buy(self, item: str) -> None: our_tickets = plus.get_v1_account_ticket_count() if price is not None and our_tickets < price: bui.getsound('error').play() - print('FIXME - show not-enough-tickets info.') + bui.screenmessage( + bui.Lstr(resource='notEnoughTicketsText'), + color=(1, 0, 0), + ) # gettickets.show_get_tickets_prompt() else: diff --git a/src/assets/ba_data/python/bauiv1lib/tournamententry.py b/src/assets/ba_data/python/bauiv1lib/tournamententry.py index ba34cb2ee..b8bfa9c1e 100644 --- a/src/assets/ba_data/python/bauiv1lib/tournamententry.py +++ b/src/assets/ba_data/python/bauiv1lib/tournamententry.py @@ -33,6 +33,8 @@ def __init__( # pylint: disable=too-many-branches # pylint: disable=too-many-statements + from bauiv1lib.coop.tournamentbutton import USE_ENTRY_FEES + assert bui.app.classic is not None assert bui.app.plus bui.set_analytics_screen('Tournament Entry Window') @@ -42,9 +44,15 @@ def __init__( self._tournament_id ] + self._purchase_name: str | None + self._purchase_price_name: str | None + # Set a few vars depending on the tourney fee. self._fee = self._tournament_info['fee'] - self._allow_ads = self._tournament_info['allowAds'] + assert isinstance(self._fee, int | None) + self._allow_ads = ( + self._tournament_info['allowAds'] if USE_ENTRY_FEES else False + ) if self._fee == 4: self._purchase_name = 'tournament_entry_4' self._purchase_price_name = 'price.tournament_entry_4' @@ -57,6 +65,9 @@ def __init__( elif self._fee == 1: self._purchase_name = 'tournament_entry_1' self._purchase_price_name = 'price.tournament_entry_1' + elif self._fee is None or self._fee == -1: + self._purchase_name = None + self._purchase_price_name = 'FREE-WOOT' else: if self._fee != 0: raise ValueError('invalid fee: ' + str(self._fee)) @@ -218,7 +229,7 @@ def __init__( h_align='center', v_align='center', scale=0.6, - # Note: AdMob now requires rewarded ad usage + # Note to self: AdMob requires rewarded ad usage # specifically says 'Ad' in it. text=bui.Lstr(resource='watchAnAdText'), maxwidth=95, @@ -439,29 +450,52 @@ def _update(self) -> None: ) # Keep price up-to-date and update the button with it. - self._purchase_price = plus.get_v1_account_misc_read_val( - self._purchase_price_name, None - ) + if self._purchase_price_name is not None: + self._purchase_price = ( + 0 + if self._purchase_price_name == 'FREE-WOOT' + else plus.get_v1_account_misc_read_val( + self._purchase_price_name, None + ) + ) + # HACK - this is always free now, so just have this say 'PLAY' bui.textwidget( edit=self._ticket_cost_text, text=( - bui.Lstr(resource='getTicketsWindow.freeText') - if self._purchase_price == 0 - else bui.Lstr( - resource='getTicketsWindow.ticketsText', - subs=[ - ( - '${COUNT}', - ( - str(self._purchase_price) - if self._purchase_price is not None - else '?' - ), - ) - ], - ) + bui.Lstr(resource='playText') + # if self._purchase_price == 0 + # else bui.Lstr( + # resource='getTicketsWindow.ticketsText', + # subs=[ + # ( + # '${COUNT}', + # ( + # str(self._purchase_price) + # if self._purchase_price is not None + # else '?' + # ), + # ) + # ], + # ) ), + # text=( + # bui.Lstr(resource='getTicketsWindow.freeText') + # if self._purchase_price == 0 + # else bui.Lstr( + # resource='getTicketsWindow.ticketsText', + # subs=[ + # ( + # '${COUNT}', + # ( + # str(self._purchase_price) + # if self._purchase_price is not None + # else '?' + # ), + # ) + # ], + # ) + # ), position=( self._ticket_cost_text_position_free if self._purchase_price == 0 @@ -472,19 +506,20 @@ def _update(self) -> None: bui.textwidget( edit=self._free_plays_remaining_text, - text=( - '' - if ( - self._tournament_info['freeTriesRemaining'] in [None, 0] - or self._purchase_price != 0 - ) - else '' + str(self._tournament_info['freeTriesRemaining']) - ), + # text=( + # '' + # if ( + # self._tournament_info['freeTriesRemaining'] in [None, 0] + # or self._purchase_price != 0 + # ) + # else '' + str(self._tournament_info['freeTriesRemaining']) + # ), + text='', # No longer relevant. ) bui.imagewidget( edit=self._ticket_img, - opacity=0.2 if self._purchase_price == 0 else 1.0, + opacity=0.0 if self._purchase_price == 0 else 1.0, position=( self._ticket_img_pos_free if self._purchase_price == 0 @@ -547,15 +582,16 @@ def _launch(self, practice: bool = False) -> None: self._launched = True launched = False - # If they gave us an existing, non-consistent - # practice activity, just restart it. + # If they gave us an existing, non-consistent practice activity, + # just restart it. if ( self._tournament_activity is not None and not practice == self._tournament_activity.session.submit_score ): try: if not practice: - bui.apptimer(0.1, bui.getsound('cashRegister').play) + bui.apptimer(0.1, bui.getsound('drumRollShort').play) + # bui.apptimer(0.1, bui.getsound('cashRegister').play) bui.screenmessage( bui.Lstr( translate=( @@ -584,7 +620,8 @@ def _launch(self, practice: bool = False) -> None: # launch a new session. if not launched: if not practice: - bui.apptimer(0.1, bui.getsound('cashRegister').play) + bui.apptimer(0.1, bui.getsound('drumRollShort').play) + # bui.apptimer(0.1, bui.getsound('cashRegister').play) bui.screenmessage( bui.Lstr( translate=('serverResponses', 'Entering tournament...') @@ -653,16 +690,21 @@ def _on_pay_with_tickets_press(self) -> None: ticket_count = None ticket_cost = self._purchase_price if ticket_count is not None and ticket_count < ticket_cost: - # gettickets.show_get_tickets_prompt() - print('FIXME - show not-enough-tickets msg.') bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='notEnoughTicketsText'), + color=(1, 0, 0), + ) + # gettickets.show_get_tickets_prompt() self._transition_out() return cur_time = bui.apptime() self._last_ticket_press_time = cur_time - assert isinstance(ticket_cost, int) - plus.in_game_purchase(self._purchase_name, ticket_cost) + + if self._purchase_name is not None: + assert isinstance(ticket_cost, int) + plus.in_game_purchase(self._purchase_name, ticket_cost) self._entering = True plus.add_v1_account_transaction( @@ -759,30 +801,20 @@ def _on_ad_complete(self, actually_showed: bool) -> None: plus.run_v1_account_transactions() self._launch() - # def _on_get_tickets_press(self) -> None: - # from bauiv1lib import gettickets - - # # If we're already entering, ignore presses. - # if self._entering: - # return - - # # Bring up get-tickets window and then kill ourself (we're on the - # # overlay layer so we'd show up above it). - # gettickets.GetTicketsWindow( - # modal=True, origin_widget=self._get_tickets_button - # ) - # self._transition_out() - def _on_cancel(self) -> None: plus = bui.app.plus assert plus is not None # Don't allow canceling for several seconds after poking an enter # button if it looks like we're waiting on a purchase or entering # the tournament. - if (bui.apptime() - self._last_ticket_press_time < 6.0) and ( - plus.have_outstanding_v1_account_transactions() - or plus.get_v1_account_product_purchased(self._purchase_name) - or self._entering + if ( + (bui.apptime() - self._last_ticket_press_time < 6.0) + and self._purchase_name is not None + and ( + plus.have_outstanding_v1_account_transactions() + or plus.get_v1_account_product_purchased(self._purchase_name) + or self._entering + ) ): bui.getsound('error').play() return diff --git a/src/ballistica/base/app_adapter/app_adapter_apple.cc b/src/ballistica/base/app_adapter/app_adapter_apple.cc index 2fd6beee4..6664642fb 100644 --- a/src/ballistica/base/app_adapter/app_adapter_apple.cc +++ b/src/ballistica/base/app_adapter/app_adapter_apple.cc @@ -138,7 +138,7 @@ auto AppAdapterApple::TryRender() -> bool { // Keep on drawing until the drawn window size // matches what we have (or until we try for too long or fail at drawing). - seconds_t start_time = g_core->GetAppTimeSeconds(); + seconds_t start_time = g_core->AppTimeSeconds(); for (int i = 0; i < 5; ++i) { bool size_differs = ((std::abs(resize_target_resolution_.x @@ -147,7 +147,7 @@ auto AppAdapterApple::TryRender() -> bool { || (std::abs(resize_target_resolution_.y - g_base->graphics_server->screen_pixel_height()) > 0.01f)); - if (size_differs && g_core->GetAppTimeSeconds() - start_time < 0.1 + if (size_differs && g_core->AppTimeSeconds() - start_time < 0.1 && result) { result = g_base->graphics_server->TryRender(); } diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.cc b/src/ballistica/base/app_adapter/app_adapter_sdl.cc index beec9de7f..31fe570d4 100644 --- a/src/ballistica/base/app_adapter/app_adapter_sdl.cc +++ b/src/ballistica/base/app_adapter/app_adapter_sdl.cc @@ -210,7 +210,7 @@ void AppAdapterSDL::RunMainThreadEventLoopToCompletion() { assert(g_core->InMainThread()); while (!done_) { - microsecs_t cycle_start_time = g_core->GetAppTimeMicrosecs(); + microsecs_t cycle_start_time = g_core->AppTimeMicrosecs(); // Events. SDL_Event event; @@ -274,7 +274,7 @@ void AppAdapterSDL::SleepUntilNextEventCycle_(microsecs_t cycle_start_time) { // Normally we just calc when our next draw should happen and sleep 'til // then. - microsecs_t now = g_core->GetAppTimeMicrosecs(); + microsecs_t now = g_core->AppTimeMicrosecs(); auto used_max_fps = max_fps_; millisecs_t millisecs_per_frame = 1000000 / used_max_fps; @@ -319,7 +319,7 @@ void AppAdapterSDL::SleepUntilNextEventCycle_(microsecs_t cycle_start_time) { // Maintain an 'oversleep' amount to compensate for the timer not being // exact. This should keep us exactly at our target frame-rate in the // end. - now = g_core->GetAppTimeMicrosecs(); + now = g_core->AppTimeMicrosecs(); oversleep_ = now - target_time; // Prevent oversleep from compensating by more than a few millisecs per @@ -438,7 +438,7 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) { break; case SDL_QUIT: - if (g_core->GetAppTimeSeconds() - last_windowevent_close_time_ < 0.1) { + if (g_core->AppTimeSeconds() - last_windowevent_close_time_ < 0.1) { // If they hit the window close button, skip the confirm. g_base->QuitApp(false); } else { @@ -459,7 +459,7 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) { case SDL_WINDOWEVENT_CLOSE: { // Simply note that this happened. We use this to adjust our // SDL_QUIT behavior (quit is called right after this). - last_windowevent_close_time_ = g_core->GetAppTimeSeconds(); + last_windowevent_close_time_ = g_core->AppTimeSeconds(); break; } diff --git a/src/ballistica/base/assets/asset.cc b/src/ballistica/base/assets/asset.cc index 2cb9df48e..b03b68682 100644 --- a/src/ballistica/base/assets/asset.cc +++ b/src/ballistica/base/assets/asset.cc @@ -11,7 +11,7 @@ namespace ballistica::base { Asset::Asset() { assert(g_base); assert(g_base->InLogicThread()); - last_used_time_ = g_core->GetAppTimeMillisecs(); + last_used_time_ = g_core->AppTimeMillisecs(); } auto Asset::AssetTypeName(AssetType assettype) -> const char* { @@ -65,9 +65,9 @@ void Asset::Preload(bool already_locked) { return std::string("preloading ") + AssetTypeName(GetAssetType()) + " " + GetName(); }); - preload_start_time_ = g_core->GetAppTimeMillisecs(); + preload_start_time_ = g_core->AppTimeMillisecs(); DoPreload(); - preload_end_time_ = g_core->GetAppTimeMillisecs(); + preload_end_time_ = g_core->AppTimeMillisecs(); preloaded_ = true; } } @@ -87,9 +87,9 @@ void Asset::Load(bool already_locked) { return std::string("loading ") + AssetTypeName(GetAssetType()) + " " + GetName(); }); - load_start_time_ = g_core->GetAppTimeMillisecs(); + load_start_time_ = g_core->AppTimeMillisecs(); DoLoad(); - load_end_time_ = g_core->GetAppTimeMillisecs(); + load_end_time_ = g_core->AppTimeMillisecs(); BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(50, GetName()); loaded_ = true; } diff --git a/src/ballistica/base/assets/assets.cc b/src/ballistica/base/assets/assets.cc index a05f24d40..14bc31c9e 100644 --- a/src/ballistica/base/assets/assets.cc +++ b/src/ballistica/base/assets/assets.cc @@ -165,6 +165,7 @@ void Assets::StartLoading() { LoadSystemTexture(SysTextureID::kCharacterIconMask, "characterIconMask"); LoadSystemTexture(SysTextureID::kBlack, "black"); LoadSystemTexture(SysTextureID::kWings, "wings"); + LoadSystemTexture(SysTextureID::kSpinner, "spinner"); // System cube map textures: LoadSystemCubeMapTexture(SysCubeMapTextureID::kReflectionChar, @@ -479,7 +480,7 @@ auto Assets::GetAsset(const std::string& file_name, have_pending_loads_[static_cast(d->GetAssetType())] = true; MarkAssetForLoad(d.get()); } - d->set_last_used_time(g_core->GetAppTimeMillisecs()); + d->set_last_used_time(g_core->AppTimeMillisecs()); return Object::Ref(d); } } @@ -499,7 +500,7 @@ auto Assets::GetTexture(TextPacker* packer) -> Object::Ref { have_pending_loads_[static_cast(d->GetAssetType())] = true; MarkAssetForLoad(d.get()); } - d->set_last_used_time(g_core->GetAppTimeMillisecs()); + d->set_last_used_time(g_core->AppTimeMillisecs()); return Object::Ref(d); } } @@ -519,7 +520,7 @@ auto Assets::GetQRCodeTexture(const std::string& url) have_pending_loads_[static_cast(d->GetAssetType())] = true; MarkAssetForLoad(d.get()); } - d->set_last_used_time(g_core->GetAppTimeMillisecs()); + d->set_last_used_time(g_core->AppTimeMillisecs()); return Object::Ref(d); } } @@ -542,7 +543,7 @@ auto Assets::GetCubeMapTexture(const std::string& file_name) have_pending_loads_[static_cast(d->GetAssetType())] = true; MarkAssetForLoad(d.get()); } - d->set_last_used_time(g_core->GetAppTimeMillisecs()); + d->set_last_used_time(g_core->AppTimeMillisecs()); return Object::Ref(d); } } @@ -598,7 +599,7 @@ auto Assets::GetTexture(const std::string& file_name) have_pending_loads_[static_cast(d->GetAssetType())] = true; MarkAssetForLoad(d.get()); } - d->set_last_used_time(g_core->GetAppTimeMillisecs()); + d->set_last_used_time(g_core->AppTimeMillisecs()); return Object::Ref(d); } } @@ -750,7 +751,7 @@ auto Assets::RunPendingLoadsLogicThread() -> bool { template auto Assets::RunPendingLoadList(std::vector*>* c_list) -> bool { bool flush = false; - millisecs_t starttime = g_core->GetAppTimeMillisecs(); + millisecs_t starttime = g_core->AppTimeMillisecs(); std::vector*> l; std::vector*> l_unfinished; @@ -760,8 +761,7 @@ auto Assets::RunPendingLoadList(std::vector*>* c_list) -> bool { // If we're already out of time. if (!flush - && g_core->GetAppTimeMillisecs() - starttime - > PENDING_LOAD_PROCESS_TIME) { + && g_core->AppTimeMillisecs() - starttime > PENDING_LOAD_PROCESS_TIME) { bool return_val = (!c_list->empty()); return return_val; } @@ -790,8 +790,7 @@ auto Assets::RunPendingLoadList(std::vector*>* c_list) -> bool { // If the load finished, pop it on our "done-loading" list.. otherwise // keep it around. l_finished.push_back(*i); // else l_unfinished.push_back(*i); - if (g_core->GetAppTimeMillisecs() - starttime - > PENDING_LOAD_PROCESS_TIME + if (g_core->AppTimeMillisecs() - starttime > PENDING_LOAD_PROCESS_TIME && !flush) { out_of_time = true; } @@ -832,7 +831,7 @@ auto Assets::RunPendingLoadList(std::vector*>* c_list) -> bool { void Assets::Prune(int level) { assert(g_base->InLogicThread()); - millisecs_t current_time = g_core->GetAppTimeMillisecs(); + millisecs_t current_time = g_core->AppTimeMillisecs(); // Need lists locked while accessing/modifying them. AssetListLock lock; @@ -1168,11 +1167,11 @@ auto Assets::FindAssetFile(FileType type, const std::string& name) // We wanna fail gracefully for some types. if (type == FileType::kSound && name != "blank") { g_core->Log(LogName::kBaAssets, LogLevel::kError, - "Unable to load audio: '" + name + "'; trying fallback..."); + "Unable to load audio: '" + name + "'."); return FindAssetFile(type, "blank"); } else if (type == FileType::kTexture && name != "white") { g_core->Log(LogName::kBaAssets, LogLevel::kError, - "Unable to load texture: '" + name + "'; trying fallback..."); + "Unable to load texture: '" + name + "'."); return FindAssetFile(type, "white"); } @@ -1560,8 +1559,8 @@ auto DoCompileResourceString(cJSON* obj) -> std::string { return result; } -auto Assets::CompileResourceString(const std::string& s, const std::string& loc, - bool* valid) -> std::string { +auto Assets::CompileResourceString(const std::string& s, bool* valid) + -> std::string { bool dummyvalid; if (valid == nullptr) { valid = &dummyvalid; @@ -1577,8 +1576,7 @@ auto Assets::CompileResourceString(const std::string& s, const std::string& loc, cJSON* root = cJSON_Parse(s.c_str()); if (root == nullptr) { g_core->Log(LogName::kBaAssets, LogLevel::kError, - "CompileResourceString failed (loc " + loc - + "); invalid json: '" + s + "'"); + "CompileResourceString failed; invalid json: '" + s + "'"); *valid = false; return ""; } @@ -1588,8 +1586,8 @@ auto Assets::CompileResourceString(const std::string& s, const std::string& loc, *valid = true; } catch (const std::exception& e) { g_core->Log(LogName::kBaAssets, LogLevel::kError, - "CompileResourceString failed (loc " + loc - + "): " + std::string(e.what()) + "; str='" + s + "'"); + "CompileResourceString failed: " + std::string(e.what()) + + "; str='" + s + "'"); result = ""; *valid = false; } diff --git a/src/ballistica/base/assets/assets.h b/src/ballistica/base/assets/assets.h index aa396371d..53744459c 100644 --- a/src/ballistica/base/assets/assets.h +++ b/src/ballistica/base/assets/assets.h @@ -111,8 +111,8 @@ class Assets { const std::unordered_map& language); auto GetResourceString(const std::string& key) -> std::string; auto CharStr(SpecialChar id) -> std::string; - auto CompileResourceString(const std::string& s, const std::string& loc, - bool* valid = nullptr) -> std::string; + auto CompileResourceString(const std::string& s, bool* valid = nullptr) + -> std::string; auto sys_assets_loaded() const { return sys_assets_loaded_; } diff --git a/src/ballistica/base/assets/sound_asset.cc b/src/ballistica/base/assets/sound_asset.cc index 808b92e5d..008980b99 100644 --- a/src/ballistica/base/assets/sound_asset.cc +++ b/src/ballistica/base/assets/sound_asset.cc @@ -324,7 +324,7 @@ void SoundAsset::DoUnload() { } void SoundAsset::UpdatePlayTime() { - last_play_time_ = g_core->GetAppTimeMillisecs(); + last_play_time_ = g_core->AppTimeMillisecs(); } } // namespace ballistica::base diff --git a/src/ballistica/base/audio/audio.cc b/src/ballistica/base/audio/audio.cc index 7c7e57165..ff5e9a2af 100644 --- a/src/ballistica/base/audio/audio.cc +++ b/src/ballistica/base/audio/audio.cc @@ -156,7 +156,7 @@ auto Audio::SourceBeginExisting(uint32_t play_id, int debug_id) } auto Audio::ShouldPlay(SoundAsset* sound) -> bool { - millisecs_t time = g_core->GetAppTimeMillisecs(); + millisecs_t time = g_core->AppTimeMillisecs(); assert(sound); return (time - sound->last_play_time() > 50); } diff --git a/src/ballistica/base/audio/audio_server.cc b/src/ballistica/base/audio/audio_server.cc index 6ac888cd2..ec729aa4a 100644 --- a/src/ballistica/base/audio/audio_server.cc +++ b/src/ballistica/base/audio/audio_server.cc @@ -212,9 +212,8 @@ void AudioServer::OpenALSoftLogCallback(const std::string& msg) { std::scoped_lock lock(openalsoft_android_log_mutex_); if (openalsoft_android_log_.size() < log_cap) { - openalsoft_android_log_ += "openal-log(" - + std::to_string(g_core->GetAppTimeSeconds()) - + "s): " + msg; + openalsoft_android_log_ += + "openal-log(" + std::to_string(g_core->AppTimeSeconds()) + "s): " + msg; if (openalsoft_android_log_.size() >= log_cap) { openalsoft_android_log_ += "\n\n"; @@ -477,7 +476,7 @@ void AudioServer::OnAppStartInThread_() { // Now make available any stopped sources (should be all of them). UpdateAvailableSources_(); - last_started_playing_time_ = g_core->GetAppTimeSeconds(); + last_started_playing_time_ = g_core->AppTimeSeconds(); #endif // BA_ENABLE_AUDIO } @@ -487,7 +486,7 @@ void AudioServer::Shutdown() { return; } shutting_down_ = true; - shutdown_start_time_ = g_core->GetAppTimeSeconds(); + shutdown_start_time_ = g_core->AppTimeSeconds(); // Stop all playing sounds and note the time. We'll then give everything a // moment to come to a halt before we tear down the audio context to @@ -538,8 +537,8 @@ struct AudioServer::SoundFadeNode_ { bool out; SoundFadeNode_(uint32_t play_id_in, millisecs_t duration_in, bool out_in) : play_id(play_id_in), - starttime(g_core->GetAppTimeMillisecs()), - endtime(g_core->GetAppTimeMillisecs() + duration_in), + starttime(g_core->AppTimeMillisecs()), + endtime(g_core->AppTimeMillisecs() + duration_in), out(out_in) {} }; @@ -566,16 +565,16 @@ void AudioServer::SetSuspended_(bool suspend) { try { g_core->platform->LowLevelDebugLog( "Calling alcDevicePauseSOFT at " - + std::to_string(g_core->GetAppTimeSeconds())); + + std::to_string(g_core->AppTimeSeconds())); alcDevicePauseSOFT(device); } catch (const std::exception& e) { - g_core->Log(LogName::kBaAudio, LogLevel::kError, - "Error in alcDevicePauseSOFT at time " - + std::to_string(g_core->GetAppTimeSeconds()) - + "( playing since " - + std::to_string(last_started_playing_time_) + "): " - + g_core->platform->DemangleCXXSymbol(typeid(e).name()) - + " " + e.what()); + g_core->Log( + LogName::kBaAudio, LogLevel::kError, + "Error in alcDevicePauseSOFT at time " + + std::to_string(g_core->AppTimeSeconds()) + "( playing since " + + std::to_string(last_started_playing_time_) + + "): " + g_core->platform->DemangleCXXSymbol(typeid(e).name()) + + " " + e.what()); } catch (...) { g_core->Log(LogName::kBaAudio, LogLevel::kError, "Unknown error in alcDevicePauseSOFT"); @@ -609,12 +608,12 @@ void AudioServer::SetSuspended_(bool suspend) { try { g_core->platform->LowLevelDebugLog( "Calling alcDeviceResumeSOFT at " - + std::to_string(g_core->GetAppTimeSeconds())); + + std::to_string(g_core->AppTimeSeconds())); alcDeviceResumeSOFT(device); } catch (const std::exception& e) { g_core->Log(LogName::kBaAudio, LogLevel::kError, "Error in alcDeviceResumeSOFT at time " - + std::to_string(g_core->GetAppTimeSeconds()) + ": " + + std::to_string(g_core->AppTimeSeconds()) + ": " + g_core->platform->DemangleCXXSymbol(typeid(e).name()) + " " + e.what()); } catch (...) { @@ -622,7 +621,7 @@ void AudioServer::SetSuspended_(bool suspend) { "Unknown error in alcDeviceResumeSOFT"); } #endif - last_started_playing_time_ = g_core->GetAppTimeSeconds(); + last_started_playing_time_ = g_core->AppTimeSeconds(); suspended_ = false; #if BA_ENABLE_AUDIO CHECK_AL_ERROR; @@ -774,7 +773,7 @@ void AudioServer::UpdateAvailableSources_() { // and see how many are in use, how many are currently locked by the client, // etc. #if (BA_DEBUG_BUILD || BA_TEST_BUILD) - millisecs_t t = g_core->GetAppTimeMillisecs(); + millisecs_t t = g_core->AppTimeMillisecs(); if (t - last_sanity_check_time_ > 5000) { last_sanity_check_time_ = t; @@ -1033,7 +1032,7 @@ void AudioServer::OnDeviceDisconnected() { void AudioServer::Process_() { assert(g_base->InAudioThread()); - seconds_t real_time_seconds = g_core->GetAppTimeSeconds(); + seconds_t real_time_seconds = g_core->AppTimeSeconds(); millisecs_t real_time_millisecs = real_time_seconds * 1000; // Only do real work if we're in normal running mode. @@ -1085,7 +1084,7 @@ void AudioServer::Process_() { // for the mixer to spit out some silence so we don't hear sudden cut-offs // in one or both ears. if (shutting_down_ && !shutdown_completed_) { - if (g_core->GetAppTimeSeconds() - shutdown_start_time_ > 0.2) { + if (g_core->AppTimeSeconds() - shutdown_start_time_ > 0.2) { CompleteShutdown_(); } } @@ -1125,13 +1124,13 @@ void AudioServer::ProcessSoundFades_() { AudioServer::ThreadSource_* s = GetPlayingSound_(i->second.play_id); if (s) { - if (g_core->GetAppTimeMillisecs() > i->second.endtime) { + if (g_core->AppTimeMillisecs() > i->second.endtime) { StopSound(i->second.play_id); sound_fade_nodes_.erase(i); } else { float fade_val = 1 - - (static_cast(g_core->GetAppTimeMillisecs() + - (static_cast(g_core->AppTimeMillisecs() - i->second.starttime) / static_cast(i->second.endtime - i->second.starttime)); s->SetFade(fade_val); @@ -1639,12 +1638,12 @@ void AudioServer::ClearSoundRefDeleteList() { // g_base->audio_server->PushSetSuspendedCall(true); // // Wait a reasonable amount of time for the thread to act on it. -// millisecs_t t = g_core->GetAppTimeMillisecs(); +// millisecs_t t = g_core->AppTimeMillisecs(); // while (true) { // if (g_base->audio_server->suspended()) { // break; // } -// if (g_core->GetAppTimeMillisecs() - t > 1000) { +// if (g_core->AppTimeMillisecs() - t > 1000) { // Log(LogLevel::kError, "Timed out waiting for audio suspend."); // break; // } @@ -1657,12 +1656,12 @@ void AudioServer::ClearSoundRefDeleteList() { // g_base->audio_server->PushSetSuspendedCall(false); // // Wait a reasonable amount of time for the thread to act on it. -// millisecs_t t = g_core->GetAppTimeMillisecs(); +// millisecs_t t = g_core->AppTimeMillisecs(); // while (true) { // if (!g_base->audio_server->suspended()) { // break; // } -// if (g_core->GetAppTimeMillisecs() - t > 1000) { +// if (g_core->AppTimeMillisecs() - t > 1000) { // Log(LogLevel::kError, "Timed out waiting for audio unsuspend."); // break; // } diff --git a/src/ballistica/base/audio/audio_source.cc b/src/ballistica/base/audio/audio_source.cc index fa420c86c..e3f1dc5e0 100644 --- a/src/ballistica/base/audio/audio_source.cc +++ b/src/ballistica/base/audio/audio_source.cc @@ -103,7 +103,7 @@ void AudioSource::Lock(int debug_id) { BA_DEBUG_FUNCTION_TIMER_BEGIN(); mutex_.lock(); #if BA_DEBUG_BUILD - last_lock_time_ = g_core->GetAppTimeMillisecs(); + last_lock_time_ = g_core->AppTimeMillisecs(); lock_debug_id_ = debug_id; locked_ = true; #endif @@ -115,7 +115,7 @@ auto AudioSource::TryLock(int debug_id) -> bool { #if (BA_DEBUG_BUILD || BA_TEST_BUILD) if (locked) { locked_ = true; - last_lock_time_ = g_core->GetAppTimeMillisecs(); + last_lock_time_ = g_core->AppTimeMillisecs(); lock_debug_id_ = debug_id; } #endif diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc index 99b62cf18..049810e81 100644 --- a/src/ballistica/base/base.cc +++ b/src/ballistica/base/base.cc @@ -174,6 +174,11 @@ void BaseFeatureSet::ErrorScreenMessage() { } auto BaseFeatureSet::GetV2AccountID() -> std::optional { + // Guard against this getting called early. + if (!IsAppStarted()) { + return {}; + } + auto gil = Python::ScopedInterpreterLock(); auto result = python->objs().Get(BasePython::ObjID::kGetV2AccountIdCall).Call(); @@ -199,7 +204,7 @@ void BaseFeatureSet::StartApp() { BA_PRECONDITION(g_core->InMainThread()); BA_PRECONDITION(g_base); - auto start_time = g_core->GetAppTimeSeconds(); + auto start_time = g_core->AppTimeSeconds(); // Currently limiting this to once per process. BA_PRECONDITION(!called_start_app_); @@ -253,7 +258,7 @@ void BaseFeatureSet::StartApp() { // Make some noise if this takes more than a few seconds. If we pass 5 // seconds or so we start to trigger App-Not-Responding reports which // isn't good. - auto duration = g_core->GetAppTimeSeconds() - start_time; + auto duration = g_core->AppTimeSeconds() - start_time; if (duration > 3.0) { char buffer[128]; snprintf(buffer, sizeof(buffer), @@ -272,7 +277,7 @@ void BaseFeatureSet::SuspendApp() { return; } - millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()}; + millisecs_t start_time{core::CorePlatform::TimeMonotonicMillisecs()}; // Apple mentioned 5 seconds to run stuff once backgrounded or they bring // down the hammer. Let's aim to stay under 2. @@ -280,7 +285,7 @@ void BaseFeatureSet::SuspendApp() { g_core->platform->LowLevelDebugLog( "SuspendApp@" - + std::to_string(core::CorePlatform::GetCurrentMillisecs())); + + std::to_string(core::CorePlatform::TimeMonotonicMillisecs())); app_suspended_ = true; // IMPORTANT: Any pause related stuff that event-loop-threads need to do @@ -311,13 +316,13 @@ void BaseFeatureSet::SuspendApp() { g_core->Log( LogName::kBa, LogLevel::kDebug, "SuspendApp() completed in " - + std::to_string(core::CorePlatform::GetCurrentMillisecs() + + std::to_string(core::CorePlatform::TimeMonotonicMillisecs() - start_time) + "ms."); } return; } - } while (std::abs(core::CorePlatform::GetCurrentMillisecs() - start_time) + } while (std::abs(core::CorePlatform::TimeMonotonicMillisecs() - start_time) < max_duration); // If we made it here, we timed out. Complain. @@ -325,7 +330,8 @@ void BaseFeatureSet::SuspendApp() { std::string("SuspendApp() took too long; ") + std::to_string(running_loops.size()) + " event-loops not yet suspended after " - + std::to_string(core::CorePlatform::GetCurrentMillisecs() - start_time) + + std::to_string(core::CorePlatform::TimeMonotonicMillisecs() + - start_time) + " ms: ("; bool first = true; for (auto* loop : running_loops) { @@ -383,10 +389,10 @@ void BaseFeatureSet::UnsuspendApp() { "AppAdapter::UnsuspendApp() called with app not in suspendedstate."); return; } - millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()}; + millisecs_t start_time{core::CorePlatform::TimeMonotonicMillisecs()}; g_core->platform->LowLevelDebugLog( "UnsuspendApp@" - + std::to_string(core::CorePlatform::GetCurrentMillisecs())); + + std::to_string(core::CorePlatform::TimeMonotonicMillisecs())); app_suspended_ = false; // Spin all event-loops back up. @@ -397,11 +403,12 @@ void BaseFeatureSet::UnsuspendApp() { g_base->networking->OnAppUnsuspend(); if (g_buildconfig.debug_build()) { - g_core->Log(LogName::kBa, LogLevel::kDebug, - "UnsuspendApp() completed in " - + std::to_string(core::CorePlatform::GetCurrentMillisecs() - - start_time) - + "ms."); + g_core->Log( + LogName::kBa, LogLevel::kDebug, + "UnsuspendApp() completed in " + + std::to_string(core::CorePlatform::TimeMonotonicMillisecs() + - start_time) + + "ms."); } } @@ -571,7 +578,7 @@ auto BaseFeatureSet::GetAppInstanceUUID() -> const std::string& { g_core->Log(LogName::kBa, LogLevel::kWarning, "GetSessionUUID() using rand fallback."); srand(static_cast( - core::CorePlatform::GetCurrentMillisecs())); // NOLINT + core::CorePlatform::TimeMonotonicMillisecs())); // NOLINT app_instance_uuid = std::to_string(static_cast(rand())); // NOLINT have_app_instance_uuid = true; @@ -966,7 +973,7 @@ void BaseFeatureSet::SetAppActive(bool active) { g_core->platform->LowLevelDebugLog( "SetAppActive(" + std::to_string(active) + ")@" - + std::to_string(core::CorePlatform::GetCurrentMillisecs())); + + std::to_string(core::CorePlatform::TimeMonotonicMillisecs())); // Issue a gentle warning if they are feeding us the same state twice in a // row; might imply faulty logic on an app-adapter or whatnot. @@ -990,6 +997,12 @@ void BaseFeatureSet::Reset() { audio->Reset(); } +auto BaseFeatureSet::TimeSinceEpochCloudSeconds() -> seconds_t { + // TODO(ericf): wire this up. Just using local time for now. And make sure + // that this and utc_now_cloud() in the Python layer are synced up. + return core::CorePlatform::TimeSinceEpochSeconds(); +} + void BaseFeatureSet::SetUIScale(UIScale scale) { assert(InLogicThread()); diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h index 8bc37bb71..7da635387 100644 --- a/src/ballistica/base/base.h +++ b/src/ballistica/base/base.h @@ -476,7 +476,8 @@ enum class SysTextureID : uint8_t { kFontExtras4, kCharacterIconMask, kBlack, - kWings + kWings, + kSpinner }; enum class SysCubeMapTextureID : uint8_t { @@ -761,8 +762,8 @@ class BaseFeatureSet : public FeatureSetNativeComponent, void PushMainThreadRunnable(Runnable* runnable) override; - /// Return the currently signed in V2 account id as - /// reported by the Python layer. + /// Return the currently signed in V2 account id as reported by the Python + /// layer. auto GetV2AccountID() -> std::optional; /// Return whether clipboard operations are supported at all. This gets @@ -784,6 +785,10 @@ class BaseFeatureSet : public FeatureSetNativeComponent, /// Set overall ui scale for the app. void SetUIScale(UIScale scale); + /// Time since epoch on the master-server. Tries to + /// be correct even if local time is set wrong. + auto TimeSinceEpochCloudSeconds() -> seconds_t; + // Const subsystems. AppAdapter* const app_adapter; AppConfig* const app_config; diff --git a/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc b/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc index 29ef9e4a1..8e8e02cf7 100644 --- a/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc @@ -96,7 +96,7 @@ class BGDynamicsServer::Terrain { if (collision_mesh_) { Object::Ref* ref = collision_mesh_; g_base->logic->event_loop()->PushCall([ref] { - (**ref).set_last_used_time(g_core->GetAppTimeMillisecs()); + (**ref).set_last_used_time(g_core->AppTimeMillisecs()); delete ref; }); collision_mesh_ = nullptr; diff --git a/src/ballistica/base/graphics/gl/renderer_gl.cc b/src/ballistica/base/graphics/gl/renderer_gl.cc index ed46a1308..f04005fe4 100644 --- a/src/ballistica/base/graphics/gl/renderer_gl.cc +++ b/src/ballistica/base/graphics/gl/renderer_gl.cc @@ -90,12 +90,12 @@ void RendererGL::CheckGLError(const char* file, int line) { BA_PRECONDITION_FATAL(vendor); const char* renderer = (const char*)glGetString(GL_RENDERER); BA_PRECONDITION_FATAL(renderer); - g_core->Log( - LogName::kBaGraphics, LogLevel::kError, - "OpenGL Error at " + std::string(file) + " line " + std::to_string(line) - + ": " + GLErrorToString(err) + "\nrenderer: " + renderer - + "\nvendor: " + vendor + "\nversion: " + version - + "\ntime: " + std::to_string(g_core->GetAppTimeMillisecs())); + g_core->Log(LogName::kBaGraphics, LogLevel::kError, + "OpenGL Error at " + std::string(file) + " line " + + std::to_string(line) + ": " + GLErrorToString(err) + + "\nrenderer: " + renderer + "\nvendor: " + vendor + + "\nversion: " + version + + "\ntime: " + std::to_string(g_core->AppTimeMillisecs())); } } diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc index c9fcffa2d..7c16f67f1 100644 --- a/src/ballistica/base/graphics/graphics.cc +++ b/src/ballistica/base/graphics/graphics.cc @@ -253,13 +253,13 @@ auto Graphics::GraphicsQualityFromAppConfig() -> GraphicsQualityRequest { void Graphics::SetGyroEnabled(bool enable) { // If we're turning back on, suppress gyro updates for a bit. if (enable && !gyro_enabled_) { - last_suppress_gyro_time_ = g_core->GetAppTimeMicrosecs(); + last_suppress_gyro_time_ = g_core->AppTimeMicrosecs(); } gyro_enabled_ = enable; } void Graphics::UpdateProgressBarProgress(float target) { - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); float p = target; if (p < 0) { p = 0; @@ -274,7 +274,7 @@ void Graphics::UpdateProgressBarProgress(float target) { } void Graphics::DrawProgressBar(RenderPass* pass, float opacity) { - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); float amount = progress_bar_progress_; if (amount < 0) { amount = 0; @@ -361,9 +361,9 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) { assert(g_base && g_base->InLogicThread()); // Every now and then, update our stats. - while (g_core->GetAppTimeMillisecs() >= next_stat_update_time_) { - if (g_core->GetAppTimeMillisecs() - next_stat_update_time_ > 1000) { - next_stat_update_time_ = g_core->GetAppTimeMillisecs() + 1000; + while (g_core->AppTimeMillisecs() >= next_stat_update_time_) { + if (g_core->AppTimeMillisecs() - next_stat_update_time_ > 1000) { + next_stat_update_time_ = g_core->AppTimeMillisecs() + 1000; } else { next_stat_update_time_ += 1000; } @@ -482,7 +482,7 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) { // Draw any debug graphs. { float debug_graph_y = 50.0; - auto now = g_core->GetAppTimeMillisecs(); + auto now = g_core->AppTimeMillisecs(); for (auto it = debug_graphs_.begin(); it != debug_graphs_.end();) { assert(it->second.exists()); if (now - it->second->LastUsedTime() > 1000) { @@ -508,7 +508,7 @@ auto Graphics::GetDebugGraph(const std::string& name, bool smoothed) debug_graphs_[name]->SetLabel(name); debug_graphs_[name]->SetSmoothed(smoothed); } - debug_graphs_[name]->SetLastUsedTime(g_core->GetAppTimeMillisecs()); + debug_graphs_[name]->SetLastUsedTime(g_core->AppTimeMillisecs()); return debug_graphs_[name].get(); } @@ -770,7 +770,7 @@ void Graphics::BuildAndPushFrameDef() { assert(!building_frame_def_); building_frame_def_ = true; - microsecs_t app_time_microsecs = g_core->GetAppTimeMicrosecs(); + microsecs_t app_time_microsecs = g_core->AppTimeMicrosecs(); // Store how much time this frame_def represents. auto display_time_microsecs = g_base->logic->display_time_microsecs(); @@ -1222,7 +1222,7 @@ void Graphics::EnableProgressBar(bool fade_in) { if (progress_bar_loads_ > 0) { progress_bar_ = true; progress_bar_fade_in_ = fade_in; - last_progress_bar_draw_time_ = g_core->GetAppTimeMillisecs(); + last_progress_bar_draw_time_ = g_core->AppTimeMillisecs(); last_progress_bar_start_time_ = last_progress_bar_draw_time_; progress_bar_progress_ = 0.0f; } diff --git a/src/ballistica/base/graphics/graphics_server.cc b/src/ballistica/base/graphics/graphics_server.cc index 493467acf..3be3790ed 100644 --- a/src/ballistica/base/graphics/graphics_server.cc +++ b/src/ballistica/base/graphics/graphics_server.cc @@ -147,7 +147,7 @@ auto GraphicsServer::TryRender() -> bool { auto GraphicsServer::WaitForRenderFrameDef_() -> FrameDef* { assert(g_base->app_adapter->InGraphicsContext()); - millisecs_t start_time = g_core->GetAppTimeMillisecs(); + millisecs_t start_time = g_core->AppTimeMillisecs(); // Spin and wait for a short bit for a frame_def to appear. while (true) { @@ -176,7 +176,7 @@ auto GraphicsServer::WaitForRenderFrameDef_() -> FrameDef* { } // If there's no frame_def for us, sleep for a bit and wait for it. - millisecs_t t = g_core->GetAppTimeMillisecs() - start_time; + millisecs_t t = g_core->AppTimeMillisecs() - start_time; if (t >= 1000) { if (g_buildconfig.debug_build()) { g_core->Log(LogName::kBaGraphics, LogLevel::kWarning, diff --git a/src/ballistica/base/graphics/renderer/renderer.cc b/src/ballistica/base/graphics/renderer/renderer.cc index 2a61bc605..27645a957 100644 --- a/src/ballistica/base/graphics/renderer/renderer.cc +++ b/src/ballistica/base/graphics/renderer/renderer.cc @@ -650,7 +650,7 @@ void Renderer::UpdatePixelScaleAndBackingBuffer(FrameDef* frame_def) { } void Renderer::LoadMedia(FrameDef* frame_def) { - millisecs_t t = g_core->GetAppTimeMillisecs(); + millisecs_t t = g_core->AppTimeMillisecs(); for (auto&& i : frame_def->media_components()) { Asset* mc = i.get(); assert(mc); @@ -667,7 +667,7 @@ void Renderer::LoadMedia(FrameDef* frame_def) { // // default about 1 second after a res change, etc... // // so if we're using a non-1.0 gamma, lets keep setting it periodically // // to force the issue -// millisecs_t t = g_core->GetAppTimeMillisecs(); +// millisecs_t t = g_core->AppTimeMillisecs(); // if (screen_gamma_requested_ != screen_gamma_ // || (t - last_screen_gamma_update_time_ > 300 && screen_gamma_ != 1.0f)) // { diff --git a/src/ballistica/base/graphics/support/camera.cc b/src/ballistica/base/graphics/support/camera.cc index e8137cb9f..d236d9708 100644 --- a/src/ballistica/base/graphics/support/camera.cc +++ b/src/ballistica/base/graphics/support/camera.cc @@ -207,10 +207,9 @@ void Camera::UpdatePosition() { lr_jitter = 0.0f; } else { lr_jitter = - sinf(static_cast(g_core->GetAppTimeMillisecs()) / 108.0f) + sinf(static_cast(g_core->AppTimeMillisecs()) / 108.0f) * 0.4f - + sinf(static_cast(g_core->GetAppTimeMillisecs()) - / 268.0f) + + sinf(static_cast(g_core->AppTimeMillisecs()) / 268.0f) * 1.0f; lr_jitter *= 0.05f; } @@ -891,7 +890,7 @@ void Camera::SetMode(CameraMode m) { if (mode_ != m) { mode_ = m; smooth_next_frame_ = false; - // last_mode_set_time_ = g_core->GetAppTimeMillisecs(); + // last_mode_set_time_ = g_core->AppTimeMillisecs(); // last_mode_set_time_ = time_; heading_ = kInitialHeading; } diff --git a/src/ballistica/base/graphics/support/screen_messages.cc b/src/ballistica/base/graphics/support/screen_messages.cc index c857d99d8..3a7dd8c35 100644 --- a/src/ballistica/base/graphics/support/screen_messages.cc +++ b/src/ballistica/base/graphics/support/screen_messages.cc @@ -65,8 +65,8 @@ void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) { // Delete old ones. if (!screen_messages_.empty()) { millisecs_t cutoff; - if (g_core->GetAppTimeMillisecs() > 5000) { - cutoff = g_core->GetAppTimeMillisecs() - 5000; + if (g_core->AppTimeMillisecs() > 5000) { + cutoff = g_core->AppTimeMillisecs() - 5000; for (auto i = screen_messages_.begin(); i != screen_messages_.end();) { if (i->creation_time < cutoff) { auto next = i; @@ -128,7 +128,7 @@ void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) { // which is calculated as part of it. i->GetText(); - millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time; + millisecs_t age = g_core->AppTimeMillisecs() - i->creation_time; youngest_age = std::min(youngest_age, age); float s_extra = 1.0f; if (age < 100) { @@ -244,7 +244,7 @@ void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) { for (auto i = screen_messages_.rbegin(); i != screen_messages_.rend(); i++) { - millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time; + millisecs_t age = g_core->AppTimeMillisecs() - i->creation_time; youngest_age = std::min(youngest_age, age); float s_extra = 1.0f; if (age < 100) { @@ -315,8 +315,8 @@ void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) { // Delete old ones. if (!screen_messages_top_.empty()) { millisecs_t cutoff; - if (g_core->GetAppTimeMillisecs() > 5000) { - cutoff = g_core->GetAppTimeMillisecs() - 5000; + if (g_core->AppTimeMillisecs() > 5000) { + cutoff = g_core->AppTimeMillisecs() - 5000; for (auto i = screen_messages_top_.begin(); i != screen_messages_top_.end();) { if (i->creation_time < cutoff) { @@ -354,7 +354,7 @@ void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) { // Update the translation if need be. i->UpdateTranslation(); - millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time; + millisecs_t age = g_core->AppTimeMillisecs() - i->creation_time; float s_extra = 1.0f; if (age < 100) { s_extra = std::min(1.1f, 1.1f * (static_cast(age) / 100.0f)); @@ -466,13 +466,13 @@ void ScreenMessages::AddScreenMessage(const std::string& msg, start_v, std::max(-100.0f, screen_messages_top_.back().v_smoothed - 25.0f)); } - screen_messages_top_.emplace_back(m, true, g_core->GetAppTimeMillisecs(), + screen_messages_top_.emplace_back(m, true, g_core->AppTimeMillisecs(), color, texture, tint_texture, tint, tint2); screen_messages_top_.back().v_smoothed = start_v; } else { - screen_messages_.emplace_back(m, false, g_core->GetAppTimeMillisecs(), - color, texture, tint_texture, tint, tint2); + screen_messages_.emplace_back(m, false, g_core->AppTimeMillisecs(), color, + texture, tint_texture, tint, tint2); } } @@ -534,8 +534,7 @@ auto ScreenMessages::ScreenMessageEntry::GetText() -> TextGroup& { void ScreenMessages::ScreenMessageEntry::UpdateTranslation() { if (translation_dirty) { - s_translated = g_base->assets->CompileResourceString( - s_raw, "Graphics::ScreenMessageEntry::UpdateTranslation"); + s_translated = g_base->assets->CompileResourceString(s_raw); translation_dirty = false; mesh_dirty = true; } diff --git a/src/ballistica/base/input/device/joystick_input.cc b/src/ballistica/base/input/device/joystick_input.cc index 09e8b6c5f..92626ee5d 100644 --- a/src/ballistica/base/input/device/joystick_input.cc +++ b/src/ballistica/base/input/device/joystick_input.cc @@ -37,7 +37,7 @@ JoystickInput::JoystickInput(int sdl_joystick_id, calibration_break_threshold_(kJoystickCalibrationBreakThreshold), custom_device_name_(custom_device_name), can_configure_(can_configure), - creation_time_(g_core->GetAppTimeMillisecs()), + creation_time_(g_core->AppTimeMillisecs()), calibrate_(calibrate) { // This is the default calibration for 'non-full' analog calibration. for (float& analog_calibration_val : analog_calibration_vals_) { @@ -374,7 +374,7 @@ void JoystickInput::Update() { // Let's take this opportunity to update our calibration // (should probably have a specific place to do that but this works) if (calibrate_) { - millisecs_t time = g_core->GetAppTimeMillisecs(); + millisecs_t time = g_core->AppTimeMillisecs(); // If we're doing 'aggressive' auto-recalibration we expand extents outward // but suck them inward a tiny bit too to account for jitter or random fluke @@ -545,7 +545,7 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { return; } - millisecs_t time = g_core->GetAppTimeMillisecs(); + millisecs_t time = g_core->AppTimeMillisecs(); SDL_Event e2; // Ignore analog-stick input while we're holding a hat switch or d-pad @@ -959,7 +959,7 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { && (e->jbutton.button != hold_position_button_) && (e->jbutton.button != back_button_)) { if (ui_only_ || e->jbutton.button == remote_enter_button_) { - millisecs_t current_time = g_core->GetAppTimeMillisecs(); + millisecs_t current_time = g_core->AppTimeMillisecs(); if (current_time - last_ui_only_print_time_ > 5000) { g_base->python->objs() .Get(BasePython::ObjID::kUIRemotePressCall) diff --git a/src/ballistica/base/input/device/touch_input.cc b/src/ballistica/base/input/device/touch_input.cc index b514bb154..1667a1774 100644 --- a/src/ballistica/base/input/device/touch_input.cc +++ b/src/ballistica/base/input/device/touch_input.cc @@ -93,7 +93,7 @@ TouchInput::TouchInput() { TouchInput::~TouchInput() = default; void TouchInput::UpdateButtons(bool new_touch) { - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); float spread_scaled_actions = kButtonSpread * base_controls_scale_ * controls_scale_actions_; float width = g_base->graphics->screen_virtual_width(); @@ -134,7 +134,7 @@ void TouchInput::UpdateButtons(bool new_touch) { closest_to_bomb = true; } if (buttons_touch_) { - last_buttons_touch_time_ = g_core->GetAppTimeMillisecs(); + last_buttons_touch_time_ = g_core->AppTimeMillisecs(); } // Handle swipe mode. diff --git a/src/ballistica/base/input/input.cc b/src/ballistica/base/input/input.cc index 2363588ee..461d586c3 100644 --- a/src/ballistica/base/input/input.cc +++ b/src/ballistica/base/input/input.cc @@ -158,7 +158,7 @@ void Input::AnnounceConnects_() { // For the first announcement just say "X controllers detected" and don't // have a sound. - if (first_print && g_core->GetAppTimeSeconds() < 3.0) { + if (first_print && g_core->AppTimeSeconds() < 3.0) { first_print = false; // If there's been several connected, just give a number. @@ -225,7 +225,7 @@ void Input::ShowStandardInputDeviceConnectedMessage_(InputDevice* j) { // On Android we never show messages for initial input-devices; we often // get large numbers of strange virtual devices that aren't actually // controllers so this is more confusing than helpful. - if (g_buildconfig.ostype_android() && g_core->GetAppTimeSeconds() < 3.0) { + if (g_buildconfig.ostype_android() && g_core->AppTimeSeconds() < 3.0) { return; } @@ -554,7 +554,7 @@ void Input::OnScreenSizeChange() { assert(g_base->InLogicThread()); } void Input::StepDisplayTime() { assert(g_base->InLogicThread()); - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); // If input has been locked an excessively long amount of time, unlock it. if (input_lock_count_temp_) { @@ -622,13 +622,13 @@ void Input::LockAllInput(bool permanent, const std::string& label) { } else { input_lock_count_temp_++; if (input_lock_count_temp_ == 1) { - last_input_temp_lock_time_ = g_core->GetAppTimeMillisecs(); + last_input_temp_lock_time_ = g_core->AppTimeMillisecs(); } input_lock_temp_labels_.push_back(label); recent_input_locks_unlocks_.push_back( "temp lock: " + label + " time " - + std::to_string(g_core->GetAppTimeMillisecs())); + + std::to_string(g_core->AppTimeMillisecs())); while (recent_input_locks_unlocks_.size() > 10) { recent_input_locks_unlocks_.pop_front(); } @@ -641,7 +641,7 @@ void Input::UnlockAllInput(bool permanent, const std::string& label) { recent_input_locks_unlocks_.push_back( permanent ? "permanent unlock: " : "temp unlock: " + label + " time " - + std::to_string(g_core->GetAppTimeMillisecs())); + + std::to_string(g_core->AppTimeMillisecs())); while (recent_input_locks_unlocks_.size() > 10) { recent_input_locks_unlocks_.pop_front(); } @@ -667,7 +667,7 @@ void Input::UnlockAllInput(bool permanent, const std::string& label) { if (input_lock_count_temp_ < 0) { g_core->Log(LogName::kBaInput, LogLevel::kWarning, "temp input unlock at time " - + std::to_string(g_core->GetAppTimeMillisecs()) + + std::to_string(g_core->AppTimeMillisecs()) + " with no active lock: '" + label + "'"); // This is to be expected since we can reset this to 0. input_lock_count_temp_ = 0; @@ -684,7 +684,7 @@ void Input::UnlockAllInput(bool permanent, const std::string& label) { void Input::PrintLockLabels_() { std::string s = "INPUT LOCK REPORT (time=" - + std::to_string(g_core->GetAppTimeMillisecs()) + "):"; + + std::to_string(g_core->AppTimeMillisecs()) + "):"; int num; s += "\n " + std::to_string(input_lock_temp_labels_.size()) + " TEMP LOCKS:"; @@ -926,7 +926,7 @@ void Input::HandleKeyPress_(const SDL_Keysym& keysym) { // fluke repeat key press event due to funky OS circumstances. static int count{}; static seconds_t last_count_reset_time{}; - auto now = g_core->GetAppTimeSeconds(); + auto now = g_core->AppTimeSeconds(); if (now - last_count_reset_time > 2.0) { count = 0; last_count_reset_time = now; @@ -1238,7 +1238,7 @@ void Input::HandleSmoothMouseScroll_(const Vector2f& velocity, bool momentum) { WidgetMessage(WidgetMessage::Type::kMouseWheelVelocityH, nullptr, cursor_pos_x_, cursor_pos_y_, velocity.x, momentum)); - last_mouse_move_time_ = g_core->GetAppTimeSeconds(); + last_mouse_move_time_ = g_core->AppTimeSeconds(); mouse_move_count_++; Camera* camera = g_base->graphics->camera(); @@ -1282,7 +1282,7 @@ void Input::HandleMouseMotion_(const Vector2f& position) { cursor_pos_y_ = g_base->graphics->PixelToVirtualY( position.y * g_base->graphics->screen_pixel_height()); - last_mouse_move_time_ = g_core->GetAppTimeSeconds(); + last_mouse_move_time_ = g_core->AppTimeSeconds(); mouse_move_count_++; // If we have a touch-input in editing mode, pass along events to it. (it @@ -1324,7 +1324,7 @@ void Input::HandleMouseDown_(int button, const Vector2f& position) { return; } - last_mouse_move_time_ = g_core->GetAppTimeSeconds(); + last_mouse_move_time_ = g_core->AppTimeSeconds(); mouse_move_count_++; // Convert normalized view coords to our virtual ones. @@ -1333,7 +1333,7 @@ void Input::HandleMouseDown_(int button, const Vector2f& position) { cursor_pos_y_ = g_base->graphics->PixelToVirtualY( position.y * g_base->graphics->screen_pixel_height()); - millisecs_t click_time = g_core->GetAppTimeMillisecs(); + millisecs_t click_time = g_core->AppTimeMillisecs(); bool double_click = (click_time - last_click_time_ <= double_click_time_); last_click_time_ = click_time; @@ -1528,7 +1528,7 @@ auto Input::IsCursorVisible() const -> bool { bool val; // Show our cursor only if its been moved recently. - val = (g_core->GetAppTimeSeconds() - last_mouse_move_time_ < 2.071); + val = (g_core->AppTimeSeconds() - last_mouse_move_time_ < 2.071); return val; } diff --git a/src/ballistica/base/input/support/remote_app_server.cc b/src/ballistica/base/input/support/remote_app_server.cc index 856bf59ff..809fc99a5 100644 --- a/src/ballistica/base/input/support/remote_app_server.cc +++ b/src/ballistica/base/input/support/remote_app_server.cc @@ -221,7 +221,7 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, RemoteAppClient* client = clients_ + joystick_id; // Take note that we heard from them. - client->last_contact_time = g_core->GetAppTimeMillisecs(); + client->last_contact_time = g_core->AppTimeMillisecs(); // Ok now iterate. uint8_t* val = buffer + 4; @@ -389,7 +389,7 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, } // Don't reuse a slot for 5 seconds (if its been heard from since this time). - millisecs_t cooldown_time = g_core->GetAppTimeMillisecs() - 5000; + millisecs_t cooldown_time = g_core->AppTimeMillisecs() - 5000; // Ok, not there already.. now look for a non-taken one and return that. for (int i = 0; i < kMaxRemoteAppClients; i++) { @@ -412,7 +412,7 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, strcpy(clients_[i].display_name, clients_[i].name); // NOLINT char* c = strchr(clients_[i].display_name, '#'); if (c) *c = 0; - clients_[i].last_contact_time = g_core->GetAppTimeMillisecs(); + clients_[i].last_contact_time = g_core->AppTimeMillisecs(); clients_[i].request_id = request_id; char m[256]; diff --git a/src/ballistica/base/logic/logic.cc b/src/ballistica/base/logic/logic.cc index 76e4f106d..7b4ddc3f5 100644 --- a/src/ballistica/base/logic/logic.cc +++ b/src/ballistica/base/logic/logic.cc @@ -236,7 +236,7 @@ void Logic::OnAppShutdown() { assert(shutting_down_); // Nuke the app from orbit if we get stuck while shutting down. - g_core->StartSuicideTimer("shutdown", 10000); + g_core->StartSuicideTimer("shutdown", 15000); // Tell base to disallow shutdown-suppressors from here on out. g_base->ShutdownSuppressDisallow(); @@ -402,7 +402,7 @@ void Logic::UpdateDisplayTimeForHeadlessMode_() { // scheduled (or at least close enough so we can fudge it and tell them // its that exact time). - auto app_time_microsecs = g_core->GetAppTimeMicrosecs(); + auto app_time_microsecs = g_core->AppTimeMicrosecs(); // Set our int based time vals so we can exactly hit timers. auto old_display_time_microsecs = display_time_microsecs_; @@ -438,7 +438,7 @@ void Logic::PostUpdateDisplayTimeForHeadlessMode_() { [headless_display_step_microsecs] { auto sleepsecs = static_cast(headless_display_step_microsecs) / 1000000.0; - auto apptimesecs = g_core->GetAppTimeSeconds(); + auto apptimesecs = g_core->AppTimeSeconds(); char buffer[256]; snprintf(buffer, sizeof(buffer), "will try to sleep for %.4f at app-time %.4f (until %.4f)", @@ -467,7 +467,7 @@ void Logic::UpdateDisplayTimeForFrameDraw_() { // - 'current' should mostly show '(avg)'; rarely '(sample)'. // - these can vary briefly during load spikes/etc. but should quickly // reconverge to stability. If not, this may need further calibration. - auto current_app_time = g_core->GetAppTimeSeconds(); + auto current_app_time = g_core->AppTimeSeconds(); // We handle the first measurement specially. if (last_display_time_update_app_time_ < 0) { diff --git a/src/ballistica/base/python/methods/python_methods_base_1.cc b/src/ballistica/base/python/methods/python_methods_base_1.cc index a7a923f2b..b2821b169 100644 --- a/src/ballistica/base/python/methods/python_methods_base_1.cc +++ b/src/ballistica/base/python/methods/python_methods_base_1.cc @@ -330,8 +330,8 @@ static auto PyAppTime(PyObject* self, PyObject* args, PyObject* keywds) const_cast(kwlist))) { return nullptr; } - return PyFloat_FromDouble( - 0.001 * static_cast(g_core->GetAppTimeMillisecs())); + return PyFloat_FromDouble(0.001 + * static_cast(g_core->AppTimeMillisecs())); BA_PYTHON_CATCH; } diff --git a/src/ballistica/base/python/methods/python_methods_base_2.cc b/src/ballistica/base/python/methods/python_methods_base_2.cc index e235e954c..96737c069 100644 --- a/src/ballistica/base/python/methods/python_methods_base_2.cc +++ b/src/ballistica/base/python/methods/python_methods_base_2.cc @@ -497,7 +497,7 @@ static auto PyEvaluateLstr(PyObject* self, PyObject* args, PyObject* keywds) return nullptr; } return PyUnicode_FromString( - g_base->assets->CompileResourceString(value, "evaluate_lstr").c_str()); + g_base->assets->CompileResourceString(value).c_str()); BA_PYTHON_CATCH; } @@ -533,7 +533,7 @@ static auto PyGetStringHeight(PyObject* self, PyObject* args, PyObject* keywds) } s = g_base->python->GetPyLString(s_obj); #if BA_DEBUG_BUILD - if (g_base->assets->CompileResourceString(s, "get_string_height test") != s) { + if (g_base->assets->CompileResourceString(s) != s) { BA_LOG_PYTHON_TRACE( "resource-string passed to get_string_height; this should be avoided"); } @@ -579,8 +579,7 @@ static auto PyGetStringWidth(PyObject* self, PyObject* args, PyObject* keywds) } s = g_base->python->GetPyLString(s_obj); #if BA_DEBUG_BUILD - if (g_base->assets->CompileResourceString(s, "get_string_width debug test") - != s) { + if (g_base->assets->CompileResourceString(s) != s) { BA_LOG_PYTHON_TRACE( "resource-string passed to get_string_width; this should be avoided"); } diff --git a/src/ballistica/base/support/classic_soft.h b/src/ballistica/base/support/classic_soft.h index 5e388e93e..45811a01b 100644 --- a/src/ballistica/base/support/classic_soft.h +++ b/src/ballistica/base/support/classic_soft.h @@ -7,6 +7,7 @@ #include #include "ballistica/base/base.h" +#include "ballistica/shared/math/vector3f.h" namespace ballistica::base { @@ -46,6 +47,11 @@ class ClassicSoftInterface { virtual auto GetV1AccountTypeIconString(int account_type) -> std::string = 0; virtual auto V1AccountTypeToString(int account_type) -> std::string = 0; virtual void PlayMusic(const std::string& music_type, bool continuous) = 0; + virtual void GetClassicChestDisplayInfo(const std::string& id, + std::string* texclosed, + std::string* texclosedtint, + Vector3f* color, Vector3f* tint, + Vector3f* tint2) = 0; }; } // namespace ballistica::base diff --git a/src/ballistica/base/support/context.h b/src/ballistica/base/support/context.h index ac1133bbe..118decc57 100644 --- a/src/ballistica/base/support/context.h +++ b/src/ballistica/base/support/context.h @@ -15,6 +15,7 @@ namespace ballistica::base { // other mechanisms are set up to preserve and restore context before // running, and objects can also be invalidated or otherwise cleaned up // when the context they were created under dies. +// // The end goal of all this is to support api styles for end users where // standalone snippets of code can be useful; ie: something like // bs.newnode() to create something meaningful without having to worry @@ -36,26 +37,27 @@ class ContextRef { ContextRef(); explicit ContextRef(Context* sgc); - /// ContextRefs are considered equal if both are pointing to the exact same - /// Context object (or both are pointing to no Context). + /// ContextRefs are considered equal if both are pointing to the exact + /// same Context object (or both are pointing to no Context). auto operator==(const ContextRef& other) const -> bool; template auto GetContextTyped() const -> T* { // Ew; dynamic cast. - // Note: if it ever seems like speed is an issue here, we can - // cache the results with std::type_index entries. There should - // generally be a very small number of types involved. + // + // Note: if it ever seems like speed is an issue here, we can cache the + // results with std::type_index entries. There should generally be a + // very small number of types involved. return dynamic_cast(target_.get()); } - /// An empty context-ref was explicitly set to an empty state. - /// Note that this is different than an expired context-ref, which - /// originally pointed to some context that has since died. + /// An empty context-ref was explicitly set to an empty state. Note that + /// this is different than an expired context-ref, which originally + /// pointed to some context that has since died. auto IsEmpty() const { return empty_; } - /// Has this context died since it was set? - /// Note that a context created as empty is not considered expired. + /// Has this context died since it was set? Note that a context created as + /// empty is not considered expired. auto IsExpired() const -> bool { if (empty_) { return false; // Can't kill what was never alive. @@ -64,7 +66,8 @@ class ContextRef { } /// Return the context this ref points to. This will be nullptr for empty - /// contexts. Throws an exception if a target context was set but has expired. + /// contexts. Throws an exception if a target context was set but has + /// expired. auto Get() const -> Context* { auto* target = target_.get(); if (target == nullptr && !empty_) { @@ -84,14 +87,13 @@ class ContextRef { bool empty_; }; -/// Object containing the actual context_ref data/information. -/// App-modes can subclass this to provide the actual context_ref they desire, -/// and then code can use CurrentTyped() to safely retrieve context_ref as that -/// type. +/// Object containing the actual context_ref data/information. App-modes can +/// subclass this to provide the actual context_ref they desire, and then +/// code can use CurrentTyped() to safely retrieve context_ref as that type. class Context : public Object { public: - /// Return the current context_ref cast to a desired type. - /// Throws an Exception if the context_ref is unset or is another type. + /// Return the current context_ref cast to a desired type. Throws an + /// Exception if the context_ref is unset or is another type. template static auto CurrentTyped() -> T& { T* t = g_base->CurrentContext().GetContextTyped(); @@ -102,13 +104,13 @@ class Context : public Object { return *t; } - /// Called when a PythonContextCall is created in this context_ref. - /// The context_ref class may want to store a weak-reference to the - /// call and inform the call when the context_ref is going down so that - /// resources may be freed. Other permanent contexts may not need to - /// bother. - /// FIXME: This mechanism can probably be generalized so that other - /// things such as assets and timers can use it. + /// Called when a PythonContextCall is created in this context_ref. The + /// context_ref class may want to store a weak-reference to the call and + /// inform the call when the context_ref is going down so that resources + /// may be freed. Other permanent contexts may not need to bother. + /// + /// FIXME: This mechanism can probably be generalized so that other things + /// such as assets and timers can use it. virtual void RegisterContextCall(PythonContextCall* call); /// Return a short description of the context_ref; will be used when @@ -118,9 +120,9 @@ class Context : public Object { /// Return whether this context should allow default timer-types to be /// created within it (AppTimer, DisplayTimer). Scene type contexts - /// generally have their own timer types which are better integrated - /// with scenes (responding to changes in game speed/etc.) so this can - /// be used to encourage/enforce usage of those timers. + /// generally have their own timer types which are better integrated with + /// scenes (responding to changes in game speed/etc.) so this can be used + /// to encourage/enforce usage of those timers. virtual auto ContextAllowsDefaultTimerTypes() -> bool; }; diff --git a/src/ballistica/base/ui/dev_console.cc b/src/ballistica/base/ui/dev_console.cc index e3603ea4e..50aa2df2c 100644 --- a/src/ballistica/base/ui/dev_console.cc +++ b/src/ballistica/base/ui/dev_console.cc @@ -1683,7 +1683,7 @@ auto DevConsole::PasteFromClipboard() -> bool { } void DevConsole::UpdateCarat_() { - last_carat_x_change_time_ = g_core->GetAppTimeMillisecs(); + last_carat_x_change_time_ = g_core->AppTimeMillisecs(); auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjfwef"); auto unichars_clamped = unichars; diff --git a/src/ballistica/base/ui/ui.cc b/src/ballistica/base/ui/ui.cc index 749351cc7..f6121078b 100644 --- a/src/ballistica/base/ui/ui.cc +++ b/src/ballistica/base/ui/ui.cc @@ -370,7 +370,7 @@ void UI::SetUIInputDevice(InputDevice* input_device) { ui_input_device_ = input_device; // So they dont get stolen from immediately. - last_input_device_use_time_ = g_core->GetAppTimeMillisecs(); + last_input_device_use_time_ = g_core->AppTimeMillisecs(); } void UI::Reset() { @@ -432,7 +432,7 @@ auto UI::GetWidgetForInput(InputDevice* input_device) -> ui_v1::Widget* { return nullptr; } - millisecs_t time = g_core->GetAppTimeMillisecs(); + millisecs_t time = g_core->AppTimeMillisecs(); bool print_menu_owner{}; ui_v1::Widget* ret_val; diff --git a/src/ballistica/classic/classic.cc b/src/ballistica/classic/classic.cc index e011a655e..f4cf91bd6 100644 --- a/src/ballistica/classic/classic.cc +++ b/src/ballistica/classic/classic.cc @@ -250,4 +250,11 @@ void ClassicFeatureSet::PlayMusic(const std::string& music_type, python->PlayMusic(music_type, continuous); } +void ClassicFeatureSet::GetClassicChestDisplayInfo( + const std::string& id, std::string* texclosed, std::string* texclosedtint, + Vector3f* color, Vector3f* tint, Vector3f* tint2) { + python->GetClassicChestDisplayInfo(id, texclosed, texclosedtint, color, tint, + tint2); +} + } // namespace ballistica::classic diff --git a/src/ballistica/classic/classic.h b/src/ballistica/classic/classic.h index 2ae85657f..943235064 100644 --- a/src/ballistica/classic/classic.h +++ b/src/ballistica/classic/classic.h @@ -108,6 +108,9 @@ class ClassicFeatureSet : public FeatureSetNativeComponent, auto GetV1AccountTypeIconString(int account_type) -> std::string override; auto V1AccountTypeToString(int account_type) -> std::string override; auto GetV1AccountType() -> int override; + void GetClassicChestDisplayInfo(const std::string& id, std::string* texclosed, + std::string* texclosedtint, Vector3f* color, + Vector3f* tint, Vector3f* tint2) override; ClassicPython* const python; V1Account* const v1_account; diff --git a/src/ballistica/classic/python/classic_python.cc b/src/ballistica/classic/python/classic_python.cc index 4cc7a3b6d..47b36f657 100644 --- a/src/ballistica/classic/python/classic_python.cc +++ b/src/ballistica/classic/python/classic_python.cc @@ -4,6 +4,7 @@ #include +#include "ballistica/base/python/base_python.h" #include "ballistica/classic/python/methods/python_methods_classic.h" #include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/shared/python/python_command.h" // IWYU pragma: keep. @@ -30,6 +31,60 @@ extern "C" auto PyInit__baclassic() -> PyObject* { void ClassicPython::ImportPythonObjs() { #include "ballistica/classic/mgen/pyembed/binding_classic.inc" + + // Cache some basic display values for chests from the Python layer. This + // way C++ UI stuff doesn't have to call out to Python when drawing the + // root UI/etc. + + // Pull default chest display info. + chest_display_default_ = {ChestDisplayFromPython( + objs().Get(ObjID::kChestAppearanceDisplayInfoDefault))}; + + // And overrides. + for (auto&& item : + objs().Get(ObjID::kChestAppearanceDisplayInfos).DictItems()) { + chest_displays_[item.first.GetAttr("value").ValueAsString()] = + ChestDisplayFromPython(item.second); + } +} + +auto ClassicPython::ChestDisplayFromPython(const PythonRef& ref) + -> ChestDisplay_ { + ChestDisplay_ out; + + out.texclosed = ref.GetAttr("texclosed").ValueAsString().c_str(); + out.texclosedtint = ref.GetAttr("texclosedtint").ValueAsString().c_str(); + out.color = base::BasePython::GetPyVector3f(ref.GetAttr("color").get()); + out.tint = base::BasePython::GetPyVector3f(ref.GetAttr("tint").get()); + out.tint2 = base::BasePython::GetPyVector3f(ref.GetAttr("tint2").get()); + + return out; +} + +void ClassicPython::GetClassicChestDisplayInfo(const std::string& id, + std::string* texclosed, + std::string* texclosedtint, + Vector3f* color, Vector3f* tint, + Vector3f* tint2) { + assert(texclosed); + assert(texclosedtint); + assert(color); + assert(tint); + assert(tint2); + auto&& display{chest_displays_.find(id)}; + if (display != chest_displays_.end()) { + *texclosed = display->second.texclosed; + *texclosedtint = display->second.texclosedtint; + *color = display->second.color; + *tint = display->second.tint; + *tint2 = display->second.tint2; + } else { + *texclosed = chest_display_default_.texclosed; + *texclosedtint = chest_display_default_.texclosedtint; + *color = chest_display_default_.color; + *tint = chest_display_default_.tint; + *tint2 = chest_display_default_.tint2; + } } void ClassicPython::PlayMusic(const std::string& music_type, bool continuous) { diff --git a/src/ballistica/classic/python/classic_python.h b/src/ballistica/classic/python/classic_python.h index b02f885a7..47fe8c4b2 100644 --- a/src/ballistica/classic/python/classic_python.h +++ b/src/ballistica/classic/python/classic_python.h @@ -4,9 +4,11 @@ #define BALLISTICA_CLASSIC_PYTHON_CLASSIC_PYTHON_H_ #include +#include #include "ballistica/base/base.h" #include "ballistica/classic/classic.h" +#include "ballistica/shared/math/vector3f.h" #include "ballistica/shared/python/python_object_set.h" namespace ballistica::classic { @@ -20,6 +22,8 @@ class ClassicPython { enum class ObjID { kDoPlayMusicCall, kGetInputDeviceMappedValueCall, + kChestAppearanceDisplayInfoDefault, + kChestAppearanceDisplayInfos, kLast // Sentinel; must be at end. }; @@ -34,7 +38,22 @@ class ClassicPython { const auto& objs() { return objs_; } + void GetClassicChestDisplayInfo(const std::string& id, std::string* texclosed, + std::string* texclosedtint, Vector3f* color, + Vector3f* tint, Vector3f* tint2); + private: + struct ChestDisplay_ { + Vector3f color; + std::string texclosed; + std::string texclosedtint; + Vector3f tint; + Vector3f tint2; + }; + + auto ChestDisplayFromPython(const PythonRef& ref) -> ChestDisplay_; + ChestDisplay_ chest_display_default_; + std::unordered_map chest_displays_; PythonObjectSet objs_; }; diff --git a/src/ballistica/classic/python/methods/python_methods_classic.cc b/src/ballistica/classic/python/methods/python_methods_classic.cc index 2cc6d4785..1254ae541 100644 --- a/src/ballistica/classic/python/methods/python_methods_classic.cc +++ b/src/ballistica/classic/python/methods/python_methods_classic.cc @@ -3,7 +3,6 @@ #include "ballistica/classic/python/methods/python_methods_classic.h" #include -#include #include #include @@ -296,9 +295,9 @@ static auto PySetRootUIAccountValues(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - const char* tickets_text; - const char* tokens_text; - const char* league_rank_text; + int tickets; + int tokens; + int league_rank; const char* league_type; const char* achievements_percent_text; const char* level_text; @@ -308,19 +307,19 @@ static auto PySetRootUIAccountValues(PyObject* self, PyObject* args, const char* chest_1_appearance; const char* chest_2_appearance; const char* chest_3_appearance; - float chest_0_unlock_time; - float chest_1_unlock_time; - float chest_2_unlock_time; - float chest_3_unlock_time; - float chest_0_ad_allow_time; - float chest_1_ad_allow_time; - float chest_2_ad_allow_time; - float chest_3_ad_allow_time; + double chest_0_unlock_time; + double chest_1_unlock_time; + double chest_2_unlock_time; + double chest_3_unlock_time; + double chest_0_ad_allow_time; + double chest_1_ad_allow_time; + double chest_2_ad_allow_time; + double chest_3_ad_allow_time; int gold_pass{}; - static const char* kwlist[] = {"tickets_text", - "tokens_text", - "league_rank_text", + static const char* kwlist[] = {"tickets", + "tokens", + "league_rank", "league_type", "achievements_percent_text", "level_text", @@ -341,8 +340,8 @@ static auto PySetRootUIAccountValues(PyObject* self, PyObject* args, "chest_3_ad_allow_time", nullptr}; if (!PyArg_ParseTupleAndKeywords( - args, keywds, "sssssssspssssffffffff", const_cast(kwlist), - &tickets_text, &tokens_text, &league_rank_text, &league_type, + args, keywds, "iiissssspssssdddddddd", const_cast(kwlist), + &tickets, &tokens, &league_rank, &league_type, &achievements_percent_text, &level_text, &xp_text, &inbox_count_text, &gold_pass, &chest_0_appearance, &chest_1_appearance, &chest_2_appearance, &chest_3_appearance, &chest_0_unlock_time, @@ -357,20 +356,21 @@ static auto PySetRootUIAccountValues(PyObject* self, PyObject* args, // Pass these all along to the app-mode which will store them and forward // them to any existing UI. - appmode->SetRootUITicketsMeterText(tickets_text); - appmode->SetRootUITokensMeterText(tokens_text); - appmode->SetRootUILeagueRankText(league_rank_text); + appmode->SetRootUITicketsMeterValue(tickets); + appmode->SetRootUITokensMeterValue(tokens); + appmode->SetRootUILeagueRankValue(league_rank); appmode->SetRootUILeagueType(league_type); appmode->SetRootUIAchievementsPercentText(achievements_percent_text); appmode->SetRootUILevelText(level_text); appmode->SetRootUIXPText(xp_text); appmode->SetRootUIInboxCountText(inbox_count_text); appmode->SetRootUIGoldPass(gold_pass); - appmode->SetRootUIChests(chest_0_appearance, chest_1_appearance, - chest_2_appearance, chest_3_appearance); + appmode->SetRootUIChests( + chest_0_appearance, chest_1_appearance, chest_2_appearance, + chest_3_appearance, chest_0_unlock_time, chest_1_unlock_time, + chest_2_unlock_time, chest_3_unlock_time, chest_0_ad_allow_time, + chest_1_ad_allow_time, chest_2_ad_allow_time, chest_3_ad_allow_time); - printf("WOULD SET TIMES TO %.2f %.2f\n", chest_0_unlock_time, - chest_0_ad_allow_time); Py_RETURN_NONE; BA_PYTHON_CATCH; } @@ -381,9 +381,9 @@ static PyMethodDef PySetRootUIAccountValuesDef = { METH_VARARGS | METH_KEYWORDS, // flags "set_root_ui_account_values(*,\n" - " tickets_text: str,\n" - " tokens_text: str,\n" - " league_rank_text: str,\n" + " tickets: int,\n" + " tokens: int,\n" + " league_rank: int,\n" " league_type: str,\n" " achievements_percent_text: str,\n" " level_text: str,\n" diff --git a/src/ballistica/classic/support/classic_app_mode.cc b/src/ballistica/classic/support/classic_app_mode.cc index eb261a576..8c50e5eef 100644 --- a/src/ballistica/classic/support/classic_app_mode.cc +++ b/src/ballistica/classic/support/classic_app_mode.cc @@ -149,10 +149,10 @@ void ClassicAppMode::Reset_() { // At this point uiv1 is in a reset-to-default state. Now plug in our // current values for everything. if (auto* root_widget = uiv1_->root_widget()) { - root_widget->SetTicketsMeterText(root_ui_tickets_meter_text_); - root_widget->SetTokensMeterText(root_ui_tokens_meter_text_, - root_ui_gold_pass_); - root_widget->SetLeagueRankText(root_ui_league_rank_text_); + root_widget->SetTicketsMeterValue(root_ui_tickets_meter_value_); + root_widget->SetTokensMeterValue(root_ui_tokens_meter_value_, + root_ui_gold_pass_); + root_widget->SetLeagueRankValue(root_ui_league_rank_value_); root_widget->SetLeagueType(root_ui_league_type_); root_widget->SetAchievementPercentText(root_ui_achievement_percent_text_); root_widget->SetLevelText(root_ui_level_text_); @@ -160,7 +160,11 @@ void ClassicAppMode::Reset_() { root_widget->SetInboxCountText(root_ui_inbox_count_text_); root_widget->SetChests( root_ui_chest_0_appearance_, root_ui_chest_1_appearance_, - root_ui_chest_2_appearance_, root_ui_chest_3_appearance_); + root_ui_chest_2_appearance_, root_ui_chest_3_appearance_, + root_ui_chest_0_unlock_time_, root_ui_chest_1_unlock_time_, + root_ui_chest_2_unlock_time_, root_ui_chest_3_unlock_time_, + root_ui_chest_0_ad_allow_time_, root_ui_chest_1_ad_allow_time_, + root_ui_chest_2_ad_allow_time_, root_ui_chest_3_ad_allow_time_); root_widget->SetHaveLiveValues(root_ui_have_live_values_); } } @@ -332,7 +336,7 @@ void ClassicAppMode::HostScanCycle() { &((reinterpret_cast(&from))->sin_addr), buffer2, sizeof(buffer2)); entry.last_query_id = query_id; - entry.last_contact_time = g_core->GetAppTimeMillisecs(); + entry.last_contact_time = g_core->AppTimeMillisecs(); } } PruneScanResults_(); @@ -357,7 +361,7 @@ void ClassicAppMode::EndHostScanning() { } void ClassicAppMode::PruneScanResults_() { - millisecs_t t = g_core->GetAppTimeMillisecs(); + millisecs_t t = g_core->AppTimeMillisecs(); auto i = scan_results_.begin(); while (i != scan_results_.end()) { auto i_next = i; @@ -516,8 +520,8 @@ auto ClassicAppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t { void ClassicAppMode::StepDisplayTime() { assert(g_base->InLogicThread()); - auto startms{core::CorePlatform::GetCurrentMillisecs()}; - millisecs_t app_time = g_core->GetAppTimeMillisecs(); + auto startms{core::CorePlatform::TimeMonotonicMillisecs()}; + millisecs_t app_time = g_core->AppTimeMillisecs(); g_core->platform->SetDebugKey("LastUpdateTime", std::to_string(startms)); in_update_ = true; @@ -592,7 +596,7 @@ void ClassicAppMode::StepDisplayTime() { // Report excessively long updates. if (g_core->core_config().debug_timing && app_time >= next_long_update_report_time_) { - auto duration{core::CorePlatform::GetCurrentMillisecs() - startms}; + auto duration{core::CorePlatform::TimeMonotonicMillisecs() - startms}; // Complain when our full update takes longer than 1/60th second. if (duration > (1000 / 60)) { @@ -762,7 +766,7 @@ void ClassicAppMode::UpdateKickVote_() { kick_vote_in_progress_ = false; return; } - millisecs_t current_time{g_core->GetAppTimeMillisecs()}; + millisecs_t current_time{g_core->AppTimeMillisecs()}; int total_client_count = 0; int yes_votes = 0; int no_votes = 0; @@ -859,7 +863,7 @@ void ClassicAppMode::UpdateKickVote_() { void ClassicAppMode::StartKickVote(scene_v1::ConnectionToClient* starter, scene_v1::ConnectionToClient* target) { // Restrict votes per client. - millisecs_t current_time = g_core->GetAppTimeMillisecs(); + millisecs_t current_time = g_core->AppTimeMillisecs(); if (starter == target) { // Don't let anyone kick themselves. @@ -1413,7 +1417,7 @@ auto ClassicAppMode::ShouldAnnouncePartyJoinsAndLeaves() -> bool { } auto ClassicAppMode::IsPlayerBanned(const scene_v1::PlayerSpec& spec) -> bool { - millisecs_t current_time = g_core->GetAppTimeMillisecs(); + millisecs_t current_time = g_core->AppTimeMillisecs(); // Now is a good time to prune no-longer-banned specs. while (!banned_players_.empty() @@ -1431,7 +1435,7 @@ auto ClassicAppMode::IsPlayerBanned(const scene_v1::PlayerSpec& spec) -> bool { void ClassicAppMode::BanPlayer(const scene_v1::PlayerSpec& spec, millisecs_t duration) { - banned_players_.emplace_back(g_core->GetAppTimeMillisecs() + duration, spec); + banned_players_.emplace_back(g_core->AppTimeMillisecs() + duration, spec); } void ClassicAppMode::HandleQuitOnIdle_() { @@ -1550,51 +1554,51 @@ void ClassicAppMode::RunMainMenu() { } } -void ClassicAppMode::SetRootUITicketsMeterText(const std::string text) { +void ClassicAppMode::SetRootUITicketsMeterValue(int value) { BA_PRECONDITION(g_base->InLogicThread()); - if (text == root_ui_tickets_meter_text_) { + if (value == root_ui_tickets_meter_value_) { return; } // Store the value. - root_ui_tickets_meter_text_ = text; + root_ui_tickets_meter_value_ = value; // Apply it to any existing UI. if (uiv1_) { if (auto* root_widget = uiv1_->root_widget()) { - root_widget->SetTicketsMeterText(root_ui_tickets_meter_text_); + root_widget->SetTicketsMeterValue(root_ui_tickets_meter_value_); } } } -void ClassicAppMode::SetRootUITokensMeterText(const std::string text) { +void ClassicAppMode::SetRootUITokensMeterValue(int value) { BA_PRECONDITION(g_base->InLogicThread()); - if (text == root_ui_tokens_meter_text_) { + if (value == root_ui_tokens_meter_value_) { return; } // Store the value. - root_ui_tokens_meter_text_ = text; + root_ui_tokens_meter_value_ = value; // Apply it to any existing UI. if (uiv1_) { if (auto* root_widget = uiv1_->root_widget()) { - root_widget->SetTokensMeterText(root_ui_tokens_meter_text_, - root_ui_gold_pass_); + root_widget->SetTokensMeterValue(root_ui_tokens_meter_value_, + root_ui_gold_pass_); } } } -void ClassicAppMode::SetRootUILeagueRankText(const std::string text) { +void ClassicAppMode::SetRootUILeagueRankValue(int value) { BA_PRECONDITION(g_base->InLogicThread()); - if (text == root_ui_league_rank_text_) { + if (value == root_ui_league_rank_value_) { return; } // Store the value. - root_ui_league_rank_text_ = text; + root_ui_league_rank_value_ = value; // Apply it to any existing UI. if (uiv1_) { if (auto* root_widget = uiv1_->root_widget()) { - root_widget->SetLeagueRankText(root_ui_league_rank_text_); + root_widget->SetLeagueRankValue(root_ui_league_rank_value_); } } } @@ -1694,8 +1698,8 @@ void ClassicAppMode::SetRootUIGoldPass(bool enabled) { // Apply it to any existing UI. if (uiv1_) { if (auto* root_widget = uiv1_->root_widget()) { - root_widget->SetTokensMeterText(root_ui_tokens_meter_text_, - root_ui_gold_pass_); + root_widget->SetTokensMeterValue(root_ui_tokens_meter_value_, + root_ui_gold_pass_); } } } @@ -1716,15 +1720,28 @@ void ClassicAppMode::SetRootUIHaveLiveValues(bool have_live_values) { } } -void ClassicAppMode::SetRootUIChests(const std::string& chest_0_appearance, - const std::string& chest_1_appearance, - const std::string& chest_2_appearance, - const std::string& chest_3_appearance) { +void ClassicAppMode::SetRootUIChests( + const std::string& chest_0_appearance, + const std::string& chest_1_appearance, + const std::string& chest_2_appearance, + const std::string& chest_3_appearance, seconds_t chest_0_unlock_time, + seconds_t chest_1_unlock_time, seconds_t chest_2_unlock_time, + seconds_t chest_3_unlock_time, seconds_t chest_0_ad_allow_time, + seconds_t chest_1_ad_allow_time, seconds_t chest_2_ad_allow_time, + seconds_t chest_3_ad_allow_time) { BA_PRECONDITION(g_base->InLogicThread()); if (chest_0_appearance == root_ui_chest_0_appearance_ && chest_1_appearance == root_ui_chest_1_appearance_ && chest_2_appearance == root_ui_chest_2_appearance_ - && chest_3_appearance == root_ui_chest_3_appearance_) { + && chest_3_appearance == root_ui_chest_3_appearance_ + && chest_0_unlock_time == root_ui_chest_0_unlock_time_ + && chest_1_unlock_time == root_ui_chest_1_unlock_time_ + && chest_2_unlock_time == root_ui_chest_2_unlock_time_ + && chest_3_unlock_time == root_ui_chest_3_unlock_time_ + && chest_0_ad_allow_time == root_ui_chest_0_ad_allow_time_ + && chest_1_ad_allow_time == root_ui_chest_1_ad_allow_time_ + && chest_2_ad_allow_time == root_ui_chest_2_ad_allow_time_ + && chest_3_ad_allow_time == root_ui_chest_3_ad_allow_time_) { return; } @@ -1733,13 +1750,25 @@ void ClassicAppMode::SetRootUIChests(const std::string& chest_0_appearance, root_ui_chest_1_appearance_ = chest_1_appearance; root_ui_chest_2_appearance_ = chest_2_appearance; root_ui_chest_3_appearance_ = chest_3_appearance; + root_ui_chest_0_unlock_time_ = chest_0_unlock_time; + root_ui_chest_1_unlock_time_ = chest_1_unlock_time; + root_ui_chest_2_unlock_time_ = chest_2_unlock_time; + root_ui_chest_3_unlock_time_ = chest_3_unlock_time; + root_ui_chest_0_ad_allow_time_ = chest_0_ad_allow_time; + root_ui_chest_1_ad_allow_time_ = chest_1_ad_allow_time; + root_ui_chest_2_ad_allow_time_ = chest_2_ad_allow_time; + root_ui_chest_3_ad_allow_time_ = chest_3_ad_allow_time; // Apply it to any existing UI. if (uiv1_) { if (auto* root_widget = uiv1_->root_widget()) { root_widget->SetChests( root_ui_chest_0_appearance_, root_ui_chest_1_appearance_, - root_ui_chest_2_appearance_, root_ui_chest_3_appearance_); + root_ui_chest_2_appearance_, root_ui_chest_3_appearance_, + root_ui_chest_0_unlock_time_, root_ui_chest_1_unlock_time_, + root_ui_chest_2_unlock_time_, root_ui_chest_3_unlock_time_, + root_ui_chest_0_ad_allow_time_, root_ui_chest_1_ad_allow_time_, + root_ui_chest_2_ad_allow_time_, root_ui_chest_3_ad_allow_time_); } } } diff --git a/src/ballistica/classic/support/classic_app_mode.h b/src/ballistica/classic/support/classic_app_mode.h index edfaafe08..153a57e28 100644 --- a/src/ballistica/classic/support/classic_app_mode.h +++ b/src/ballistica/classic/support/classic_app_mode.h @@ -215,19 +215,24 @@ class ClassicAppMode : public base::AppMode { public_party_public_address_ipv6_ = val; } - void SetRootUITicketsMeterText(const std::string text); - void SetRootUITokensMeterText(const std::string text); - void SetRootUILeagueRankText(const std::string text); + void SetRootUITicketsMeterValue(int value); + void SetRootUITokensMeterValue(int value); + void SetRootUILeagueRankValue(int value); void SetRootUILeagueType(const std::string text); void SetRootUIAchievementsPercentText(const std::string text); void SetRootUILevelText(const std::string text); void SetRootUIXPText(const std::string text); void SetRootUIInboxCountText(const std::string text); void SetRootUIGoldPass(bool enabled); - void SetRootUIChests(const std::string& chest_0_appearance, - const std::string& chest_1_appearance, - const std::string& chest_2_appearance, - const std::string& chest_3_appearance); + void SetRootUIChests( + const std::string& chest_0_appearance, + const std::string& chest_1_appearance, + const std::string& chest_2_appearance, + const std::string& chest_3_appearance, seconds_t chest_0_unlock_time, + seconds_t chest_1_unlock_time, seconds_t chest_2_unlock_time, + seconds_t chest_3_unlock_time, seconds_t chest_0_ad_allow_time, + seconds_t chest_1_ad_allow_time, seconds_t chest_2_ad_allow_time, + seconds_t chest_3_ad_allow_time); void SetRootUIHaveLiveValues(bool val); private: @@ -250,6 +255,15 @@ class ClassicAppMode : public base::AppMode { std::string root_ui_chest_1_appearance_; std::string root_ui_chest_2_appearance_; std::string root_ui_chest_3_appearance_; + seconds_t root_ui_chest_0_unlock_time_; + seconds_t root_ui_chest_1_unlock_time_; + seconds_t root_ui_chest_2_unlock_time_; + seconds_t root_ui_chest_3_unlock_time_; + seconds_t root_ui_chest_0_ad_allow_time_; + seconds_t root_ui_chest_1_ad_allow_time_; + seconds_t root_ui_chest_2_ad_allow_time_; + seconds_t root_ui_chest_3_ad_allow_time_; + uint32_t next_scan_query_id_{}; int scan_socket_{-1}; int host_protocol_version_{-1}; @@ -301,6 +315,9 @@ class ClassicAppMode : public base::AppMode { int public_party_max_size_{8}; int public_party_player_count_{0}; int public_party_max_player_count_{8}; + int root_ui_tickets_meter_value_; + int root_ui_tokens_meter_value_; + int root_ui_league_rank_value_; float debug_speed_mult_{1.0f}; float replay_speed_mult_{1.0f}; std::set admin_public_ids_; @@ -308,9 +325,6 @@ class ClassicAppMode : public base::AppMode { std::string public_party_name_; std::string public_party_min_league_; std::string public_party_stats_url_; - std::string root_ui_tickets_meter_text_; - std::string root_ui_tokens_meter_text_; - std::string root_ui_league_rank_text_; std::string root_ui_league_type_; std::string root_ui_achievement_percent_text_; std::string root_ui_level_text_; diff --git a/src/ballistica/classic/support/stress_test.cc b/src/ballistica/classic/support/stress_test.cc index 5441578dc..e49b29b4b 100644 --- a/src/ballistica/classic/support/stress_test.cc +++ b/src/ballistica/classic/support/stress_test.cc @@ -53,7 +53,7 @@ void StressTest::ProcessInputs(int player_count) { assert(g_base->InLogicThread()); assert(player_count >= 0); - millisecs_t time = g_core->GetAppTimeMillisecs(); + millisecs_t time = g_core->AppTimeMillisecs(); // FIXME: If we don't check for stress_test_last_leave_time_ we totally // confuse the game.. need to be able to survive that. diff --git a/src/ballistica/core/core.cc b/src/ballistica/core/core.cc index d8b41e5de..55b3764fe 100644 --- a/src/ballistica/core/core.cc +++ b/src/ballistica/core/core.cc @@ -72,7 +72,7 @@ auto CoreFeatureSet::Import(const CoreConfig* config) -> CoreFeatureSet* { } void CoreFeatureSet::DoImport_(const CoreConfig& config) { - millisecs_t start_millisecs = CorePlatform::GetCurrentMillisecs(); + millisecs_t start_millisecs = CorePlatform::TimeMonotonicMillisecs(); assert(g_core == nullptr); g_core = new CoreFeatureSet(config); @@ -87,7 +87,7 @@ CoreFeatureSet::CoreFeatureSet(CoreConfig config) python{new CorePython()}, platform{CorePlatform::Create()}, core_config_{std::move(config)}, - last_app_time_measure_microsecs_{CorePlatform::GetCurrentMicrosecs()}, + last_app_time_measure_microsecs_{CorePlatform::TimeMonotonicMicrosecs()}, vr_mode_{config.vr_mode} { // We're a singleton. If there's already one of us, something's wrong. assert(g_core == nullptr); @@ -313,7 +313,7 @@ auto CoreFeatureSet::SoftImportBase() -> BaseSoftInterface* { // // We include time-since-start as part of the message here. // char buffer[128]; // snprintf(buffer, sizeof(buffer), "%s @ %.3fs.", msg, -// g_core->GetAppTimeSeconds() + offset_seconds); +// g_core->AppTimeSeconds() + offset_seconds); // Log(LogName::kBaLifecycle, LogLevel::kDebug, buffer); // } else { // Log(LogName::kBaLifecycle, LogLevel::kDebug, msg); @@ -355,23 +355,23 @@ static void WaitThenDie(millisecs_t wait, const std::string& action) { FatalError("Timed out waiting for " + action + "."); } -auto CoreFeatureSet::GetAppTimeMillisecs() -> millisecs_t { +auto CoreFeatureSet::AppTimeMillisecs() -> millisecs_t { UpdateAppTime_(); return app_time_microsecs_ / 1000; } -auto CoreFeatureSet::GetAppTimeMicrosecs() -> microsecs_t { +auto CoreFeatureSet::AppTimeMicrosecs() -> microsecs_t { UpdateAppTime_(); return app_time_microsecs_; } -auto CoreFeatureSet::GetAppTimeSeconds() -> seconds_t { +auto CoreFeatureSet::AppTimeSeconds() -> seconds_t { UpdateAppTime_(); return static_cast(app_time_microsecs_) / 1000000; } void CoreFeatureSet::UpdateAppTime_() { - microsecs_t t = CorePlatform::GetCurrentMicrosecs(); + microsecs_t t = CorePlatform::TimeMonotonicMicrosecs(); // If we're at a different time than our last query, do our funky math. if (t != last_app_time_measure_microsecs_) { diff --git a/src/ballistica/core/core.h b/src/ballistica/core/core.h index 7ba9e3f20..c66d81d5c 100644 --- a/src/ballistica/core/core.h +++ b/src/ballistica/core/core.h @@ -73,21 +73,21 @@ class CoreFeatureSet { /// App-time is basically the total time that the engine has been actively /// running. (The 'App' here is a slight misnomer). It will stop /// progressing while the app is suspended and will never go backwards. - auto GetAppTimeMillisecs() -> millisecs_t; + auto AppTimeMillisecs() -> millisecs_t; /// Return current app-time in microseconds. /// /// App-time is basically the total time that the engine has been actively /// running. (The 'App' here is a slight misnomer). It will stop /// progressing while the app is suspended and will never go backwards. - auto GetAppTimeMicrosecs() -> microsecs_t; + auto AppTimeMicrosecs() -> microsecs_t; /// Return current app-time in seconds. /// /// App-time is basically the total time that the engine has been actively /// running. (The 'App' here is a slight misnomer). It will stop /// progressing while the app is suspended and will never go backwards. - auto GetAppTimeSeconds() -> seconds_t; + auto AppTimeSeconds() -> seconds_t; /// Are we in the 'main' thread? The thread that first inited Core is /// considered the 'main' thread; on most platforms it is the one where diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc index abb58fe7b..d58de8a6d 100644 --- a/src/ballistica/core/platform/core_platform.cc +++ b/src/ballistica/core/platform/core_platform.cc @@ -116,7 +116,8 @@ void CorePlatform::LowLevelDebugLog(const std::string& msg) { HandleLowLevelDebugLog(msg); } -CorePlatform::CorePlatform() : start_time_millisecs_(GetCurrentMillisecs()) {} +CorePlatform::CorePlatform() + : start_time_millisecs_(TimeMonotonicMillisecs()) {} void CorePlatform::PostInit() { // Hmm; we seem to get some funky invalid utf8 out of @@ -960,8 +961,8 @@ auto CorePlatform::SetSocketNonBlocking(int sd) -> bool { #endif } -auto CorePlatform::GetTicks() const -> millisecs_t { - return GetCurrentMillisecs() - start_time_millisecs_; +auto CorePlatform::TimeSinceLaunchMillisecs() const -> millisecs_t { + return TimeMonotonicMillisecs() - start_time_millisecs_; } auto CorePlatform::GetPlatformName() -> std::string { @@ -1072,27 +1073,27 @@ void CorePlatform::SetDebugKey(const std::string& key, void CorePlatform::HandleLowLevelDebugLog(const std::string& msg) {} -auto CorePlatform::GetCurrentMillisecs() -> millisecs_t { +auto CorePlatform::TimeMonotonicMillisecs() -> millisecs_t { return std::chrono::time_point_cast( std::chrono::steady_clock::now()) .time_since_epoch() .count(); } -auto CorePlatform::GetCurrentMicrosecs() -> millisecs_t { +auto CorePlatform::TimeMonotonicMicrosecs() -> millisecs_t { return std::chrono::time_point_cast( std::chrono::steady_clock::now()) .time_since_epoch() .count(); } -auto CorePlatform::GetSecondsSinceEpoch() -> double { +auto CorePlatform::TimeSinceEpochSeconds() -> double { return std::chrono::duration( std::chrono::system_clock::now().time_since_epoch()) .count(); } -auto CorePlatform::GetCurrentWholeSeconds() -> int64_t { +auto CorePlatform::TimeMonotonicWholeSeconds() -> int64_t { return std::chrono::time_point_cast( std::chrono::steady_clock::now()) .time_since_epoch() diff --git a/src/ballistica/core/platform/core_platform.h b/src/ballistica/core/platform/core_platform.h index a1b2f5f5e..51b8b1d06 100644 --- a/src/ballistica/core/platform/core_platform.h +++ b/src/ballistica/core/platform/core_platform.h @@ -346,31 +346,31 @@ class CorePlatform { /// monotonic. For most purposes, AppTime values are preferable since /// their progression pauses during app suspension and they are 100% /// guaranteed to not go backwards. - auto GetTicks() const -> millisecs_t; + auto TimeSinceLaunchMillisecs() const -> millisecs_t; /// Return a raw current milliseconds value. It *should* be monotonic. It /// is relative to an undefined start point; only use it for time /// differences. Generally the AppTime values are preferable since their /// progression pauses during app suspension and they are 100% guaranteed /// to not go backwards. - static auto GetCurrentMillisecs() -> millisecs_t; + static auto TimeMonotonicMillisecs() -> millisecs_t; /// Return a raw current microseconds value. It *should* be monotonic. It /// is relative to an undefined start point; only use it for time /// differences. Generally the AppTime values are preferable since their /// progression pauses during app suspension and they are 100% guaranteed /// to not go backwards. - static auto GetCurrentMicrosecs() -> microsecs_t; + static auto TimeMonotonicMicrosecs() -> microsecs_t; /// Return a raw current seconds integer value. It *should* be monotonic. /// It is relative to an undefined start point; only use it for time /// differences. Generally the AppTime values are preferable since their /// progression pauses during app suspension and they are 100% guaranteed /// to not go backwards. - static auto GetCurrentWholeSeconds() -> int64_t; + static auto TimeMonotonicWholeSeconds() -> int64_t; /// Return seconds since the epoch; same as Python's time.time(). - static auto GetSecondsSinceEpoch() -> double; + static auto TimeSinceEpochSeconds() -> double; static void SleepSeconds(seconds_t duration); static void SleepMillisecs(millisecs_t duration); diff --git a/src/ballistica/core/support/core_config.h b/src/ballistica/core/support/core_config.h index 1ccfef092..d7b930302 100644 --- a/src/ballistica/core/support/core_config.h +++ b/src/ballistica/core/support/core_config.h @@ -10,8 +10,8 @@ namespace ballistica::core { -/// Collection of low level options for a run of the engine; passed -/// when initing the core feature-set. +/// A collection of low level options for a run of the engine; passed when +/// initing the core feature-set. class CoreConfig { public: static auto ForArgsAndEnvVars(int argc, char** argv) -> CoreConfig; diff --git a/src/ballistica/scene_v1/connection/connection.cc b/src/ballistica/scene_v1/connection/connection.cc index 54863869b..8de0859cf 100644 --- a/src/ballistica/scene_v1/connection/connection.cc +++ b/src/ballistica/scene_v1/connection/connection.cc @@ -34,7 +34,7 @@ const int kPingMeasureInterval = 2000; Connection::Connection() { // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) - creation_time_ = last_average_update_time_ = g_core->GetAppTimeMillisecs(); + creation_time_ = last_average_update_time_ = g_core->AppTimeMillisecs(); } void Connection::ProcessWaitingMessages() { @@ -181,13 +181,13 @@ void Connection::HandleGamePacket(const std::vector& data) { "Error: got invalid BA_SCENEPACKET_KEEPALIVE packet."); return; } - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); HandleResends(real_time, data, 1); break; } case BA_SCENEPACKET_MESSAGE: { - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); // Expect 1 byte type, 2 byte num, 3 byte acks, at least 1 byte payload. if (data.size() < 7) { @@ -211,7 +211,7 @@ void Connection::HandleGamePacket(const std::vector& data) { ReliableMessageIn& msg(in_messages_[num]); msg.data.resize(data.size() - 6); memcpy(&(msg.data[0]), &(data[6]), msg.data.size()); - msg.arrival_time = g_core->GetAppTimeMillisecs(); + msg.arrival_time = g_core->AppTimeMillisecs(); // Now run all in-order packets we've got. ProcessWaitingMessages(); @@ -308,7 +308,7 @@ void Connection::SendReliableMessage(const std::vector& data) { assert(out_messages_.find(num) == out_messages_.end()); ReliableMessageOut& msg(out_messages_[num]); - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); msg.data = data; msg.first_send_time = msg.last_send_time = real_time; @@ -341,7 +341,7 @@ void Connection::SendUnreliableMessage(const std::vector& data) { } uint16_t num = next_out_unreliable_message_num_++; - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); // Add our header/acks and go ahead and send this one out. // 1 byte for type, 2 for packet-num, 2 for unreliable packet-num, 3 for acks. @@ -367,7 +367,7 @@ void Connection::SendJMessage(cJSON* val) { } void Connection::Update() { - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); // Update our averages once per second. while (real_time - last_average_update_time_ > 1000) { diff --git a/src/ballistica/scene_v1/connection/connection_to_client.cc b/src/ballistica/scene_v1/connection/connection_to_client.cc index 64682c835..85dfda968 100644 --- a/src/ballistica/scene_v1/connection/connection_to_client.cc +++ b/src/ballistica/scene_v1/connection/connection_to_client.cc @@ -95,7 +95,7 @@ ConnectionToClient::~ConnectionToClient() { void ConnectionToClient::Update() { Connection::Update(); // Handles common stuff. - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); // If we're waiting for handshake response still, keep sending out handshake // attempts. @@ -246,7 +246,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { // Don't allow fresh clients to start kick votes for a while. next_kick_vote_allow_time_ = - g_core->GetAppTimeMillisecs() + kNewClientKickVoteDelay; + g_core->AppTimeMillisecs() + kNewClientKickVoteDelay; // At this point we have their name, so lets announce their arrival. if (appmode->ShouldAnnouncePartyJoinsAndLeaves()) { @@ -263,7 +263,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { // Also mark the time for flashing the 'someone just joined your // party' message in the corner. appmode->set_last_connection_to_client_join_time( - g_core->GetAppTimeMillisecs()); + g_core->AppTimeMillisecs()); // Added midway through protocol 29: // We now send a json dict of info about ourself first thing. This @@ -338,8 +338,7 @@ void ConnectionToClient::SendScreenMessage(const std::string& s, float r, // Older clients don't support the screen-message message, so in that case // we just send it as a chat-message from . if (build_number() < 14248) { - std::string value = - g_base->assets->CompileResourceString(s, "sendScreenMessage"); + std::string value = g_base->assets->CompileResourceString(s); std::string our_spec_string = PlayerSpec::GetDummyPlayerSpec("").GetSpecString(); std::vector msg_out(1 + 1 + our_spec_string.size() + value.size()); @@ -500,7 +499,7 @@ void ConnectionToClient::HandleMessagePacket( case BA_MESSAGE_CHAT: { // We got a chat message from a client. - millisecs_t now = g_core->GetAppTimeMillisecs(); + millisecs_t now = g_core->AppTimeMillisecs(); // Ignore this if they're chat blocked. if (now >= chat_block_time_) { @@ -636,7 +635,7 @@ void ConnectionToClient::HandleMessagePacket( } case BA_MESSAGE_REMOVE_REMOTE_PLAYER: { - last_remove_player_time_ = g_core->GetAppTimeMillisecs(); + last_remove_player_time_ = g_core->AppTimeMillisecs(); if (buffer.size() != 2) { g_core->Log(LogName::kBaNetworking, LogLevel::kError, "Error: invalid remove-remote-player packet"); @@ -693,7 +692,7 @@ void ConnectionToClient::HandleMessagePacket( // master-server info for this client, delay their join (we'll // eventually give up and just give them a blank slate). if (still_waiting_for_auth - && (g_core->GetAppTimeMillisecs() - creation_time() < 10000)) { + && (g_core->AppTimeMillisecs() - creation_time() < 10000)) { SendScreenMessage( "{\"v\":\"${A}...\",\"s\":[[\"${A}\",{\"r\":" "\"loadingTryAgainText\",\"f\":\"loadingText\"}]]}", diff --git a/src/ballistica/scene_v1/connection/connection_to_host.cc b/src/ballistica/scene_v1/connection/connection_to_host.cc index 877e4b7de..462e981df 100644 --- a/src/ballistica/scene_v1/connection/connection_to_host.cc +++ b/src/ballistica/scene_v1/connection/connection_to_host.cc @@ -58,7 +58,7 @@ ConnectionToHost::~ConnectionToHost() { } void ConnectionToHost::Update() { - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); // Send out null messages occasionally for ping measurement purposes. // Note that we currently only do this from the client since we might not diff --git a/src/ballistica/scene_v1/dynamics/dynamics.cc b/src/ballistica/scene_v1/dynamics/dynamics.cc index b545aea6f..96f4bc77c 100644 --- a/src/ballistica/scene_v1/dynamics/dynamics.cc +++ b/src/ballistica/scene_v1/dynamics/dynamics.cc @@ -514,7 +514,7 @@ void Dynamics::ProcessCollision_() { void Dynamics::Process() { in_process_ = true; // Update this once so we can recycle results. - real_time_ = g_core->GetAppTimeMillisecs(); + real_time_ = g_core->AppTimeMillisecs(); ProcessCollision_(); dWorldQuickStep(ode_world_, kGameStepSeconds); dJointGroupEmpty(ode_contact_group_); diff --git a/src/ballistica/scene_v1/node/globals_node.cc b/src/ballistica/scene_v1/node/globals_node.cc index a167c1de6..40a549cd4 100644 --- a/src/ballistica/scene_v1/node/globals_node.cc +++ b/src/ballistica/scene_v1/node/globals_node.cc @@ -27,7 +27,7 @@ class GlobalsNodeType : public NodeType { public: #define BA_NODE_TYPE_CLASS GlobalsNode BA_NODE_CREATE_CALL(CreateGlobals); - BA_INT64_ATTR_READONLY(real_time, GetAppTimeMillisecs); + BA_INT64_ATTR_READONLY(real_time, AppTimeMillisecs); BA_INT64_ATTR_READONLY(time, GetTime); BA_INT64_ATTR_READONLY(step, GetStep); BA_FLOAT_ATTR(debris_friction, debris_friction, SetDebrisFriction); @@ -206,7 +206,7 @@ auto GlobalsNode::IsCurrentGlobals() const -> bool { && scene->globals_node() == this); } -auto GlobalsNode::GetAppTimeMillisecs() -> millisecs_t { +auto GlobalsNode::AppTimeMillisecs() -> millisecs_t { // Pull this from our scene so we return consistent values throughout a step. return scene()->last_step_real_time(); } diff --git a/src/ballistica/scene_v1/node/globals_node.h b/src/ballistica/scene_v1/node/globals_node.h index b1d487e89..e0e486bde 100644 --- a/src/ballistica/scene_v1/node/globals_node.h +++ b/src/ballistica/scene_v1/node/globals_node.h @@ -17,7 +17,7 @@ class GlobalsNode : public Node { ~GlobalsNode() override; void SetAsForeground(); auto IsCurrentGlobals() const -> bool; - auto GetAppTimeMillisecs() -> millisecs_t; + auto AppTimeMillisecs() -> millisecs_t; auto GetTime() -> millisecs_t; auto GetStep() -> int64_t; auto debris_friction() const -> float { return debris_friction_; } diff --git a/src/ballistica/scene_v1/node/session_globals_node.cc b/src/ballistica/scene_v1/node/session_globals_node.cc index 7479632b8..f577ef617 100644 --- a/src/ballistica/scene_v1/node/session_globals_node.cc +++ b/src/ballistica/scene_v1/node/session_globals_node.cc @@ -12,7 +12,7 @@ class SessionGlobalsNodeType : public NodeType { public: #define BA_NODE_TYPE_CLASS SessionGlobalsNode BA_NODE_CREATE_CALL(CreateSessionGlobals); - BA_INT64_ATTR_READONLY(real_time, GetAppTimeMillisecs); + BA_INT64_ATTR_READONLY(real_time, AppTimeMillisecs); BA_INT64_ATTR_READONLY(time, GetTime); BA_INT64_ATTR_READONLY(step, GetStep); #undef BA_NODE_TYPE_CLASS @@ -38,7 +38,7 @@ SessionGlobalsNode::SessionGlobalsNode(Scene* scene) : Node(scene, node_type) { SessionGlobalsNode::~SessionGlobalsNode() = default; -auto SessionGlobalsNode::GetAppTimeMillisecs() -> millisecs_t { +auto SessionGlobalsNode::AppTimeMillisecs() -> millisecs_t { // Pull this from our scene so we return consistent values throughout a step. return scene()->last_step_real_time(); } diff --git a/src/ballistica/scene_v1/node/session_globals_node.h b/src/ballistica/scene_v1/node/session_globals_node.h index dcc07552b..01ae47b3d 100644 --- a/src/ballistica/scene_v1/node/session_globals_node.h +++ b/src/ballistica/scene_v1/node/session_globals_node.h @@ -12,7 +12,7 @@ class SessionGlobalsNode : public Node { static auto InitType() -> NodeType*; explicit SessionGlobalsNode(Scene* scene); ~SessionGlobalsNode() override; - auto GetAppTimeMillisecs() -> millisecs_t; + auto AppTimeMillisecs() -> millisecs_t; auto GetTime() -> millisecs_t; auto GetStep() -> int64_t; }; diff --git a/src/ballistica/scene_v1/node/sound_node.cc b/src/ballistica/scene_v1/node/sound_node.cc index 2d26f8a8e..32ce4a1b2 100644 --- a/src/ballistica/scene_v1/node/sound_node.cc +++ b/src/ballistica/scene_v1/node/sound_node.cc @@ -142,7 +142,7 @@ void SoundNode::Step() { } } if (positional_ && position_dirty_ && playing_) { - millisecs_t t = g_core->GetAppTimeMillisecs(); + millisecs_t t = g_core->AppTimeMillisecs(); if (t - last_position_update_time_ > 100) { base::AudioSource* s = g_base->audio->SourceBeginExisting(play_id_, 107); if (s) { diff --git a/src/ballistica/scene_v1/node/spaz_node.cc b/src/ballistica/scene_v1/node/spaz_node.cc index b9956ddc2..3aee29f23 100644 --- a/src/ballistica/scene_v1/node/spaz_node.cc +++ b/src/ballistica/scene_v1/node/spaz_node.cc @@ -1797,7 +1797,7 @@ void SpazNode::DoFlyPress() { // Keep from doing too many sparkles. static millisecs_t last_sparkle_time = 0; - millisecs_t t = g_core->GetAppTimeMillisecs(); + millisecs_t t = g_core->AppTimeMillisecs(); if (t - last_sparkle_time > 200) { last_sparkle_time = t; auto* s = g_base->audio->SourceBeginNew(); diff --git a/src/ballistica/scene_v1/node/terrain_node.cc b/src/ballistica/scene_v1/node/terrain_node.cc index 2d1d4e7a3..4aec612c0 100644 --- a/src/ballistica/scene_v1/node/terrain_node.cc +++ b/src/ballistica/scene_v1/node/terrain_node.cc @@ -104,7 +104,7 @@ TerrainNode::~TerrainNode() { // without our reference. if (collision_mesh_.exists()) { collision_mesh_->collision_mesh_data()->set_last_used_time( - g_core->GetAppTimeMillisecs()); + g_core->AppTimeMillisecs()); } } @@ -123,7 +123,7 @@ void TerrainNode::set_collision_mesh(SceneCollisionMesh* val) { // if we had an old one, mark its last-used time so caching works properly.. if (collision_mesh_.exists()) { collision_mesh_->collision_mesh_data()->set_last_used_time( - g_core->GetAppTimeMillisecs()); + g_core->AppTimeMillisecs()); } collision_mesh_ = val; diff --git a/src/ballistica/scene_v1/node/text_node.cc b/src/ballistica/scene_v1/node/text_node.cc index 9c6e932c1..6f3d3fe2e 100644 --- a/src/ballistica/scene_v1/node/text_node.cc +++ b/src/ballistica/scene_v1/node/text_node.cc @@ -117,8 +117,7 @@ void TextNode::SetText(const std::string& val) { if (do_format_check) { bool valid; - g_base->assets->CompileResourceString(val, "setText format check", - &valid); + g_base->assets->CompileResourceString(val, &valid); if (!valid) { BA_LOG_ONCE( LogName::kBa, LogLevel::kError, @@ -354,8 +353,7 @@ void TextNode::Draw(base::FrameDef* frame_def) { // Apply subs/resources to get our actual text if need be. if (text_translation_dirty_) { - text_translated_ = - g_base->assets->CompileResourceString(text_raw_, "TextNode::OnDraw"); + text_translated_ = g_base->assets->CompileResourceString(text_raw_); text_translation_dirty_ = false; text_group_dirty_ = true; text_width_dirty_ = true; diff --git a/src/ballistica/scene_v1/node/time_display_node.cc b/src/ballistica/scene_v1/node/time_display_node.cc index 947609d5b..b04229da5 100644 --- a/src/ballistica/scene_v1/node/time_display_node.cc +++ b/src/ballistica/scene_v1/node/time_display_node.cc @@ -50,12 +50,12 @@ TimeDisplayNode::~TimeDisplayNode() = default; auto TimeDisplayNode::GetOutput() -> std::string { assert(g_base->InLogicThread()); if (translations_dirty_) { - time_suffix_hours_ = g_base->assets->CompileResourceString( - R"({"r":"timeSuffixHoursText"})", "tda"); + time_suffix_hours_ = + g_base->assets->CompileResourceString(R"({"r":"timeSuffixHoursText"})"); time_suffix_minutes_ = g_base->assets->CompileResourceString( - R"({"r":"timeSuffixMinutesText"})", "tdb"); + R"({"r":"timeSuffixMinutesText"})"); time_suffix_seconds_ = g_base->assets->CompileResourceString( - R"({"r":"timeSuffixSecondsText"})", "tdc"); + R"({"r":"timeSuffixSecondsText"})"); translations_dirty_ = false; output_dirty_ = true; } diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc b/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc index cd34041c9..c6780dc67 100644 --- a/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc +++ b/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc @@ -115,7 +115,7 @@ auto PythonClassSceneDataAsset::GetValue(PythonClassSceneDataAsset* self) // haha really need to rename this class. base::DataAsset* datadata = data->data_data(); datadata->Load(); - datadata->set_last_used_time(g_core->GetAppTimeMillisecs()); + datadata->set_last_used_time(g_core->AppTimeMillisecs()); PyObject* obj = datadata->object().get(); assert(obj); Py_INCREF(obj); diff --git a/src/ballistica/scene_v1/support/client_session.cc b/src/ballistica/scene_v1/support/client_session.cc index a63a4ef3e..49961fd55 100644 --- a/src/ballistica/scene_v1/support/client_session.cc +++ b/src/ballistica/scene_v1/support/client_session.cc @@ -1015,7 +1015,7 @@ void ClientSession::HandleSessionMessage(const std::vector& buffer) { // let's also use this opportunity to graph our command-buffer size // for network debugging... if (NetGraph *graph = // g_graphics->GetClientSessionStepBufferGraph()) { - // graph->addSample(GetAppTimeMillisecs(), steps_on_list_); + // graph->addSample(AppTimeMillisecs(), steps_on_list_); // } break; diff --git a/src/ballistica/scene_v1/support/client_session_net.cc b/src/ballistica/scene_v1/support/client_session_net.cc index ce3d34e28..a1b0d6751 100644 --- a/src/ballistica/scene_v1/support/client_session_net.cc +++ b/src/ballistica/scene_v1/support/client_session_net.cc @@ -52,7 +52,7 @@ void ClientSessionNet::OnCommandBufferUnderrun() { // We currently don't do anything here; we want to just power // through hitches and keep aiming for our target time. // (though perhaps we could take note here for analytics purposes). - // printf("Underrun at %d\n", GetAppTimeMillisecs()); + // printf("Underrun at %d\n", AppTimeMillisecs()); // fflush(stdout); } @@ -100,7 +100,7 @@ void ClientSessionNet::UpdateBuffering() { + (1.0f - smoothing) * static_cast(bucket.max_delay_from_projection); } - auto now = g_core->GetAppTimeMillisecs(); + auto now = g_core->AppTimeMillisecs(); // We want target-base-time to wind up at our projected time minus some // safety offset to account for buffering fluctuations. @@ -160,7 +160,7 @@ void ClientSessionNet::OnReset(bool rewind) { } void ClientSessionNet::OnBaseTimeStepAdded(int step) { - auto now = g_core->GetAppTimeMillisecs(); + auto now = g_core->AppTimeMillisecs(); millisecs_t new_base_time_received = base_time_received_ + step; diff --git a/src/ballistica/scene_v1/support/host_session.cc b/src/ballistica/scene_v1/support/host_session.cc index 25340fa27..9f00e258b 100644 --- a/src/ballistica/scene_v1/support/host_session.cc +++ b/src/ballistica/scene_v1/support/host_session.cc @@ -27,7 +27,7 @@ namespace ballistica::scene_v1 { HostSession::HostSession(PyObject* session_type_obj) - : last_kick_idle_players_decrement_time_(g_core->GetAppTimeMillisecs()) { + : last_kick_idle_players_decrement_time_(g_core->AppTimeMillisecs()) { assert(g_base->logic); assert(g_base->InLogicThread()); assert(session_type_obj != nullptr); @@ -327,7 +327,7 @@ void HostSession::SetKickIdlePlayers(bool enable) { // If this has changed, reset our disconnect-time reporting. assert(g_base->InLogicThread()); if (enable != kick_idle_players_) { - last_kick_idle_players_decrement_time_ = g_core->GetAppTimeMillisecs(); + last_kick_idle_players_decrement_time_ = g_core->AppTimeMillisecs(); } kick_idle_players_ = enable; } @@ -456,7 +456,7 @@ void HostSession::DecrementPlayerTimeOuts(millisecs_t millisecs) { } void HostSession::ProcessPlayerTimeOuts() { - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); if (foreground_host_activity_.exists() && foreground_host_activity_->game_speed() > 0.0 @@ -487,7 +487,7 @@ void HostSession::StepScene() { void HostSession::Update(int time_advance_millisecs, double time_advance) { assert(g_base->InLogicThread()); - millisecs_t update_time_start = core::CorePlatform::GetCurrentMillisecs(); + millisecs_t update_time_start = core::CorePlatform::TimeMonotonicMillisecs(); // HACK: we used to do a bunch of fudging to try and advance time by // exactly 16 milliseconds per frame which would give us a clean 2 sim @@ -546,7 +546,7 @@ void HostSession::Update(int time_advance_millisecs, double time_advance) { // slow down if we're overloaded and have a better chance at maintaining // a reasonable frame-rate/etc. auto elapsed = - core::CorePlatform::GetCurrentMillisecs() - update_time_start; + core::CorePlatform::TimeMonotonicMillisecs() - update_time_start; if (elapsed >= 1000 / 30) { too_slow = true; break; diff --git a/src/ballistica/scene_v1/support/player.cc b/src/ballistica/scene_v1/support/player.cc index 2db8aa63f..432951633 100644 --- a/src/ballistica/scene_v1/support/player.cc +++ b/src/ballistica/scene_v1/support/player.cc @@ -19,7 +19,7 @@ namespace ballistica::scene_v1 { Player::Player(int id_in, HostSession* host_session) : id_(id_in), - creation_time_(g_core->GetAppTimeMillisecs()), + creation_time_(g_core->AppTimeMillisecs()), host_session_(host_session) { assert(host_session); assert(g_base->InLogicThread()); @@ -40,7 +40,7 @@ Player::~Player() { } auto Player::GetAge() const -> millisecs_t { - return g_core->GetAppTimeMillisecs() - creation_time_; + return g_core->AppTimeMillisecs() - creation_time_; } auto Player::GetName(bool full, bool icon) const -> std::string { diff --git a/src/ballistica/scene_v1/support/scene.cc b/src/ballistica/scene_v1/support/scene.cc index 0293ad79d..a545af550 100644 --- a/src/ballistica/scene_v1/support/scene.cc +++ b/src/ballistica/scene_v1/support/scene.cc @@ -40,7 +40,7 @@ void Scene::SetMapBounds(float xmin, float ymin, float zmin, float xmax, Scene::Scene(millisecs_t start_time) : time_(start_time), stepnum_(start_time / kGameStepMilliseconds), - last_step_real_time_(g_core->GetAppTimeMillisecs()) { + last_step_real_time_(g_core->AppTimeMillisecs()) { dynamics_ = Object::New(this); // Reset world bounds to default. @@ -145,7 +145,7 @@ void Scene::Step() { // Step all our nodes. { in_step_ = true; - last_step_real_time_ = g_core->GetAppTimeMillisecs(); + last_step_real_time_ = g_core->AppTimeMillisecs(); for (auto&& i : nodes_) { Node* node = i.get(); node->Step(); diff --git a/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc b/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc index 298c1d7f9..3d16d34f7 100644 --- a/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc +++ b/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc @@ -201,7 +201,7 @@ void SceneV1InputDeviceDelegate::ShipBufferIfFull() { ConnectionToHost* hc = remote_player_.get(); // Ship the buffer once it gets big enough or once enough time has passed. - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); size_t size = remote_input_commands_buffer_.size(); if (size > 2 diff --git a/src/ballistica/scene_v1/support/session_stream.cc b/src/ballistica/scene_v1/support/session_stream.cc index cc6780ddf..b196f3889 100644 --- a/src/ballistica/scene_v1/support/session_stream.cc +++ b/src/ballistica/scene_v1/support/session_stream.cc @@ -374,7 +374,7 @@ void SessionStream::ShipSessionCommandsMessage() { AddMessageToReplay(out_message_); } out_message_.clear(); - last_send_time_ = g_core->GetAppTimeMillisecs(); + last_send_time_ = g_core->AppTimeMillisecs(); } void SessionStream::AddMessageToReplay(const std::vector& message) { @@ -441,7 +441,7 @@ void SessionStream::EndCommand(bool is_time_set) { if (host_session_) { auto* appmode = classic::ClassicAppMode::GetSingleton(); // Now if its been long enough *AND* this is a time-step command, send. - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); millisecs_t diff = real_time - last_send_time_; if (is_time_set && diff >= app_mode_->buffer_time()) { ShipSessionCommandsMessage(); diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index 82c917e00..ca606c13f 100644 --- a/src/ballistica/shared/ballistica.cc +++ b/src/ballistica/shared/ballistica.cc @@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int { namespace ballistica { // These are set automatically via script; don't modify them here. -const int kEngineBuildNumber = 22155; +const int kEngineBuildNumber = 22178; const char* kEngineVersion = "1.7.37"; const int kEngineApiVersion = 9; @@ -53,7 +53,7 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int { core::BaseSoftInterface* l_base{}; try { - auto time1 = core::CorePlatform::GetCurrentMillisecs(); + auto time1 = core::CorePlatform::TimeMonotonicMillisecs(); // Even at the absolute start of execution we should be able to // reasonably log errors. Set env var BA_CRASH_TEST=1 to test this. @@ -68,7 +68,7 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int { // import it first thing even if we don't explicitly use it. l_core = core::CoreFeatureSet::Import(&core_config); - auto time2 = core::CorePlatform::GetCurrentMillisecs(); + auto time2 = core::CorePlatform::TimeMonotonicMillisecs(); // If a command was passed, simply run it and exit. We want to act // simply as a Python interpreter in that case; we don't do any @@ -98,7 +98,7 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int { // those modules get loaded from in the first place. l_core->python->MonolithicModeBaEnvConfigure(); - auto time3 = core::CorePlatform::GetCurrentMillisecs(); + auto time3 = core::CorePlatform::TimeMonotonicMillisecs(); // We need the base feature-set to run a full app but we don't have a hard // dependency to it. Let's see if it's available. @@ -107,7 +107,7 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int { FatalError("Base module unavailable; can't run app."); } - auto time4 = core::CorePlatform::GetCurrentMillisecs(); + auto time4 = core::CorePlatform::TimeMonotonicMillisecs(); // ------------------------------------------------------------------------- // Phase 2: "The pieces are moving." @@ -126,7 +126,7 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int { // environment do that part). // Make noise if it takes us too long to get to this point. - auto time5 = core::CorePlatform::GetCurrentMillisecs(); + auto time5 = core::CorePlatform::TimeMonotonicMillisecs(); auto total_duration = time5 - time1; if (total_duration > 5000) { auto core_import_duration = time2 - time1; diff --git a/src/ballistica/shared/foundation/event_loop.cc b/src/ballistica/shared/foundation/event_loop.cc index 52a564a68..5bdb44541 100644 --- a/src/ballistica/shared/foundation/event_loop.cc +++ b/src/ballistica/shared/foundation/event_loop.cc @@ -206,7 +206,7 @@ void EventLoop::WaitForNextEvent_(bool single_cycle) { // If we've got active timers, wait for messages with a timeout so we can // run the next timer payload. if (!suspended_ && timers_.ActiveTimerCount() > 0) { - microsecs_t apptime = g_core->GetAppTimeMicrosecs(); + microsecs_t apptime = g_core->AppTimeMicrosecs(); microsecs_t wait_time = timers_.TimeToNextExpire(apptime); if (wait_time > 0) { std::unique_lock lock(thread_message_mutex_); @@ -306,7 +306,7 @@ void EventLoop::Run_(bool single_cycle) { } if (!suspended_) { - timers_.Run(g_core->GetAppTimeMicrosecs()); + timers_.Run(g_core->AppTimeMicrosecs()); RunPendingRunnables_(); } @@ -584,7 +584,7 @@ auto EventLoop::NewTimer(microsecs_t length, bool repeat, Runnable* runnable) assert(g_core); assert(ThreadIsCurrent()); assert(Object::IsValidManagedObject(runnable)); - return timers_.NewTimer(g_core->GetAppTimeMicrosecs(), length, 0, + return timers_.NewTimer(g_core->AppTimeMicrosecs(), length, 0, repeat ? -1 : 0, runnable); } @@ -718,7 +718,7 @@ void EventLoop::AcquireGIL_() { assert(g_base_soft && g_base_soft->InLogicThread()); auto debug_timing{g_core->core_config().debug_timing}; millisecs_t startmillisecs{ - debug_timing ? core::CorePlatform::GetCurrentMillisecs() : 0}; + debug_timing ? core::CorePlatform::TimeMonotonicMillisecs() : 0}; if (py_thread_state_) { PyEval_RestoreThread(py_thread_state_); @@ -726,7 +726,8 @@ void EventLoop::AcquireGIL_() { } if (debug_timing) { - auto duration{core::CorePlatform::GetCurrentMillisecs() - startmillisecs}; + auto duration{core::CorePlatform::TimeMonotonicMillisecs() + - startmillisecs}; if (duration > (1000 / 120)) { g_core->Log(LogName::kBa, LogLevel::kInfo, "GIL acquire took too long (" + std::to_string(duration) diff --git a/src/ballistica/shared/foundation/fatal_error.cc b/src/ballistica/shared/foundation/fatal_error.cc index 8cfe5d4ae..beaa578f7 100644 --- a/src/ballistica/shared/foundation/fatal_error.cc +++ b/src/ballistica/shared/foundation/fatal_error.cc @@ -122,7 +122,7 @@ void FatalError::ReportFatalError(const std::string& message, Logging::V1CloudLog(logmsg); Logging::EmitLog("root", LogLevel::kCritical, - core::CorePlatform::GetSecondsSinceEpoch(), logmsg); + core::CorePlatform::TimeSinceEpochSeconds(), logmsg); fprintf(stderr, "%s\n", logmsg.c_str()); std::string prefix = "FATAL-ERROR-LOG:"; @@ -182,9 +182,9 @@ void FatalError::DoBlockingFatalErrorDialog(const std::string& message) { // There's a chance that it can't (if threads are suspended, if it is // blocked on a synchronous call to another thread, etc.) so if we don't // see something happening soon, just give up on showing a dialog. - auto starttime = core::CorePlatform::GetCurrentMillisecs(); + auto starttime = core::CorePlatform::TimeMonotonicMillisecs(); while (!started) { - if (core::CorePlatform::GetCurrentMillisecs() - starttime > 3000) { + if (core::CorePlatform::TimeMonotonicMillisecs() - starttime > 3000) { return; } core::CorePlatform::SleepMillisecs(10); @@ -211,7 +211,7 @@ auto FatalError::HandleFatalError(bool exit_cleanly, if (!in_top_level_exception_handler) { if (exit_cleanly) { Logging::EmitLog("root", LogLevel::kCritical, - core::CorePlatform::GetSecondsSinceEpoch(), + core::CorePlatform::TimeSinceEpochSeconds(), "Calling exit(1)..."); // Inform anyone who cares that the engine is going down NOW. @@ -223,7 +223,7 @@ auto FatalError::HandleFatalError(bool exit_cleanly, exit(1); } else { Logging::EmitLog("root", LogLevel::kCritical, - core::CorePlatform::GetSecondsSinceEpoch(), + core::CorePlatform::TimeSinceEpochSeconds(), "Calling abort()..."); abort(); } diff --git a/src/ballistica/shared/foundation/macros.cc b/src/ballistica/shared/foundation/macros.cc index b69a26351..f168de732 100644 --- a/src/ballistica/shared/foundation/macros.cc +++ b/src/ballistica/shared/foundation/macros.cc @@ -25,7 +25,7 @@ void MacroFunctionTimerEnd(core::CoreFeatureSet* corefs, millisecs_t starttime, return; } assert(corefs); - millisecs_t endtime = corefs->platform->GetTicks(); + millisecs_t endtime = corefs->platform->TimeSinceLaunchMillisecs(); if (endtime - starttime > time) { core::g_core->Log(LogName::kBa, LogLevel::kWarning, std::to_string(endtime - starttime) @@ -42,7 +42,7 @@ void MacroFunctionTimerEndThread(core::CoreFeatureSet* corefs, return; } assert(corefs); - millisecs_t endtime = corefs->platform->GetTicks(); + millisecs_t endtime = corefs->platform->TimeSinceLaunchMillisecs(); if (endtime - starttime > time) { g_core->Log(LogName::kBa, LogLevel::kWarning, std::to_string(endtime - starttime) + " milliseconds spent by " @@ -60,7 +60,7 @@ void MacroFunctionTimerEndEx(core::CoreFeatureSet* corefs, return; } assert(corefs); - millisecs_t endtime = corefs->platform->GetTicks(); + millisecs_t endtime = corefs->platform->TimeSinceLaunchMillisecs(); if (endtime - starttime > time) { g_core->Log(LogName::kBa, LogLevel::kWarning, std::to_string(endtime - starttime) + " milliseconds spent in " @@ -78,7 +78,7 @@ void MacroFunctionTimerEndThreadEx(core::CoreFeatureSet* corefs, return; } assert(corefs); - millisecs_t endtime = corefs->platform->GetTicks(); + millisecs_t endtime = corefs->platform->TimeSinceLaunchMillisecs(); if (endtime - starttime > time) { g_core->Log(LogName::kBa, LogLevel::kWarning, std::to_string(endtime - starttime) + " milliseconds spent by " @@ -96,7 +96,7 @@ void MacroTimeCheckEnd(core::CoreFeatureSet* corefs, millisecs_t starttime, return; } assert(corefs); - millisecs_t e = corefs->platform->GetTicks(); + millisecs_t e = corefs->platform->TimeSinceLaunchMillisecs(); if (e - starttime > time) { g_core->Log(LogName::kBa, LogLevel::kWarning, std::string(name) + " took " + std::to_string(e - starttime) diff --git a/src/ballistica/shared/foundation/macros.h b/src/ballistica/shared/foundation/macros.h index 100c4cce5..528686d8c 100644 --- a/src/ballistica/shared/foundation/macros.h +++ b/src/ballistica/shared/foundation/macros.h @@ -43,7 +43,7 @@ // FIXME: Turn these into C++ classes. #if BA_DEBUG_BUILD #define BA_DEBUG_FUNCTION_TIMER_BEGIN() \ - millisecs_t _dfts = g_core->platform->GetTicks() + millisecs_t _dfts = g_core->platform->TimeSinceLaunchMillisecs() #define BA_DEBUG_FUNCTION_TIMER_END(time) \ ::ballistica::MacroFunctionTimerEnd(g_core, _dfts, time, __PRETTY_FUNCTION__) #define BA_DEBUG_FUNCTION_TIMER_END_THREAD(time) \ @@ -55,7 +55,7 @@ ::ballistica::MacroFunctionTimerEndThreadEx(g_core, _dfts, time, \ __PRETTY_FUNCTION__, what) #define BA_DEBUG_TIME_CHECK_BEGIN(name) \ - millisecs_t name##_ts = g_core->platform->GetTicks() + millisecs_t name##_ts = g_core->platform->TimeSinceLaunchMillisecs() #define BA_DEBUG_TIME_CHECK_END(name, time) \ ::ballistica::MacroTimeCheckEnd(g_core, name##_ts, time, #name, __FILE__, \ __LINE__) diff --git a/src/ballistica/shared/foundation/object.cc b/src/ballistica/shared/foundation/object.cc index 03dee019c..1d1298911 100644 --- a/src/ballistica/shared/foundation/object.cc +++ b/src/ballistica/shared/foundation/object.cc @@ -24,7 +24,7 @@ Object::Object() { #if BA_DEBUG_BUILD // Mark when we were born. assert(g_core); - object_birth_time_ = g_core->GetAppTimeMillisecs(); + object_birth_time_ = g_core->AppTimeMillisecs(); // Add ourself to the global object list. { @@ -126,7 +126,7 @@ void Object::LsObjects() { { std::scoped_lock lock(g_core->object_list_mutex); s = std::to_string(g_core->object_count) + " Objects at time " - + std::to_string(g_core->GetAppTimeMillisecs()) + ";"; + + std::to_string(g_core->AppTimeMillisecs()) + ";"; if (explicit_bool(true)) { std::unordered_map obj_map; diff --git a/src/ballistica/shared/python/python_ref.cc b/src/ballistica/shared/python/python_ref.cc index 7a368ca25..a324f32c5 100644 --- a/src/ballistica/shared/python/python_ref.cc +++ b/src/ballistica/shared/python/python_ref.cc @@ -4,6 +4,7 @@ #include #include +#include #include "ballistica/core/core.h" #include "ballistica/core/support/base_soft.h" @@ -251,6 +252,7 @@ auto PythonRef::GetAttr(const char* name) const -> PythonRef { auto PythonRef::DictGetItem(const char* name) const -> PythonRef { assert(Python::HaveGIL()); ThrowIfUnset(); + assert(PyDict_Check(obj_)); // Caller's job to ensure this. PyObject* key = PyUnicode_FromString(name); PyObject* out = PyDict_GetItemWithError(obj_, key); Py_DECREF(key); @@ -270,6 +272,26 @@ auto PythonRef::DictGetItem(const char* name) const -> PythonRef { return {}; } +auto PythonRef::DictItems() const + -> std::vector> { + assert(Python::HaveGIL()); + ThrowIfUnset(); + + assert(PyDict_Check(obj_)); // Caller's job to ensure this. + + Py_ssize_t pos{}; + PyObject *key, *value; + std::vector> out; + out.resize(PyDict_Size(obj_)); + size_t i = 0; + while (PyDict_Next(obj_, &pos, &key, &value)) { + out[i].first.Acquire(key); + out[i].second.Acquire(value); + i++; + } + return out; +} + auto PythonRef::NewRef() const -> PyObject* { assert(Python::HaveGIL()); ThrowIfUnset(); diff --git a/src/ballistica/shared/python/python_ref.h b/src/ballistica/shared/python/python_ref.h index fea453f29..cbeb5c832 100644 --- a/src/ballistica/shared/python/python_ref.h +++ b/src/ballistica/shared/python/python_ref.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "ballistica/shared/ballistica.h" // IWYU pragma: keep. @@ -153,6 +154,9 @@ class PythonRef { /// Throws Exception if an error occurs. auto DictGetItem(const char* name) const -> PythonRef; + /// Return all items in a dict as C++ structures. + auto DictItems() const -> std::vector>; + /// The equivalent of calling Python str() on the contained PyObject, and /// gracefully handles invalid refs. To throw exceptions on invalid refs, /// use ValueAsString(); diff --git a/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc b/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc index 96fff73b1..90c823b01 100644 --- a/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc +++ b/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc @@ -124,7 +124,7 @@ PyTypeObject PythonClassUISound::type_obj; PyMethodDef PythonClassUISound::tp_methods[] = { {"play", (PyCFunction)PythonClassUISound::Play, METH_VARARGS | METH_KEYWORDS, - "play() -> None\n" + "play(volume: float = 1.0) -> None\n" "\n" "Play the sound locally.\n" ""}, diff --git a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc index e10084fdd..9b083e7b5 100644 --- a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc +++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc @@ -22,6 +22,7 @@ #include "ballistica/ui_v1/widget/root_widget.h" #include "ballistica/ui_v1/widget/row_widget.h" #include "ballistica/ui_v1/widget/scroll_widget.h" +#include "ballistica/ui_v1/widget/spinner_widget.h" namespace ballistica::ui_v1 { @@ -901,6 +902,98 @@ static PyMethodDef PyImageWidgetDef = { "are applied to the Widget.", }; +// ----------------------------- imagewidget ----------------------------------- + +static auto PySpinnerWidget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* edit_obj{Py_None}; + PyObject* parent_obj{Py_None}; + ContainerWidget* parent_widget{}; + PyObject* size_obj{Py_None}; + PyObject* pos_obj{Py_None}; + PyObject* visible_obj{Py_None}; + + static const char* kwlist[] = {"edit", "parent", "size", + "position", "visible", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|OOOOO", const_cast(kwlist), &edit_obj, + &parent_obj, &size_obj, &pos_obj, &visible_obj)) + return nullptr; + + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); + } + + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + + // Grab the edited widget or create a new one. + Object::Ref b; + if (edit_obj != Py_None) { + b = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); + if (!b.exists()) + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } else { + parent_widget = parent_obj == Py_None + ? g_ui_v1->screen_root_widget() + : dynamic_cast( + UIV1Python::GetPyWidget(parent_obj)); + if (parent_widget == nullptr) { + throw Exception("Parent widget nonexistent or not a container.", + PyExcType::kWidgetNotFound); + } + b = Object::New(); + } + if (size_obj != Py_None) { + auto size{Python::GetPyFloat(size_obj)}; + b->set_size(size); + } + if (pos_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(pos_obj); + b->set_translate(p.x, p.y); + } + if (visible_obj != Py_None) { + b->set_visible(Python::GetPyBool(visible_obj)); + } + + // If making a new widget, add it at the end. + if (edit_obj == Py_None) { + g_ui_v1->AddWidget(b.get(), parent_widget); + } + + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + + return b->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PySpinnerWidgetDef = { + "spinnerwidget", // name + (PyCFunction)PySpinnerWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "spinnerwidget(*,\n" + " edit: bauiv1.Widget | None = None,\n" + " parent: bauiv1.Widget | None = None,\n" + " size: float | None = None,\n" + " position: Sequence[float] | None = None,\n" + " visible: bool | None = None,\n" + ")\n" + " -> bauiv1.Widget\n" + "\n" + "Create or edit a spinner widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget.", +}; + // ----------------------------- columnwidget ---------------------------------- static auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) @@ -1580,6 +1673,7 @@ static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) PyObject* parent_obj{Py_None}; PyObject* edit_obj{Py_None}; PyObject* center_small_content_obj{Py_None}; + PyObject* center_small_content_horizontally_obj{Py_None}; ContainerWidget* parent_widget{}; PyObject* color_obj{Py_None}; PyObject* highlight_obj{Py_None}; @@ -1599,6 +1693,7 @@ static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) "capture_arrows", "on_select_call", "center_small_content", + "center_small_content_horizontally", "color", "highlight", "border_opacity", @@ -1610,13 +1705,13 @@ static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) nullptr}; if (!PyArg_ParseTupleAndKeywords( - args, keywds, "|OOOOOOOOOOOOOOOOO", const_cast(kwlist), + args, keywds, "|OOOOOOOOOOOOOOOOOO", const_cast(kwlist), &edit_obj, &parent_obj, &size_obj, &pos_obj, &background_obj, &selected_child_obj, &capture_arrows_obj, &on_select_call_obj, - ¢er_small_content_obj, &color_obj, &highlight_obj, - &border_opacity_obj, &simple_culling_v_obj, - &selection_loops_to_parent_obj, &claims_left_right_obj, - &claims_up_down_obj, &autoselect_obj)) + ¢er_small_content_obj, ¢er_small_content_horizontally_obj, + &color_obj, &highlight_obj, &border_opacity_obj, + &simple_culling_v_obj, &selection_loops_to_parent_obj, + &claims_left_right_obj, &claims_up_down_obj, &autoselect_obj)) return nullptr; if (!g_base->CurrentContext().IsEmpty()) { @@ -1670,6 +1765,10 @@ static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) widget->set_center_small_content( Python::GetPyBool(center_small_content_obj)); } + if (center_small_content_horizontally_obj != Py_None) { + widget->set_center_small_content_horizontally( + Python::GetPyBool(center_small_content_horizontally_obj)); + } if (color_obj != Py_None) { std::vector c = Python::GetPyFloats(color_obj); if (c.size() != 3) { @@ -1731,6 +1830,7 @@ static PyMethodDef PyScrollWidgetDef = { " capture_arrows: bool = False,\n" " on_select_call: Callable | None = None,\n" " center_small_content: bool | None = None,\n" + " center_small_content_horizontally: bool | None = None,\n" " color: Sequence[float] | None = None,\n" " highlight: bool | None = None,\n" " border_opacity: float | None = None,\n" @@ -2101,7 +2201,7 @@ static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) // we should probably extend TextWidget to handle this internally, but // punting on that for now. widget->set_description(g_base->assets->CompileResourceString( - g_base->python->GetPyLString(description_obj), "textwidget set desc")); + g_base->python->GetPyLString(description_obj))); } if (autoselect_obj != Py_None) { widget->set_auto_select(Python::GetPyBool(autoselect_obj)); @@ -2661,18 +2761,80 @@ static PyMethodDef PyOnScreenChangeDef = { "(internal)", }; +// ------------------------ root_ui_pause_updates ------------------------------ + +static auto PyRootUIPauseUpdates(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + + auto* root_widget = g_ui_v1->root_widget(); + BA_PRECONDITION(root_widget); + root_widget->PauseUpdates(); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyRootUIPauseUpdatesDef = { + "root_ui_pause_updates", // name + (PyCFunction)PyRootUIPauseUpdates, // method + METH_NOARGS, // flags + + "root_ui_pause_updates() -> None\n" + "\n" + "Temporarily pause updates to the root ui for animation purposes.", +}; + +// ------------------------ root_ui_resume_updates ----------------------------- + +static auto PyRootUIResumeUpdates(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + + auto* root_widget = g_ui_v1->root_widget(); + BA_PRECONDITION(root_widget); + root_widget->ResumeUpdates(); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyRootUIResumeUpdatesDef = { + "root_ui_resume_updates", // name + (PyCFunction)PyRootUIResumeUpdates, // method + METH_NOARGS, // flags + + "root_ui_resume_updates() -> None\n" + "\n" + "Temporarily resume updates to the root ui for animation purposes.", +}; + // ----------------------------------------------------------------------------- auto PythonMethodsUIV1::GetMethods() -> std::vector { - return { - PyRootUIBackPressDef, PyGetSpecialWidgetDef, PySetPartyWindowOpenDef, - PyButtonWidgetDef, PyCheckBoxWidgetDef, PyImageWidgetDef, - PyColumnWidgetDef, PyContainerWidgetDef, PyRowWidgetDef, - PyScrollWidgetDef, PyHScrollWidgetDef, PyTextWidgetDef, - PyWidgetDef, PyUIBoundsDef, PyGetSoundDef, - PyGetTextureDef, PyGetQRCodeTextureDef, PyGetMeshDef, - PyIsAvailableDef, PyOnScreenChangeDef, - }; + return {PyRootUIBackPressDef, + PyGetSpecialWidgetDef, + PySetPartyWindowOpenDef, + PyButtonWidgetDef, + PyCheckBoxWidgetDef, + PyImageWidgetDef, + PySpinnerWidgetDef, + PyColumnWidgetDef, + PyContainerWidgetDef, + PyRowWidgetDef, + PyScrollWidgetDef, + PyHScrollWidgetDef, + PyTextWidgetDef, + PyWidgetDef, + PyUIBoundsDef, + PyGetSoundDef, + PyGetTextureDef, + PyGetQRCodeTextureDef, + PyGetMeshDef, + PyIsAvailableDef, + PyOnScreenChangeDef, + PyRootUIPauseUpdatesDef, + PyRootUIResumeUpdatesDef}; } #pragma clang diagnostic pop diff --git a/src/ballistica/ui_v1/ui_v1.cc b/src/ballistica/ui_v1/ui_v1.cc index f6b2c517d..c1a61545e 100644 --- a/src/ballistica/ui_v1/ui_v1.cc +++ b/src/ballistica/ui_v1/ui_v1.cc @@ -278,7 +278,6 @@ void UIV1FeatureSet::ConfirmQuit(QuitType quit_type) { } UIV1FeatureSet::UILock::UILock(bool write) { - assert(g_base->ui); assert(g_base->InLogicThread()); if (write && g_ui_v1->ui_lock_count_ != 0) { diff --git a/src/ballistica/ui_v1/ui_v1.h b/src/ballistica/ui_v1/ui_v1.h index 13c86b5c8..73d7b354a 100644 --- a/src/ballistica/ui_v1/ui_v1.h +++ b/src/ballistica/ui_v1/ui_v1.h @@ -143,11 +143,8 @@ class UIV1FeatureSet : public FeatureSetNativeComponent, Object::Ref root_widget_; int ui_lock_count_{}; int language_state_{}; - // int party_icon_number_{}; bool always_use_internal_on_screen_keyboard_{}; bool party_window_open_{}; - // bool account_signed_in_{}; - // std::string account_name_{}; }; } // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/widget/check_box_widget.cc b/src/ballistica/ui_v1/widget/check_box_widget.cc index dd4c16969..d569feaa0 100644 --- a/src/ballistica/ui_v1/widget/check_box_widget.cc +++ b/src/ballistica/ui_v1/widget/check_box_widget.cc @@ -46,7 +46,7 @@ void CheckBoxWidget::SetHeight(float height_in) { } void CheckBoxWidget::Draw(base::RenderPass* pass, bool draw_transparent) { - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + millisecs_t real_time = g_core->AppTimeMillisecs(); have_drawn_ = true; float l = 0.0f; @@ -236,7 +236,7 @@ void CheckBoxWidget::SetValue(bool value) { // Don't animate if we're setting initial values. if (checked_ != value && have_drawn_) { - last_change_time_ = g_core->GetAppTimeMillisecs(); + last_change_time_ = g_core->AppTimeMillisecs(); } checked_ = value; } @@ -245,7 +245,7 @@ void CheckBoxWidget::Activate() { g_base->audio->SafePlaySysSound(base::SysSoundID::kSwish3); checked_ = !checked_; check_dirty_ = true; - last_change_time_ = g_core->GetAppTimeMillisecs(); + last_change_time_ = g_core->AppTimeMillisecs(); if (auto* call = on_value_change_call_.get()) { PythonRef args(Py_BuildValue("(O)", checked_ ? Py_True : Py_False), PythonRef::kSteal); diff --git a/src/ballistica/ui_v1/widget/root_widget.cc b/src/ballistica/ui_v1/widget/root_widget.cc index 2277528af..d93cf2091 100644 --- a/src/ballistica/ui_v1/widget/root_widget.cc +++ b/src/ballistica/ui_v1/widget/root_widget.cc @@ -3,6 +3,7 @@ #include "ballistica/ui_v1/widget/root_widget.h" #include +#include #include #include @@ -10,9 +11,12 @@ #include "ballistica/base/assets/assets.h" #include "ballistica/base/graphics/renderer/render_pass.h" #include "ballistica/base/graphics/support/frame_def.h" +#include "ballistica/base/support/classic_soft.h" #include "ballistica/base/support/context.h" #include "ballistica/shared/buildconfig/buildconfig_common.h" #include "ballistica/shared/foundation/inline.h" +#include "ballistica/shared/foundation/types.h" +#include "ballistica/shared/generic/utils.h" #include "ballistica/ui_v1/python/ui_v1_python.h" #include "ballistica/ui_v1/widget/button_widget.h" #include "ballistica/ui_v1/widget/image_widget.h" @@ -849,10 +853,35 @@ void RootWidget::Setup() { chest_3_lock_icon_ = AddImage_(imgd); } + // TV icons. + { + ImageDef_ imgd; + imgd.x = -34.0f; + imgd.y = -27.0f; + imgd.width = 32.0f; + imgd.height = 32.0f; + imgd.img = "tv"; + imgd.depth_min = 0.3f; + imgd.color_r = 1.5f; + imgd.color_g = 1.0f; + imgd.color_b = 2.0f; + + imgd.button = chest_0_button_; + chest_0_tv_icon_ = AddImage_(imgd); + + imgd.button = chest_1_button_; + chest_1_tv_icon_ = AddImage_(imgd); + + imgd.button = chest_2_button_; + chest_2_tv_icon_ = AddImage_(imgd); + + imgd.button = chest_3_button_; + chest_3_tv_icon_ = AddImage_(imgd); + } + // Lock times. { TextDef_ td; - // td.width = 0.0f; td.text = "3h 2m"; td.x = 0.0f; td.y = 55.0f; @@ -895,6 +924,11 @@ void RootWidget::Setup() { b.disable_offset_scale = 1.5f; b.pre_buffer = 20.0f; b.allow_in_game = false; + + // This is a very big icon that can interfere with clicking stuff near + // it, so suck target area in a bit. + b.target_extra_left = -20.0f; + b.target_extra_right = -20.0f; inventory_button_ = AddButton_(b); bottom_right_buttons_.push_back(inventory_button_); } @@ -925,13 +959,23 @@ void RootWidget::Setup() { void RootWidget::Draw(base::RenderPass* pass, bool transparent) { // Opaque pass gets drawn first; use that as an opportunity to step up our // motion. - if (!transparent) { - millisecs_t current_time = pass->frame_def()->display_time_millisecs(); - millisecs_t time_diff = - std::min(millisecs_t{100}, current_time - update_time_); + seconds_t current_time = pass->frame_def()->display_time(); + seconds_t time_diff = std::min(seconds_t{0.1}, current_time - update_time_); + + // millisecs_t current_time = pass->frame_def()->display_time_millisecs(); + // millisecs_t time_diff = + // std::min(millisecs_t{100}, current_time - update_time_); + + StepChildWidgets_(time_diff); + StepChests_(); + + if (update_pause_count_ != 0) { + // update_pause_time_ += + } else { + update_pause_time_ = 0.0; + } - StepChildWidgets_(static_cast(time_diff)); update_time_ = current_time; } ContainerWidget::Draw(pass, transparent); @@ -977,6 +1021,7 @@ auto RootWidget::AddButton_(const ButtonDef_& def) -> RootWidget::Button_* { } else { b.widget->SetUpWidget(screen_stack_widget_); } + // We wanna prevent anyone from redirecting these to point to outside // widgets since we'll probably outlive those outside widgets. b.widget->set_neighbors_locked(true); @@ -1067,11 +1112,23 @@ void RootWidget::UpdateForFocusedWindow_(Widget* widget) { MarkForUpdate(); } -void RootWidget::StepChildWidgets_(float dt) { +void RootWidget::StepChests_() { + // Aim to run this once per second. + auto now = g_core->AppTimeSeconds(); + if (now - last_chests_step_time_ < 1.0) { + return; + } + last_chests_step_time_ = now; + UpdateChests_(); +} + +void RootWidget::StepChildWidgets_(seconds_t dt) { // Hitches tend to break our math and cause buttons to overshoot on their // transitions in and then back up. So let's limit our max dt to about // what ~30fps would give us. - dt = std::min(dt, 1000.0f / 30.0f); + dt = std::min(dt, 1.0 / 30.0); + + float dt_ms = dt * 1000.0; if (!child_widgets_dirty_) { return; @@ -1110,7 +1167,7 @@ void RootWidget::StepChildWidgets_(float dt) { float xpos = 0.0f; for (auto* btn : top_left_buttons_) { auto enabled = btn->enabled; - float bwidthhalf = btn->width * 0.5; + float bwidthhalf = btn->width * 0.5f; if (enabled) { xpos += bwidthhalf + btn->pre_buffer; } @@ -1190,8 +1247,8 @@ void RootWidget::StepChildWidgets_(float dt) { } // Now push our smooth value towards our target value. - b.x_smoothed += (b.x_target - b.x_smoothed) * 0.015f * dt; - b.y_smoothed += (b.y_target - b.y_smoothed) * 0.015f * dt; + b.x_smoothed += (b.x_target - b.x_smoothed) * 0.015f * dt_ms; + b.y_smoothed += (b.y_target - b.y_smoothed) * 0.015f * dt_ms; // Snap in place once we reach the target; otherwise note that we need // to keep going. @@ -1296,7 +1353,7 @@ void RootWidget::UpdateLayout() { // Run an immediate step to update things; (avoids jumpy positions if // resizing game window)) - StepChildWidgets_(0.0f); + StepChildWidgets_(0.0); } void RootWidget::OnUIScaleChange() { MarkForUpdate(); } @@ -1419,12 +1476,12 @@ void RootWidget::SetSquadSizeLabel(int val) { } } -void RootWidget::SetTicketsMeterText(const std::string& val) { +void RootWidget::SetTicketsMeterValue(int val) { assert(tickets_meter_text_); - tickets_meter_text_->widget->SetText(val); + tickets_meter_text_->widget->SetText(val >= 0 ? std::to_string(val) : ""); } -void RootWidget::SetTokensMeterText(const std::string& val, bool gold_pass) { +void RootWidget::SetTokensMeterValue(int val, bool gold_pass) { assert(tokens_meter_text_); assert(get_tokens_button_); gold_pass_ = gold_pass; @@ -1436,7 +1493,7 @@ void RootWidget::SetTokensMeterText(const std::string& val, bool gold_pass) { g_buildconfig.enable_os_font_rendering() ? "\xE2\x88\x9E" : "inf"); } else { get_tokens_button_->force_hide = false; - tokens_meter_text_->widget->SetText(val); + tokens_meter_text_->widget->SetText(val >= 0 ? std::to_string(val) : ""); } UpdateTokensMeterTextColor_(); // May need to animate in/out. @@ -1452,9 +1509,10 @@ void RootWidget::UpdateTokensMeterTextColor_() { } } -void RootWidget::SetLeagueRankText(const std::string& val) { +void RootWidget::SetLeagueRankValue(int val) { assert(league_rank_text_); - league_rank_text_->widget->SetText(val); + league_rank_text_->widget->SetText(val > 0 ? ("#" + std::to_string(val)) + : ""); } void RootWidget::SetLeagueType(const std::string& val) { @@ -1553,68 +1611,169 @@ void RootWidget::SetHaveLiveValues(bool have_live_values) { chest_backing_->widget->set_opacity(have_live_values ? 1.0f : 0.5f); } -void RootWidget::SetChests(const std::string& chest_0_appearance, - const std::string& chest_1_appearance, - const std::string& chest_2_appearance, - const std::string& chest_3_appearance) { +void RootWidget::SetChests( + const std::string& chest_0_appearance, + const std::string& chest_1_appearance, + const std::string& chest_2_appearance, + const std::string& chest_3_appearance, seconds_t chest_0_unlock_time, + seconds_t chest_1_unlock_time, seconds_t chest_2_unlock_time, + seconds_t chest_3_unlock_time, seconds_t chest_0_ad_allow_time, + seconds_t chest_1_ad_allow_time, seconds_t chest_2_ad_allow_time, + seconds_t chest_3_ad_allow_time) { chest_0_appearance_ = chest_0_appearance; chest_1_appearance_ = chest_1_appearance; chest_2_appearance_ = chest_2_appearance; chest_3_appearance_ = chest_3_appearance; + chest_0_unlock_time_ = chest_0_unlock_time; + chest_1_unlock_time_ = chest_1_unlock_time; + chest_2_unlock_time_ = chest_2_unlock_time; + chest_3_unlock_time_ = chest_3_unlock_time; + chest_0_ad_allow_time_ = chest_0_ad_allow_time; + chest_1_ad_allow_time_ = chest_1_ad_allow_time; + chest_2_ad_allow_time_ = chest_2_ad_allow_time; + chest_3_ad_allow_time_ = chest_3_ad_allow_time; UpdateChests_(); } +void RootWidget::OnLanguageChange() { + ContainerWidget::OnLanguageChange(); + translations_dirty_ = true; +} + void RootWidget::UpdateChests_() { - std::vector> slots = - // NOLINTNEXTLINE (clang-format's formatting here upsets cpplint). + // Make sure we've got the latest translated strings for open times. + if (translations_dirty_) { + time_suffix_hours_ = + g_base->assets->CompileResourceString(R"({"r":"timeSuffixHoursText"})"); + time_suffix_minutes_ = g_base->assets->CompileResourceString( + R"({"r":"timeSuffixMinutesText"})"); + time_suffix_seconds_ = g_base->assets->CompileResourceString( + R"({"r":"timeSuffixSecondsText"})"); + translations_dirty_ = false; + } + + std::vector> + slots = + // NOLINTNEXTLINE (clang-format's formatting here upsets cpplint). { {chest_0_appearance_, chest_0_button_, chest_0_lock_icon_, - chest_0_time_text_}, + chest_0_tv_icon_, chest_0_time_text_, chest_0_unlock_time_, + chest_0_ad_allow_time_}, {chest_1_appearance_, chest_1_button_, chest_1_lock_icon_, - chest_1_time_text_}, + chest_1_tv_icon_, chest_1_time_text_, chest_1_unlock_time_, + chest_1_ad_allow_time_}, {chest_2_appearance_, chest_2_button_, chest_2_lock_icon_, - chest_2_time_text_}, + chest_2_tv_icon_, chest_2_time_text_, chest_2_unlock_time_, + chest_2_ad_allow_time_}, {chest_3_appearance_, chest_3_button_, chest_3_lock_icon_, - chest_3_time_text_}, + chest_3_tv_icon_, chest_3_time_text_, chest_3_unlock_time_, + chest_3_ad_allow_time_}, }; // We drop the backing/slots down a bit if we have no chests. auto have_chests{false}; - for (const auto& [appearance, b, l, t] : slots) { + + // clang-format off + for (const auto& [appearance, + b, + l, + tv, + t, + ut, + aat] : slots) { + // clang-format on + if (appearance != "") { have_chests = true; } } - for (const auto& [appearance, b, l, t] : slots) { - assert(b); - assert(l); + auto now{g_base->TimeSinceEpochCloudSeconds()}; + + // clang-format off + for (const auto& [appearance, + btn, + lock_img, + tv_img, + txt, + unlocktm, + adallowtm] : slots) { + // clang-format on + + assert(btn); + assert(lock_img); Object::Ref tex; if (appearance == "") { // Empty slot. - b->widget->set_color(0.473f, 0.44f, 0.583f); - b->width = b->height = 80.0f; - b->y = have_chests ? 44.0f : -2.0f; + btn->widget->set_color(0.473f, 0.44f, 0.583f); + btn->width = btn->height = 80.0f; + btn->y = have_chests ? 44.0f : -2.0f; { base::Assets::AssetListLock lock; tex = g_base->assets->GetTexture("chestIconEmpty"); } - l->visible = false; - t->visible = false; + lock_img->visible = false; + tv_img->visible = false; + txt->visible = false; + + btn->widget->SetTintTexture(nullptr); + btn->widget->set_tint_color(1.0f, 1.0f, 1.0f); + btn->widget->set_tint2_color(1.0f, 1.0f, 1.0f); + } else { + Object::Ref textint; + // Chest in slot. have_chests = true; - b->widget->set_color(1.0f, 1.0f, 1.0f); - b->width = b->height = 110.0f; - b->y = 44.0f; + btn->width = btn->height = 110.0f; + btn->y = 44.0f; + std::string chest_tex_closed; + std::string chest_tex_closed_tint; + Vector3f chest_color; + Vector3f chest_tint; + Vector3f chest_tint2; + if (auto* classic = g_base->classic()) { + classic->GetClassicChestDisplayInfo( + appearance, &chest_tex_closed, &chest_tex_closed_tint, &chest_color, + &chest_tint, &chest_tint2); + } else { + chest_tex_closed = "chestIcon"; + chest_tex_closed_tint = "white"; + chest_color = Vector3f{1.0f, 1.0f, 1.0f}; + chest_tint = Vector3f{1.0f, 1.0f, 1.0f}; + chest_tint2 = Vector3f{1.0f, 1.0f, 1.0f}; + } { base::Assets::AssetListLock lock; - tex = g_base->assets->GetTexture("chestIcon"); + tex = g_base->assets->GetTexture(chest_tex_closed); + textint = g_base->assets->GetTexture(chest_tex_closed_tint); + } + btn->widget->set_color(chest_color.x, chest_color.y, chest_color.z); + btn->widget->SetTintTexture(textint.get()); + btn->widget->set_tint_color(chest_tint.x, chest_tint.y, chest_tint.z); + btn->widget->set_tint2_color(chest_tint2.x, chest_tint2.y, chest_tint2.z); + + auto to_unlock{gold_pass_ ? 0 + : static_cast(std::ceil(unlocktm - now))}; + + if (to_unlock > 0) { + // Show the ad-available tag IF the ad provides an allow-ad time AND + // that time has passed AND we've got an ad ready to go. + auto allow_ad{adallowtm > 0.0 && adallowtm <= now + && g_core->have_incentivized_ad}; + + lock_img->visible = true; + txt->visible = true; + tv_img->visible = allow_ad; + txt->widget->SetText(GetTimeStr_(to_unlock)); + } else { + lock_img->visible = false; + tv_img->visible = false; + txt->visible = false; } - l->visible = true; - t->visible = true; } - b->widget->SetTexture(tex.get()); + btn->widget->SetTexture(tex.get()); } assert(chest_backing_); @@ -1623,6 +1782,80 @@ void RootWidget::UpdateChests_() { child_widgets_dirty_ = true; } +auto RootWidget::GetTimeStr_(seconds_t diff) -> std::string { + // NOTE: Adapted from time_display_node.cc. Not sure if it would make + // sense to share this code somewhere?.. + std::string output; + auto show_sub_seconds{false}; + + auto t{static_cast(diff * 1000.0)}; + bool is_negative = false; + if (t < 0) { + t = -t; + is_negative = true; + } + + // Hours + int h = static_cast_check_fit(((t / 1000) / (60 * 60))); + if (h != 0) { + std::string s = time_suffix_hours_; + char buffer[100]; + snprintf(buffer, sizeof(buffer), "%d", h); + Utils::StringReplaceOne(&s, "${COUNT}", buffer); + if (!output.empty()) { + output += " "; + } + output += s; + } + + // Minutes. + int m = static_cast_check_fit(((t / 1000) / 60) % 60); + if (m != 0) { + std::string s = time_suffix_minutes_; + char buffer[100]; + snprintf(buffer, sizeof(buffer), "%d", m); + Utils::StringReplaceOne(&s, "${COUNT}", buffer); + if (!output.empty()) { + output += " "; + } + output += s; + } + + // Only show seconds when within a few minutes. + if (m < 2) { + if (show_sub_seconds) { + float sec = fmod(static_cast(t) / 1000.0f, 60.0f); + if (sec >= 0.005f || output.empty()) { + std::string s = time_suffix_seconds_; + char buffer[100]; + snprintf(buffer, sizeof(buffer), "%.2f", sec); + Utils::StringReplaceOne(&s, "${COUNT}", buffer); + if (!output.empty()) { + output += " "; + } + output += s; + } + } else { + // Seconds (integer). + int sec = static_cast_check_fit(t / 1000 % 60); + if (sec != 0 || output.empty()) { + std::string s = time_suffix_seconds_; + char buffer[100]; + snprintf(buffer, sizeof(buffer), "%d", sec); + Utils::StringReplaceOne(&s, "${COUNT}", buffer); + if (!output.empty()) { + output += " "; + } + output += s; + } + } + } + if (is_negative) { + output = "-" + output; + } + return output; +} + void RootWidget::SetInboxCountText(const std::string& val) { assert(inbox_count_text_); @@ -1638,4 +1871,18 @@ void RootWidget::SetInboxCountText(const std::string& val) { } } +void RootWidget::PauseUpdates() { + assert(g_base->InLogicThread()); + // TODO(ericf): wire this up. + // printf("HELLO PAUSING\n"); + update_pause_count_ += 1; +} + +void RootWidget::ResumeUpdates() { + assert(g_base->InLogicThread()); + // TODO(ericf): wire this up. + // printf("HELLO RESUMING\n"); + update_pause_count_ -= 1; +} + } // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/widget/root_widget.h b/src/ballistica/ui_v1/widget/root_widget.h index 80c5266df..7296695e5 100644 --- a/src/ballistica/ui_v1/widget/root_widget.h +++ b/src/ballistica/ui_v1/widget/root_widget.h @@ -35,13 +35,14 @@ class RootWidget : public ContainerWidget { /// Called when UIScale or screen dimensions change. void OnUIScaleChange(); + void OnLanguageChange() override; void UpdateLayout() override; void SetSquadSizeLabel(int val); void SetAccountState(bool signed_in, const std::string& name); - void SetTicketsMeterText(const std::string& val); - void SetTokensMeterText(const std::string& val, bool gold_pass); - void SetLeagueRankText(const std::string& val); + void SetTicketsMeterValue(int val); + void SetTokensMeterValue(int val, bool gold_pass); + void SetLeagueRankValue(int val); void SetLeagueType(const std::string& val); void SetAchievementPercentText(const std::string& val); void SetLevelText(const std::string& val); @@ -50,11 +51,26 @@ class RootWidget : public ContainerWidget { void SetChests(const std::string& chest_0_appearance, const std::string& chest_1_appearance, const std::string& chest_2_appearance, - const std::string& chest_3_appearance); + const std::string& chest_3_appearance, + seconds_t chest_0_unlock_time, seconds_t chest_1_unlock_time, + seconds_t chest_2_unlock_time, seconds_t chest_3_unlock_time, + seconds_t chest_0_ad_allow_time, + seconds_t chest_1_ad_allow_time, + seconds_t chest_2_ad_allow_time, + seconds_t chest_3_ad_allow_time); void SetHaveLiveValues(bool have_live_values); auto bottom_left_height() const { return bottom_left_height_; } + /// Temporarily pause updates to things such as + /// ticket/token meters so they can be applied at a + /// set time or animated. + void PauseUpdates(); + + /// Resume updates to things such as ticket/token + /// meters. Snaps to the latest values. + void ResumeUpdates(); + private: struct ButtonDef_; struct Button_; @@ -65,13 +81,15 @@ class RootWidget : public ContainerWidget { enum class MeterType_ { kLevel, kTrophy, kTickets, kTokens }; enum class VAlign_ { kTop, kCenter, kBottom }; + auto GetTimeStr_(seconds_t diff) -> std::string; void UpdateChests_(); void UpdateTokensMeterText_(); void UpdateForFocusedWindow_(Widget* widget); auto AddButton_(const ButtonDef_& def) -> Button_*; auto AddText_(const TextDef_& def) -> Text_*; auto AddImage_(const ImageDef_& def) -> Image_*; - void StepChildWidgets_(float dt); + void StepChildWidgets_(seconds_t dt); + void StepChests_(); void AddMeter_(MeterType_ type, float h_align, float r, float g, float b, bool plus, const std::string& s); void UpdateTokensMeterTextColor_(); @@ -80,6 +98,9 @@ class RootWidget : public ContainerWidget { std::string chest_1_appearance_; std::string chest_2_appearance_; std::string chest_3_appearance_; + std::string time_suffix_hours_; + std::string time_suffix_minutes_; + std::string time_suffix_seconds_; std::list buttons_; std::list texts_; std::list images_; @@ -116,6 +137,10 @@ class RootWidget : public ContainerWidget { Image_* chest_1_lock_icon_{}; Image_* chest_2_lock_icon_{}; Image_* chest_3_lock_icon_{}; + Image_* chest_0_tv_icon_{}; + Image_* chest_1_tv_icon_{}; + Image_* chest_2_tv_icon_{}; + Image_* chest_3_tv_icon_{}; Text_* squad_size_text_{}; Text_* account_name_text_{}; Text_* tickets_meter_text_{}; @@ -129,14 +154,26 @@ class RootWidget : public ContainerWidget { Text_* chest_1_time_text_{}; Text_* chest_2_time_text_{}; Text_* chest_3_time_text_{}; + seconds_t chest_0_unlock_time_{-1.0}; + seconds_t chest_1_unlock_time_{-1.0}; + seconds_t chest_2_unlock_time_{-1.0}; + seconds_t chest_3_unlock_time_{-1.0}; + seconds_t chest_0_ad_allow_time_{-1.0}; + seconds_t chest_1_ad_allow_time_{-1.0}; + seconds_t chest_2_ad_allow_time_{-1.0}; + seconds_t chest_3_ad_allow_time_{-1.0}; + seconds_t last_chests_step_time_{-1.0f}; + seconds_t update_pause_time_{}; + seconds_t update_time_{}; float base_scale_{1.0f}; float bottom_left_height_{}; - millisecs_t update_time_{}; + int update_pause_count_{}; ToolbarVisibility toolbar_visibility_{ToolbarVisibility::kInGame}; bool child_widgets_dirty_{true}; bool in_main_menu_{}; bool gold_pass_{}; bool have_live_values_{}; + bool translations_dirty_{true}; }; } // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/widget/scroll_widget.cc b/src/ballistica/ui_v1/widget/scroll_widget.cc index 9a4b64d69..dc8dfc9db 100644 --- a/src/ballistica/ui_v1/widget/scroll_widget.cc +++ b/src/ballistica/ui_v1/widget/scroll_widget.cc @@ -201,7 +201,7 @@ auto ScrollWidget::HandleMessage(const base::WidgetMessage& m) -> bool { avg_scroll_speed_v_ = smoothing * avg_scroll_speed_v_ + (1.0f - smoothing) * 0.0f; } - last_sub_widget_h_scroll_claim_time_ = g_core->GetAppTimeMillisecs(); + last_sub_widget_h_scroll_claim_time_ = g_core->AppTimeMillisecs(); } pass = false; break; @@ -227,7 +227,7 @@ auto ScrollWidget::HandleMessage(const base::WidgetMessage& m) -> bool { // ignore vertical scrolling (should probably make this less fuzzy). bool ignore_regular_scrolling = false; bool child_claimed_h_scroll_recently = - (g_core->GetAppTimeMillisecs() - last_sub_widget_h_scroll_claim_time_ + (g_core->AppTimeMillisecs() - last_sub_widget_h_scroll_claim_time_ < 100); if (child_claimed_h_scroll_recently && std::abs(avg_scroll_speed_h_) > std::abs(avg_scroll_speed_v_)) @@ -569,9 +569,21 @@ void ScrollWidget::UpdateLayout() { amount_visible_ = 0; return; } - float child_h = (**i).GetHeight(); - child_max_offset_ = child_h - (height() - 2 * (border_height_ + V_MARGIN)); - amount_visible_ = (height() - 2 * (border_height_ + V_MARGIN)) / child_h; + + float extra_border_x{4.0}; // Whee arbitrary hard coded values. + float xoffs; + if (center_small_content_horizontally_) { + float our_width{width()}; + float child_width = (**i).GetWidth(); + xoffs = (our_width - child_width) * 0.5 - border_width_ - extra_border_x; + } else { + xoffs = extra_border_x + border_width_; + } + + float child_height = (**i).GetHeight(); + child_max_offset_ = + child_height - (height() - 2 * (border_height_ + V_MARGIN)); + amount_visible_ = (height() - 2 * (border_height_ + V_MARGIN)) / child_height; if (amount_visible_ > 1) { amount_visible_ = 1; if (center_small_content_) { @@ -585,8 +597,9 @@ void ScrollWidget::UpdateLayout() { if (mouse_held_thumb_) { if (child_offset_v_ - > child_h - (height() - 2 * (border_height_ + V_MARGIN))) { - child_offset_v_ = child_h - (height() - 2 * (border_height_ + V_MARGIN)); + > child_height - (height() - 2 * (border_height_ + V_MARGIN))) { + child_offset_v_ = + child_height - (height() - 2 * (border_height_ + V_MARGIN)); inertia_scroll_rate_ = 0; } if (child_offset_v_ < 0) { @@ -594,9 +607,9 @@ void ScrollWidget::UpdateLayout() { inertia_scroll_rate_ = 0; } } - (**i).set_translate(4 + border_width_, height() - (border_height_ + V_MARGIN) - + child_offset_v_smoothed_ - - child_h + center_offset_y_); + (**i).set_translate(xoffs, height() - (border_height_ + V_MARGIN) + + child_offset_v_smoothed_ - child_height + + center_offset_y_); thumb_dirty_ = true; } diff --git a/src/ballistica/ui_v1/widget/scroll_widget.h b/src/ballistica/ui_v1/widget/scroll_widget.h index cfee14dd3..812f0ea6f 100644 --- a/src/ballistica/ui_v1/widget/scroll_widget.h +++ b/src/ballistica/ui_v1/widget/scroll_widget.h @@ -32,6 +32,10 @@ class ScrollWidget : public ContainerWidget { center_small_content_ = val; MarkForUpdate(); } + auto set_center_small_content_horizontally(bool val) { + center_small_content_horizontally_ = val; + MarkForUpdate(); + } void OnTouchDelayTimerExpired(); auto set_color(float r, float g, float b) { color_red_ = r; @@ -106,6 +110,7 @@ class ScrollWidget : public ContainerWidget { bool glow_dirty_{true}; bool thumb_dirty_{true}; bool center_small_content_{}; + bool center_small_content_horizontally_{}; bool touch_held_{}; bool highlight_{true}; bool capture_arrows_{false}; diff --git a/src/ballistica/ui_v1/widget/spinner_widget.cc b/src/ballistica/ui_v1/widget/spinner_widget.cc new file mode 100644 index 000000000..e851f0e47 --- /dev/null +++ b/src/ballistica/ui_v1/widget/spinner_widget.cc @@ -0,0 +1,58 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui_v1/widget/spinner_widget.h" + +#include +#include + +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/base.h" +#include "ballistica/base/graphics/component/simple_component.h" + +namespace ballistica::ui_v1 { + +SpinnerWidget::SpinnerWidget() {} +SpinnerWidget::~SpinnerWidget() = default; + +auto SpinnerWidget::GetWidth() -> float { return size_; } +auto SpinnerWidget::GetHeight() -> float { return size_; } + +void SpinnerWidget::Draw(base::RenderPass* pass, bool draw_transparent) { + seconds_t current_time = pass->frame_def()->display_time(); + + // We only draw in transparent pass. + if (!draw_transparent) { + return; + } + + // Fade presence in any time we're visible and out any time we're not. + if (visible_) { + presence_ = std::min( + 1.0, presence_ + pass->frame_def()->display_time_elapsed() * 1.0); + } else { + presence_ = std::max( + 0.0, presence_ - pass->frame_def()->display_time_elapsed() * 2.0); + // Also don't draw anything in this case. + return; + } + + auto alpha{std::max(0.0, std::min(1.0, presence_ * 2.0 - 1.0))}; + + base::SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(1.0f, 1.0f, 1.0f, alpha); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kSpinner)); + { + auto xf = c.ScopedTransform(); + c.Scale(size_, size_, 1.0f); + c.Rotate(-360.0f * std::fmod(current_time * 2.0, 1.0), 0.0f, 0.0f, 1.0f); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); + } + c.Submit(); +} + +auto SpinnerWidget::HandleMessage(const base::WidgetMessage& m) -> bool { + return false; +} + +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/widget/spinner_widget.h b/src/ballistica/ui_v1/widget/spinner_widget.h new file mode 100644 index 000000000..ab724dc34 --- /dev/null +++ b/src/ballistica/ui_v1/widget/spinner_widget.h @@ -0,0 +1,36 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_V1_WIDGET_SPINNER_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_SPINNER_WIDGET_H_ + +#include + +#include "ballistica/ui_v1/widget/widget.h" + +namespace ballistica::ui_v1 { + +class SpinnerWidget : public Widget { + public: + SpinnerWidget(); + ~SpinnerWidget() override; + void Draw(base::RenderPass* pass, bool transparent) override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; + void set_size(float size) { size_ = size; } + + /// Setting the visibility attr on a spinner will cause it to fade in + /// gradually when made visible. Setting visible-in-container will not + /// have this effect. + void set_visible(bool val) { visible_ = val; } + auto GetWidth() -> float override; + auto GetHeight() -> float override; + auto GetWidgetTypeName() -> std::string override { return "spinner"; } + + private: + float size_{32.0f}; + float presence_{}; + bool visible_{true}; +}; + +} // namespace ballistica::ui_v1 + +#endif // BALLISTICA_UI_V1_WIDGET_SPINNER_WIDGET_H_ diff --git a/src/ballistica/ui_v1/widget/text_widget.cc b/src/ballistica/ui_v1/widget/text_widget.cc index c4a3fde06..f96733e21 100644 --- a/src/ballistica/ui_v1/widget/text_widget.cc +++ b/src/ballistica/ui_v1/widget/text_widget.cc @@ -547,8 +547,7 @@ void TextWidget::SetText(const std::string& text_in_raw) { if (do_format_check) { bool valid; - g_base->assets->CompileResourceString( - text_in_raw, "TextWidget::set_text format check", &valid); + g_base->assets->CompileResourceString(text_in_raw, &valid); if (!valid) { BA_LOG_ONCE(LogName::kBa, LogLevel::kError, "Invalid resource string: '" + text_in_raw + "'"); @@ -951,8 +950,7 @@ void TextWidget::UpdateTranslation_() { if (editable()) { text_translated_ = text_raw_; } else { - text_translated_ = g_base->assets->CompileResourceString( - text_raw_, "TextWidget::UpdateTranslation"); + text_translated_ = g_base->assets->CompileResourceString(text_raw_); } text_translation_dirty_ = false; text_group_dirty_ = true; diff --git a/src/meta/baclassicmeta/pyembed/binding_classic.py b/src/meta/baclassicmeta/pyembed/binding_classic.py index 04af0f5b5..61c678780 100644 --- a/src/meta/baclassicmeta/pyembed/binding_classic.py +++ b/src/meta/baclassicmeta/pyembed/binding_classic.py @@ -6,9 +6,15 @@ from baclassic._music import do_play_music from baclassic._input import get_input_device_mapped_value +from baclassic._chest import ( + CHEST_APPEARANCE_DISPLAY_INFOS, + CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, +) # The C++ layer looks for this variable: values = [ do_play_music, # kDoPlayMusicCall get_input_device_mapped_value, # kGetInputDeviceMappedValueCall + CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, # kChestAppearanceDisplayInfoDefault + CHEST_APPEARANCE_DISPLAY_INFOS, # kChestAppearanceDisplayInfos ] diff --git a/tests/test_efro/test_dataclassio.py b/tests/test_efro/test_dataclassio.py index a24e56176..6cc0f0565 100644 --- a/tests/test_efro/test_dataclassio.py +++ b/tests/test_efro/test_dataclassio.py @@ -1129,6 +1129,74 @@ class _TestClassE8: assert dataclass_from_dict(_TestClassE8, todict) == orig +def test_enum_fallback() -> None: + """Test enum_fallback IOAttr values.""" + # pylint: disable=missing-class-docstring + # pylint: disable=unused-variable + + @ioprepped + @dataclass + class TestClass: + + class TestEnum1(Enum): + VAL1 = 'val1' + VAL2 = 'val2' + VAL3 = 'val3' + + class TestEnum2(Enum): + VAL1 = 'val1' + VAL2 = 'val2' + VAL3 = 'val3' + + enum1val: Annotated[TestEnum1, IOAttrs('e1')] + enum2val: Annotated[ + TestEnum2, IOAttrs('e2', enum_fallback=TestEnum2.VAL1) + ] + + # All valid values; should work. + _obj = dataclass_from_dict(TestClass, {'e1': 'val1', 'e2': 'val1'}) + + # Bad Enum1 value; should fail since there's no fallback. + with pytest.raises(ValueError): + _obj = dataclass_from_dict(TestClass, {'e1': 'val4', 'e2': 'val1'}) + + # Bad Enum2 value; the attr provides a fallback but still should + # fail since we didn't explicitly specify lossy loading. + with pytest.raises(ValueError): + obj = dataclass_from_dict(TestClass, {'e1': 'val1', 'e2': 'val4'}) + + # Bad Enum2 value; should successfully substitute our fallback value + # since we specify lossy loading. + obj_w_fb = dataclass_from_dict( + TestClass, {'e1': 'val1', 'e2': 'val4'}, lossy=True + ) + assert obj_w_fb.enum2val is obj_w_fb.TestEnum2.VAL1 + + # Allowing fallbacks means data might be lost on any load, so we + # disallow writes for such data to be safe. + with pytest.raises(ValueError): + dataclass_to_dict(obj_w_fb) + + # Using wrong type as enum_fallback should fail. + with pytest.raises(TypeError): + + @ioprepped + @dataclass + class TestClass2: + + class TestEnum1(Enum): + VAL1 = 'val1' + VAL2 = 'val2' + + class TestEnum2(Enum): + VAL1 = 'val1' + VAL2 = 'val2' + + enum1val: Annotated[ + TestEnum1, IOAttrs('e1', enum_fallback=TestEnum2.VAL1) + ] + + class MTTestTypeID(Enum): """IDs for our multi-type class.""" @@ -1460,56 +1528,295 @@ def test_multi_type_2() -> None: val3 = dataclass_from_dict(MTTest2Base, indict3) -def test_enum_fallback() -> None: - """Test enum_fallback IOAttr values.""" - # pylint: disable=missing-class-docstring - # pylint: disable=unused-variable +# Define 2 variations of Test3 - an 'old' and 'new' one - to simulate +# older/newer versions of the same schema. +class MTTest3OldTypeID(Enum): + """IDs for our multi-type class.""" - @ioprepped - @dataclass - class TestClass: + CLASS_1 = 'm1' + CLASS_2 = 'm2' - class TestEnum1(Enum): - VAL1 = 'val1' - VAL2 = 'val2' - VAL3 = 'val3' - class TestEnum2(Enum): - VAL1 = 'val1' - VAL2 = 'val2' - VAL3 = 'val3' +class MTTest3OldBase(IOMultiType[MTTest3OldTypeID]): + """Our multi-type class. - enum1val: Annotated[TestEnum1, IOAttrs('e1')] - enum2val: Annotated[ - TestEnum2, IOAttrs('e2', enum_fallback=TestEnum2.VAL1) - ] + These top level multi-type classes are special parent classes + that know about all of their child classes and how to serialize + & deserialize them using explicit type ids. We can then use the + parent class in annotations and dataclassio will do the right thing. + Useful for stuff like Message classes where we may want to store a + bunch of different types of them into one place. + """ - # All valid values; should work. - _obj = dataclass_from_dict(TestClass, {'e1': 'val1', 'e2': 'val1'}) + @override + @classmethod + def get_type(cls, type_id: MTTest3OldTypeID) -> type[MTTest3OldBase]: + """Return the subclass for each of our type-ids.""" - # Bad Enum1 value; should fail since there's no fallback. - with pytest.raises(ValueError): - _obj = dataclass_from_dict(TestClass, {'e1': 'val4', 'e2': 'val1'}) + # This uses assert_never() to ensure we cover all cases in the + # enum. Though this is less efficient than looking up by dict + # would be. If we had lots of values we could also support lazy + # loading by importing classes only when their value is being + # requested. + val: type[MTTest3OldBase] + if type_id is MTTest3OldTypeID.CLASS_1: + val = MTTest3OldClass1 + elif type_id is MTTest3OldTypeID.CLASS_2: + val = MTTest3OldClass2 + else: + assert_never(type_id) + return val - # Bad Enum2 value; should substitute our fallback value. - obj = dataclass_from_dict(TestClass, {'e1': 'val1', 'e2': 'val4'}) - assert obj.enum2val is obj.TestEnum2.VAL1 + @override + @classmethod + def get_type_id(cls) -> MTTest3OldTypeID: + """Provide the type-id for this subclass.""" + # If we wanted, we could just maintain a static mapping of + # types-to-ids here, but there are benefits to letting each + # child class speak for itself. Namely that we can do + # lazy-loading and don't need to have all types present here. - # Using wrong type as enum_fallback should fail. - with pytest.raises(TypeError): + # So we'll let all our child classes override this. + raise NotImplementedError() - @ioprepped - @dataclass - class TestClass2: + @override + @classmethod + def get_unknown_type_fallback(cls) -> MTTest3OldBase | None: + # Define a fallback here that can be returned in cases of + # unrecognized types (though only if 'lossy' is enabled for the + # load). + return MTTest3OldClass1(ival=42) - class TestEnum1(Enum): - VAL1 = 'val1' - VAL2 = 'val2' - class TestEnum2(Enum): - VAL1 = 'val1' - VAL2 = 'val2' +@ioprepped +@dataclass +class MTTest3OldClass1(MTTest3OldBase): + """A test child-class for use with our multi-type class.""" - enum1val: Annotated[ - TestEnum1, IOAttrs('e1', enum_fallback=TestEnum2.VAL1) - ] + ival: int + + @override + @classmethod + def get_type_id(cls) -> MTTest3OldTypeID: + return MTTest3OldTypeID.CLASS_1 + + +@ioprepped +@dataclass +class MTTest3OldClass2(MTTest3OldBase): + """Another test child-class for use with our multi-type class.""" + + sval: str + + @override + @classmethod + def get_type_id(cls) -> MTTest3OldTypeID: + return MTTest3OldTypeID.CLASS_2 + + +@ioprepped +@dataclass +class MTTest3OldWrapper: + """Testing something *containing* a test class instance.""" + + child: MTTest3OldBase + + +@ioprepped +@dataclass +class MTTest3OldListWrapper: + """Testing something *containing* a test class instance.""" + + children: list[MTTest3OldBase] + + +class MTTest3NewTypeID(Enum): + """IDs for our multi-type class.""" + + CLASS_1 = 'm1' + CLASS_2 = 'm2' + CLASS_3 = 'm3' + + +class MTTest3NewBase(IOMultiType[MTTest3NewTypeID]): + """Our multi-type class. + + These top level multi-type classes are special parent classes + that know about all of their child classes and how to serialize + & deserialize them using explicit type ids. We can then use the + parent class in annotations and dataclassio will do the right thing. + Useful for stuff like Message classes where we may want to store a + bunch of different types of them into one place. + """ + + @override + @classmethod + def get_type(cls, type_id: MTTest3NewTypeID) -> type[MTTest3NewBase]: + """Return the subclass for each of our type-ids.""" + + # This uses assert_never() to ensure we cover all cases in the + # enum. Though this is less efficient than looking up by dict + # would be. If we had lots of values we could also support lazy + # loading by importing classes only when their value is being + # requested. + val: type[MTTest3NewBase] + if type_id is MTTest3NewTypeID.CLASS_1: + val = MTTest3NewClass1 + elif type_id is MTTest3NewTypeID.CLASS_2: + val = MTTest3NewClass2 + elif type_id is MTTest3NewTypeID.CLASS_3: + val = MTTest3NewClass3 + else: + assert_never(type_id) + return val + + @override + @classmethod + def get_type_id(cls) -> MTTest3NewTypeID: + """Provide the type-id for this subclass.""" + # If we wanted, we could just maintain a static mapping of + # types-to-ids here, but there are benefits to letting each + # child class speak for itself. Namely that we can do + # lazy-loading and don't need to have all types present here. + + # So we'll let all our child classes override this. + raise NotImplementedError() + + @override + @classmethod + def get_unknown_type_fallback(cls) -> MTTest3NewBase | None: + # Define a fallback here that can be returned in cases of + # unrecognized types (though only if 'lossy' is enabled for the + # load). + return MTTest3NewClass1(ival=43) + + +@ioprepped +@dataclass +class MTTest3NewClass1(MTTest3NewBase): + """A test child-class for use with our multi-type class.""" + + ival: int + + @override + @classmethod + def get_type_id(cls) -> MTTest3NewTypeID: + return MTTest3NewTypeID.CLASS_1 + + +@ioprepped +@dataclass +class MTTest3NewClass2(MTTest3NewBase): + """Another test child-class for use with our multi-type class.""" + + sval: str + + @override + @classmethod + def get_type_id(cls) -> MTTest3NewTypeID: + return MTTest3NewTypeID.CLASS_2 + + +@ioprepped +@dataclass +class MTTest3NewClass3(MTTest3NewBase): + """Another test child-class for use with our multi-type class.""" + + bval: bool + + @override + @classmethod + def get_type_id(cls) -> MTTest3NewTypeID: + return MTTest3NewTypeID.CLASS_3 + + +@ioprepped +@dataclass +class MTTest3NewWrapper: + """Testing something *containing* a test class instance.""" + + child: MTTest3NewBase + + +@ioprepped +@dataclass +class MTTest3NewListWrapper: + """Testing something *containing* a test class instance.""" + + children: list[MTTest3NewBase] + + +def test_multi_type_3() -> None: + """Test IOMultiType stuff.""" + + # Define some data using our 'newer' schema and it should load using + # our 'older' one. + data2 = dataclass_to_dict(MTTest3NewClass2(sval='foof')) + obj2 = dataclass_from_dict(MTTest3OldBase, data2) + assert isinstance(obj2, MTTest3OldClass2) + + # However, this won't work with class 3 which only exists in the + # 'newer' schema. So this should fail. + data3 = dataclass_to_dict(MTTest3NewClass3(bval=True)) + with pytest.raises(ValueError): + obj3 = dataclass_from_dict(MTTest3OldBase, data3) + + # Running in lossy mode should succeed, however, since we define a + # fallback call on our multitype. The fallback should give us a + # particular MTTestClass1. + obj3 = dataclass_from_dict(MTTest3OldBase, data3, lossy=True) + assert obj3 == MTTest3OldClass1(ival=42) + + # ---------------------------------------------------------------- + # Now do the same tests with a dataclass *containing* one of these + # dataclasses (since this goes through a different code path). + # ---------------------------------------------------------------- + + # Define some data using our 'newer' schema and it should load using + # our 'older' one. + wdata2 = dataclass_to_dict( + MTTest3NewWrapper(child=MTTest3NewClass2(sval='foof')) + ) + wobj2 = dataclass_from_dict(MTTest3OldWrapper, wdata2) + assert isinstance(wobj2, MTTest3OldWrapper) + assert isinstance(wobj2.child, MTTest3OldClass2) + + # However, this won't work with class 3 which only exists in the + # 'newer' schema. So this should fail. + wdata3 = dataclass_to_dict(MTTest3NewWrapper(MTTest3NewClass3(bval=True))) + with pytest.raises(ValueError): + wobj3 = dataclass_from_dict(MTTest3OldWrapper, wdata3) + + # Running in lossy mode should succeed, however, since we define a + # fallback call on our multitype. The fallback should give us a + # particular MTTestClass1. + wobj3 = dataclass_from_dict(MTTest3OldWrapper, wdata3, lossy=True) + assert wobj3 == MTTest3OldWrapper(child=MTTest3OldClass1(ival=42)) + + # ---------------------------------------------------------------- + # Once more with a dataclass containing a *sequence* of these, which + # is a slightly different code path again. + # ---------------------------------------------------------------- + + # Define some data using our 'newer' schema and it should load using + # our 'older' one. + wldata2 = dataclass_to_dict( + MTTest3NewListWrapper(children=[MTTest3NewClass2(sval='foof')]) + ) + wlobj2 = dataclass_from_dict(MTTest3OldListWrapper, wldata2) + assert isinstance(wlobj2, MTTest3OldListWrapper) + assert isinstance(wlobj2.children[0], MTTest3OldClass2) + + # However, this won't work with class 3 which only exists in the + # 'newer' schema. So this should fail. + wldata3 = dataclass_to_dict( + MTTest3NewListWrapper([MTTest3NewClass3(bval=True)]) + ) + with pytest.raises(ValueError): + wlobj3 = dataclass_from_dict(MTTest3OldListWrapper, wldata3) + + # Running in lossy mode should succeed, however, since we define a + # fallback call on our multitype. The fallback should give us a + # particular MTTestClass1. + wlobj3 = dataclass_from_dict(MTTest3OldListWrapper, wldata3, lossy=True) + assert wlobj3 == MTTest3OldListWrapper(children=[MTTest3OldClass1(ival=42)]) diff --git a/tools/bacommon/bs.py b/tools/bacommon/bs.py new file mode 100644 index 000000000..7d673b805 --- /dev/null +++ b/tools/bacommon/bs.py @@ -0,0 +1,733 @@ +# Released under the MIT License. See LICENSE for details. +# +"""BombSquad specific bits.""" + +from __future__ import annotations + +import datetime +from enum import Enum +from dataclasses import dataclass, field +from typing import Annotated, override, assert_never + +from efro.dataclassio import ioprepped, IOAttrs, IOMultiType +from efro.message import Message, Response + + +@ioprepped +@dataclass +class PrivatePartyMessage(Message): + """Message asking about info we need for private-party UI.""" + + need_datacode: Annotated[bool, IOAttrs('d')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [PrivatePartyResponse] + + +@ioprepped +@dataclass +class PrivatePartyResponse(Response): + """Here's that private party UI info you asked for, boss.""" + + success: Annotated[bool, IOAttrs('s')] + tokens: Annotated[int, IOAttrs('t')] + gold_pass: Annotated[bool, IOAttrs('g')] + datacode: Annotated[str | None, IOAttrs('d')] + + +class ClassicChestAppearance(Enum): + """Appearances bombsquad classic chests can have.""" + + UNKNOWN = 'u' + DEFAULT = 'd' + L1 = 'l1' + L2 = 'l2' + L3 = 'l3' + L4 = 'l4' + L5 = 'l5' + L6 = 'l6' + + +@ioprepped +@dataclass +class ClassicAccountLiveData: + """Live account data fed to the client in the bs classic app mode.""" + + @dataclass + class Chest: + """A lovely chest.""" + + appearance: Annotated[ + ClassicChestAppearance, + IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN), + ] + unlock_time: Annotated[datetime.datetime, IOAttrs('t')] + ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')] + + class LeagueType(Enum): + """Type of league we are in.""" + + BRONZE = 'b' + SILVER = 's' + GOLD = 'g' + DIAMOND = 'd' + + tickets: Annotated[int, IOAttrs('ti')] + + tokens: Annotated[int, IOAttrs('to')] + gold_pass: Annotated[bool, IOAttrs('g')] + + achievements: Annotated[int, IOAttrs('a')] + achievements_total: Annotated[int, IOAttrs('at')] + + league_type: Annotated[LeagueType | None, IOAttrs('lt')] + league_num: Annotated[int | None, IOAttrs('ln')] + league_rank: Annotated[int | None, IOAttrs('lr')] + + level: Annotated[int, IOAttrs('lv')] + xp: Annotated[int, IOAttrs('xp')] + xpmax: Annotated[int, IOAttrs('xpm')] + + inbox_count: Annotated[int, IOAttrs('ibc')] + inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')] + + chests: Annotated[dict[str, Chest], IOAttrs('c')] + + +class DisplayItemTypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + TICKETS = 't' + TOKENS = 'k' + + +class DisplayItem(IOMultiType[DisplayItemTypeID]): + """Some amount of something that can be shown or described. + + Used to depict chest contents or other rewards or prices. + """ + + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + out: type[DisplayItem] + + t = DisplayItemTypeID + if type_id is t.UNKNOWN: + out = UnknownDisplayItem + elif type_id is t.TICKETS: + out = TicketsDisplayItem + elif type_id is t.TOKENS: + out = TokensDisplayItem + else: + # Important to make sure we provide all types. + assert_never(type_id) + return out + + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + """Return a string description and subs for the item. + + These decriptions are baked into the DisplayItemWrapper and + should be accessed from there by the client. This should only be + called on the server side when doing said baking. + """ + raise NotImplementedError() + + # Implement fallbacks so client can digest item lists even if they + # contain unrecognized stuff. DisplayItemWrapper contains basic + # baked down info that they can still use in such cases. + @override + @classmethod + def get_unknown_type_fallback(cls) -> DisplayItem: + return UnknownDisplayItem() + + +@ioprepped +@dataclass +class UnknownDisplayItem(DisplayItem): + """Something we don't know how to display.""" + + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.UNKNOWN + + @override + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + import logging + + # Make noise but don't break. + logging.exception( + 'UnknownDisplayItem.get_description() should never be called.' + ' Always access descriptions on the DisplayItemWrapper.' + ) + return 'Unknown', [] + + +@ioprepped +@dataclass +class TicketsDisplayItem(DisplayItem): + """Some amount of tickets.""" + + count: Annotated[int, IOAttrs('c')] + + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.TICKETS + + @override + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + return '${C} Tickets', [('${C}', str(self.count))] + + +@ioprepped +@dataclass +class TokensDisplayItem(DisplayItem): + """Some amount of tokens.""" + + count: Annotated[int, IOAttrs('c')] + + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.TOKENS + + @override + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + return '${C} Tokens', [('${C}', str(self.count))] + + +@ioprepped +@dataclass +class DisplayItemWrapper: + """Wraps a DisplayItem and common info.""" + + item: Annotated[DisplayItem, IOAttrs('i')] + description: Annotated[str, IOAttrs('d')] + description_subs: Annotated[list[str] | None, IOAttrs('s')] + + @classmethod + def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper: + """Convenience method to wrap a DisplayItem.""" + desc, subs = item.get_description() + # Flatten subs to single list. + flat_subs = [item for pair in subs for item in pair] + return DisplayItemWrapper(item, desc, flat_subs) + + +@ioprepped +@dataclass +class ChestInfoMessage(Message): + """Request info about a chest.""" + + chest_id: Annotated[str, IOAttrs('i')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [ChestInfoResponse] + + +@ioprepped +@dataclass +class ChestInfoResponse(Response): + """Here's that chest info you asked for, boss.""" + + @dataclass + class Chest: + """A lovely chest.""" + + @dataclass + class PrizeSet: + """A possible set of prizes for this chest.""" + + weight: Annotated[float, IOAttrs('w')] + contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')] + + appearance: Annotated[ + ClassicChestAppearance, + IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN), + ] + + # How much to unlock *now*. + unlock_tokens: Annotated[int, IOAttrs('tk')] + + # When unlocks on its own. + unlock_time: Annotated[datetime.datetime, IOAttrs('t')] + + # Possible prizes we contain. + prizesets: Annotated[list[PrizeSet], IOAttrs('p')] + + # Are ads allowed now? + ad_allow: Annotated[bool, IOAttrs('aa')] + + chest: Annotated[Chest | None, IOAttrs('c')] + user_tokens: Annotated[int | None, IOAttrs('t')] + + +@ioprepped +@dataclass +class ChestActionMessage(Message): + """Request action about a chest.""" + + class Action(Enum): + """Types of actions we can request.""" + + # Unlocking (for free or with tokens). + UNLOCK = 'u' + + # Watched an ad to reduce wait. + AD = 'ad' + + action: Annotated[Action, IOAttrs('a')] + + # Tokens we are paying (only applies to unlock). + token_payment: Annotated[int, IOAttrs('t')] + + chest_id: Annotated[str, IOAttrs('i')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [ChestActionResponse] + + +@ioprepped +@dataclass +class ChestActionResponse(Response): + """Here's the results of that action you asked for, boss.""" + + # Tokens that were actually charged. + tokens_charged: Annotated[int, IOAttrs('t')] = 0 + + # If present, signifies the chest has been opened and we should show + # the user this stuff that was in it. + contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None + + # If contents are present, which of the chest's prize-sets they + # represent. + prizeindex: Annotated[int, IOAttrs('i')] = 0 + + # Printable error if something goes wrong. + error: Annotated[str | None, IOAttrs('e')] = None + + # Printable warning. Shown in orange with an error sound. Does not + # mean the action failed; only that there's something to tell the + # users such as 'It looks like you are faking ad views; stop it or + # you won't have ad options anymore.' + warning: Annotated[str | None, IOAttrs('w')] = None + + # Printable success message. Shown in green with a cash-register + # sound. Can be used for things like successful wait reductions via + # ad views. + success_msg: Annotated[str | None, IOAttrs('s')] = None + + +class ClientUITypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + BASIC = 'b' + + +class ClientUI(IOMultiType[ClientUITypeID]): + """Defines some user interface on the client.""" + + @override + @classmethod + def get_type_id(cls) -> ClientUITypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + out: type[ClientUI] + + t = ClientUITypeID + if type_id is t.UNKNOWN: + out = UnknownClientUI + elif type_id is t.BASIC: + out = BasicClientUI + else: + # Important to make sure we provide all types. + assert_never(type_id) + return out + + @override + @classmethod + def get_unknown_type_fallback(cls) -> ClientUI: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return UnknownClientUI() + + +@ioprepped +@dataclass +class UnknownClientUI(ClientUI): + """Fallback type for unrecognized entries.""" + + @override + @classmethod + def get_type_id(cls) -> ClientUITypeID: + return ClientUITypeID.UNKNOWN + + +class BasicClientUIComponentTypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + TEXT = 't' + + +class BasicClientUIComponent(IOMultiType[BasicClientUIComponentTypeID]): + """Top level class for our multitype.""" + + @override + @classmethod + def get_type_id(cls) -> BasicClientUIComponentTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type( + cls, type_id: BasicClientUIComponentTypeID + ) -> type[BasicClientUIComponent]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + + t = BasicClientUIComponentTypeID + if type_id is t.UNKNOWN: + return BasicClientUIComponentUnknown + if type_id is t.TEXT: + return BasicClientUIComponentText + # if type_id is t.SCREEN_MESSAGE: + # return BasicClientUIComponentScreenMessage + + # Important to make sure we provide all types. + assert_never(type_id) + + @override + @classmethod + def get_unknown_type_fallback(cls) -> BasicClientUIComponent: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return BasicClientUIComponentUnknown() + + +@ioprepped +@dataclass +class BasicClientUIComponentUnknown(BasicClientUIComponent): + """An unknown basic client component type. + + In practice these should never show up since the master-server + generates these on the fly for the client and so should not send + clients one they can't digest. + """ + + @override + @classmethod + def get_type_id(cls) -> BasicClientUIComponentTypeID: + return BasicClientUIComponentTypeID.UNKNOWN + + +@ioprepped +@dataclass +class BasicClientUIComponentText(BasicClientUIComponent): + """Show some text in the inbox message.""" + + text: Annotated[str, IOAttrs('t')] + subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field( + default_factory=list + ) + scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0 + color: Annotated[ + tuple[float, float, float, float], IOAttrs('c', store_default=False) + ] = ( + 1.0, + 1.0, + 1.0, + 1.0, + ) + spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 + spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 + + @override + @classmethod + def get_type_id(cls) -> BasicClientUIComponentTypeID: + return BasicClientUIComponentTypeID.TEXT + + +@ioprepped +@dataclass +class BasicClientUI(ClientUI): + """A basic UI for the client.""" + + class ButtonLabel(Enum): + """Distinct button labels we support.""" + + UNKNOWN = 'u' + OK = 'o' + APPLY = 'a' + CANCEL = 'c' + ACCEPT = 'ac' + DECLINE = 'dn' + IGNORE = 'ig' + CLAIM = 'cl' + DISCARD = 'd' + + class InteractionStyle(Enum): + """Overall interaction styles we support.""" + + UNKNOWN = 'u' + BUTTON_POSITIVE = 'p' + BUTTON_POSITIVE_NEGATIVE = 'pn' + + components: Annotated[list[BasicClientUIComponent], IOAttrs('s')] + + interaction_style: Annotated[ + InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN) + ] = InteractionStyle.BUTTON_POSITIVE + + button_label_positive: Annotated[ + ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN) + ] = ButtonLabel.OK + + button_label_negative: Annotated[ + ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN) + ] = ButtonLabel.CANCEL + + @override + @classmethod + def get_type_id(cls) -> ClientUITypeID: + return ClientUITypeID.BASIC + + def contains_unknown_elements(self) -> bool: + """Whether something within us is an unknown type or enum.""" + return ( + self.interaction_style is self.InteractionStyle.UNKNOWN + or self.button_label_positive is self.ButtonLabel.UNKNOWN + or self.button_label_negative is self.ButtonLabel.UNKNOWN + or any( + c.get_type_id() is BasicClientUIComponentTypeID.UNKNOWN + for c in self.components + ) + ) + + +@ioprepped +@dataclass +class ClientUIWrapper: + """Wrapper for a ClientUI and its common data.""" + + id: Annotated[str, IOAttrs('i')] + createtime: Annotated[datetime.datetime, IOAttrs('c')] + ui: Annotated[ClientUI, IOAttrs('e')] + + +@ioprepped +@dataclass +class InboxRequestMessage(Message): + """Message requesting our inbox.""" + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [InboxRequestResponse] + + +@ioprepped +@dataclass +class InboxRequestResponse(Response): + """Here's that inbox contents you asked for, boss.""" + + wrappers: Annotated[list[ClientUIWrapper], IOAttrs('w')] + + # Printable error if something goes wrong. + error: Annotated[str | None, IOAttrs('e')] = None + + +class ClientUIAction(Enum): + """Types of actions we can run.""" + + BUTTON_PRESS_POSITIVE = 'p' + BUTTON_PRESS_NEGATIVE = 'n' + + +class ClientEffectTypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + SCREEN_MESSAGE = 'm' + SOUND = 's' + DELAY = 'd' + + +class ClientEffect(IOMultiType[ClientEffectTypeID]): + """Something that can happen on the client. + + This can include screen messages, sounds, visual effects, etc. + """ + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + + t = ClientEffectTypeID + if type_id is t.UNKNOWN: + return ClientEffectUnknown + if type_id is t.SCREEN_MESSAGE: + return ClientEffectScreenMessage + if type_id is t.SOUND: + return ClientEffectSound + if type_id is t.DELAY: + return ClientEffectDelay + + # Important to make sure we provide all types. + assert_never(type_id) + + @override + @classmethod + def get_unknown_type_fallback(cls) -> ClientEffect: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return ClientEffectUnknown() + + +@ioprepped +@dataclass +class ClientEffectUnknown(ClientEffect): + """Fallback substitute for types we don't recognize.""" + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.UNKNOWN + + +@ioprepped +@dataclass +class ClientEffectScreenMessage(ClientEffect): + """Display a screen-message.""" + + message: Annotated[str, IOAttrs('m')] + + # Note: Firestore can't store arrays of arrays so we flatten it to a + # single dimension. + subs: Annotated[list[str], IOAttrs('s')] + color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0) + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.SCREEN_MESSAGE + + +@ioprepped +@dataclass +class ClientEffectSound(ClientEffect): + """Play a sound.""" + + class Sound(Enum): + """Sounds that can be made alongside the message.""" + + UNKNOWN = 'u' + CASH_REGISTER = 'c' + ERROR = 'e' + POWER_DOWN = 'p' + GUN_COCKING = 'g' + + sound: Annotated[Sound, IOAttrs('s', enum_fallback=Sound.UNKNOWN)] + volume: Annotated[float, IOAttrs('v')] = 1.0 + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.SOUND + + +@ioprepped +@dataclass +class ClientEffectDelay(ClientEffect): + """Delay effect processing.""" + + seconds: Annotated[float, IOAttrs('s')] + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.DELAY + + +@ioprepped +@dataclass +class ClientUIActionMessage(Message): + """Do something to a client ui.""" + + id: Annotated[str, IOAttrs('i')] + action: Annotated[ClientUIAction, IOAttrs('a')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [ClientUIActionResponse] + + +@ioprepped +@dataclass +class ClientUIActionResponse(Response): + """Did something to that inbox entry, boss.""" + + class ErrorType(Enum): + """Types of errors that may have occurred.""" + + # Probably a future error type we don't recognize. + UNKNOWN = 'u' + + # Something went wrong on the server, but specifics are not + # relevant. + INTERNAL = 'i' + + # The entry expired on the server. In various cases such as 'ok' + # buttons this can generally be ignored. + EXPIRED = 'e' + + error_type: Annotated[ + ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN) + ] + + # User facing error message in the case of errors. + error_message: Annotated[str | None, IOAttrs('em')] + + effects: Annotated[list[ClientEffect], IOAttrs('fx')] diff --git a/tools/bacommon/cloud.py b/tools/bacommon/cloud.py index fbd28701e..0d8aaf2e2 100644 --- a/tools/bacommon/cloud.py +++ b/tools/bacommon/cloud.py @@ -4,7 +4,6 @@ from __future__ import annotations -import datetime from enum import Enum from dataclasses import dataclass, field from typing import TYPE_CHECKING, Annotated, override @@ -301,240 +300,3 @@ class Purchase: available_purchases: Annotated[list[Purchase], IOAttrs('p')] token_info_url: Annotated[str, IOAttrs('tiu')] - - -@ioprepped -@dataclass -class BSPrivatePartyMessage(Message): - """Message asking about info we need for private-party UI.""" - - need_datacode: Annotated[bool, IOAttrs('d')] - - @override - @classmethod - def get_response_types(cls) -> list[type[Response] | None]: - return [BSPrivatePartyResponse] - - -@ioprepped -@dataclass -class BSPrivatePartyResponse(Response): - """Here's that private party UI info you asked for, boss.""" - - success: Annotated[bool, IOAttrs('s')] - tokens: Annotated[int, IOAttrs('t')] - gold_pass: Annotated[bool, IOAttrs('g')] - datacode: Annotated[str | None, IOAttrs('d')] - - -class BSClassicChestAppearance(Enum): - """Appearances bombsquad classic chests can have.""" - - UNKNOWN = 'u' - DEFAULT = 'd' - - -@ioprepped -@dataclass -class BSClassicAccountLiveData: - """Account related data kept up to date live for classic app mode.""" - - @dataclass - class Chest: - """A lovely chest.""" - - appearance: Annotated[ - BSClassicChestAppearance, - IOAttrs('a', enum_fallback=BSClassicChestAppearance.UNKNOWN), - ] - unlock_time: Annotated[datetime.datetime, IOAttrs('t')] - ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')] - - class LeagueType(Enum): - """Type of league we are in.""" - - BRONZE = 'b' - SILVER = 's' - GOLD = 'g' - DIAMOND = 'd' - - tickets: Annotated[int, IOAttrs('ti')] - - tokens: Annotated[int, IOAttrs('to')] - gold_pass: Annotated[bool, IOAttrs('g')] - - achievements: Annotated[int, IOAttrs('a')] - achievements_total: Annotated[int, IOAttrs('at')] - - league_type: Annotated[LeagueType | None, IOAttrs('lt')] - league_num: Annotated[int | None, IOAttrs('ln')] - league_rank: Annotated[int | None, IOAttrs('lr')] - - level: Annotated[int, IOAttrs('lv')] - xp: Annotated[int, IOAttrs('xp')] - xpmax: Annotated[int, IOAttrs('xpm')] - - inbox_count: Annotated[int, IOAttrs('ibc')] - inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')] - - chests: Annotated[dict[str, Chest], IOAttrs('c')] - - -class BSInboxEntryType(Enum): - """Types of entries that can be in an inbox.""" - - UNKNOWN = 'u' # Entry types we don't support will be this. - SIMPLE = 's' - CLAIM = 'c' - CLAIM_DISCARD = 'cd' - - -@ioprepped -@dataclass -class BSInboxEntry: - """Single message in an inbox.""" - - type: Annotated[ - BSInboxEntryType, IOAttrs('t', enum_fallback=BSInboxEntryType.UNKNOWN) - ] - id: Annotated[str, IOAttrs('i')] - createtime: Annotated[datetime.datetime, IOAttrs('c')] - - # If clients don't support format_version of a message they will - # display 'app needs to be updated to show this'. - format_version: Annotated[int, IOAttrs('f', soft_default=1)] - - # These have soft defaults so can be removed in the future if desired. - message: Annotated[str, IOAttrs('m', soft_default='(invalid message)')] - subs: Annotated[list[str], IOAttrs('s', soft_default_factory=list)] - - -@ioprepped -@dataclass -class BSInboxRequestMessage(Message): - """Message requesting our inbox.""" - - @override - @classmethod - def get_response_types(cls) -> list[type[Response] | None]: - return [BSInboxRequestResponse] - - -@ioprepped -@dataclass -class BSInboxRequestResponse(Response): - """Here's that inbox contents you asked for, boss.""" - - entries: Annotated[list[BSInboxEntry], IOAttrs('m')] - - # Printable error if something goes wrong. - error: Annotated[str | None, IOAttrs('e')] = None - - -@ioprepped -@dataclass -class BSChestInfoMessage(Message): - """Request info about a chest.""" - - chest_id: Annotated[str, IOAttrs('i')] - - @override - @classmethod - def get_response_types(cls) -> list[type[Response] | None]: - return [BSChestInfoResponse] - - -@ioprepped -@dataclass -class BSChestInfoResponse(Response): - """Here's that inbox contents you asked for, boss.""" - - @dataclass - class Chest: - """A lovely chest.""" - - appearance: Annotated[ - BSClassicChestAppearance, - IOAttrs('a', enum_fallback=BSClassicChestAppearance.UNKNOWN), - ] - - # How much to unlock *now*. - unlock_tokens: Annotated[int, IOAttrs('tk')] - - # When unlocks on its own. - unlock_time: Annotated[datetime.datetime, IOAttrs('t')] - - # Are ads allowed now? - ad_allow: Annotated[bool, IOAttrs('aa')] - - chest: Annotated[Chest | None, IOAttrs('c')] - - -@ioprepped -@dataclass -class BSChestActionMessage(Message): - """Request action about a chest.""" - - class Action(Enum): - """Types of actions we can request.""" - - # Unlocking (for free or with tokens). - UNLOCK = 'u' - - # Watched an ad to reduce wait. - AD = 'ad' - - action: Annotated[Action, IOAttrs('a')] - - # Tokens we are paying (only applies to unlock). - token_payment: Annotated[int, IOAttrs('t')] - - chest_id: Annotated[str, IOAttrs('i')] - - @override - @classmethod - def get_response_types(cls) -> list[type[Response] | None]: - return [BSChestActionResponse] - - -@ioprepped -@dataclass -class BSChestActionResponse(Response): - """Here's the results of that action you asked for, boss.""" - - # If present, signifies the chest has been opened and we should show - # the user this stuff that was in it. - contents: Annotated[list[str] | None, IOAttrs('c')] = None - - # Printable error if something goes wrong. - error: Annotated[str | None, IOAttrs('e')] = None - - -class BSInboxEntryProcessType(Enum): - """Types of processing we can ask for.""" - - POSITIVE = 'p' - NEGATIVE = 'n' - - -@ioprepped -@dataclass -class BSInboxEntryProcessMessage(Message): - """Do something to an inbox entry.""" - - id: Annotated[str, IOAttrs('i')] - process_type: Annotated[BSInboxEntryProcessType, IOAttrs('t')] - - @override - @classmethod - def get_response_types(cls) -> list[type[Response] | None]: - return [BSInboxEntryProcessResponse] - - -@ioprepped -@dataclass -class BSInboxEntryProcessResponse(Response): - """Did something to that inbox entry, boss.""" - - # Printable error if something goes wrong. - error: Annotated[str | None, IOAttrs('e')] = None diff --git a/tools/efro/dataclassio/_api.py b/tools/efro/dataclassio/_api.py index 5e7aee342..d494c6d72 100644 --- a/tools/efro/dataclassio/_api.py +++ b/tools/efro/dataclassio/_api.py @@ -104,14 +104,15 @@ def dataclass_from_dict( coerce_to_float: bool = True, allow_unknown_attrs: bool = True, discard_unknown_attrs: bool = False, + lossy: bool = False, ) -> T: """Given a dict, return a dataclass of a given type. The dict must be formatted to match the specified codec (generally json-friendly object types). This means that sequence values such as tuples or sets should be passed as lists, enums should be passed as - their associated values, nested dataclasses should be passed as dicts, - etc. + their associated values, nested dataclasses should be passed as + dicts, etc. All values are checked to ensure their types/values are valid. @@ -121,14 +122,22 @@ def dataclass_from_dict( (as this would break the ability to do a lossless round-trip with data). - If coerce_to_float is True, int values passed for float typed fields - will be converted to float values. Otherwise, a TypeError is raised. - - If `allow_unknown_attrs` is False, AttributeErrors will be raised for - attributes present in the dict but not on the data class. Otherwise, - they will be preserved as part of the instance and included if it is - exported back to a dict, unless `discard_unknown_attrs` is True, in - which case they will simply be discarded. + If `coerce_to_float` is True, int values passed for float typed + fields will be converted to float values. Otherwise, a TypeError is + raised. + + If 'allow_unknown_attrs' is False, AttributeErrors will be raised + for attributes present in the dict but not on the data class. + Otherwise, they will be preserved as part of the instance and + included if it is exported back to a dict, unless + `discard_unknown_attrs` is True, in which case they will simply be + discarded. + + If `lossy` is True, Enum attrs and IOMultiType types are allowed to + use any fallbacks defined for them. This can allow older schemas to + successfully load newer data, but this can fundamentally modify the + data, so the resulting object is flagged as 'lossy' and prevented + from being serialized back out by default. """ val = _Inputter( cls, @@ -136,6 +145,7 @@ def dataclass_from_dict( coerce_to_float=coerce_to_float, allow_unknown_attrs=allow_unknown_attrs, discard_unknown_attrs=discard_unknown_attrs, + lossy=lossy, ).run(values) assert isinstance(val, cls) return val @@ -144,9 +154,11 @@ def dataclass_from_dict( def dataclass_from_json( cls: type[T], json_str: str, + *, coerce_to_float: bool = True, allow_unknown_attrs: bool = True, discard_unknown_attrs: bool = False, + lossy: bool = False, ) -> T: """Return a dataclass instance given a json string. @@ -159,6 +171,7 @@ def dataclass_from_json( coerce_to_float=coerce_to_float, allow_unknown_attrs=allow_unknown_attrs, discard_unknown_attrs=discard_unknown_attrs, + lossy=lossy, ) diff --git a/tools/efro/dataclassio/_base.py b/tools/efro/dataclassio/_base.py index dc7073774..191119ddf 100644 --- a/tools/efro/dataclassio/_base.py +++ b/tools/efro/dataclassio/_base.py @@ -24,6 +24,11 @@ # present. EXTRA_ATTRS_ATTR = '_DCIOEXATTRS' +# Attr name for a bool attr for flagging data as lossy, which means it +# may have been modified in some way during load and should generally not +# be written back out. +LOSSY_ATTR = '_DCIOLOSSY' + class Codec(Enum): """Specifies expected data format exported to or imported from.""" @@ -127,44 +132,68 @@ def get_type_id_storage_name(cls) -> str: The default is an obscure value so that it does not conflict with members of individual type attrs, but in some cases one - might prefer to serialize it to something simpler like 'type' - by overriding this call. One just needs to make sure that no + might prefer to serialize it to something simpler like 'type' by + overriding this call. One just needs to make sure that no encompassed types serialize anything to 'type' themself. """ return '_dciotype' + # NOTE: Currently (Jan 2025) mypy complains if overrides annotate + # return type of 'Self | None'. Substituting their own explicit type + # works though (see test_dataclassio). + @classmethod + def get_unknown_type_fallback(cls) -> Self | None: + """Return a fallback object in cases of unrecognized types. + + This can allow newer data to remain readable in older + environments. Use caution with this option, however, as it + effectively modifies data. + """ + return None + class IOAttrs: """For specifying io behavior in annotations. 'storagename', if passed, is the name used when storing to json/etc. - 'store_default' can be set to False to avoid writing values when equal - to the default value. Note that this requires the dataclass field - to define a default or default_factory or for its IOAttrs to - define a soft_default value. + + 'store_default' can be set to False to avoid writing values when + equal to the default value. Note that this requires the + dataclass field to define a default or default_factory or for + its IOAttrs to define a soft_default value. + 'whole_days', if True, requires datetime values to be exactly on day boundaries (see efro.util.utc_today()). - 'whole_hours', if True, requires datetime values to lie exactly on hour - boundaries (see efro.util.utc_this_hour()). - 'whole_minutes', if True, requires datetime values to lie exactly on minute - boundaries (see efro.util.utc_this_minute()). + + 'whole_hours', if True, requires datetime values to lie exactly on + hour boundaries (see efro.util.utc_this_hour()). + + 'whole_minutes', if True, requires datetime values to lie exactly on + minute boundaries (see efro.util.utc_this_minute()). + 'soft_default', if passed, injects a default value into dataclass instantiation when the field is not present in the input data. This allows dataclasses to add new non-optional fields while - gracefully 'upgrading' old data. Note that when a soft_default is - present it will take precedence over field defaults when determining - whether to store a value for a field with store_default=False - (since the soft_default value is what we'll get when reading that - same data back in when the field is omitted). + gracefully 'upgrading' old data. Note that when a soft_default + is present it will take precedence over field defaults when + determining whether to store a value for a field with + store_default=False (since the soft_default value is what we'll + get when reading that same data back in when the field is + omitted). + 'soft_default_factory' is similar to 'default_factory' in dataclass - fields; it should be used instead of 'soft_default' for mutable types - such as lists to prevent a single default object from unintentionally - changing over time. - 'enum_fallback', if provided, specifies an enum value to be substituted - in the case of unrecognized enum values. + fields; it should be used instead of 'soft_default' for mutable + types such as lists to prevent a single default object from + unintentionally changing over time. + + 'enum_fallback', if provided, specifies an enum value that can be + substituted in the case of unrecognized input values. This can + allow newer data to remain loadable in older environments. Note + that 'lossy' must be enabled in the top level load call for this + to apply, since it can fundamentally modify data. """ - # A sentinel object to detect if a parameter is supplied or not. Use + # A sentinel object to detect if a parameter is supplied or not. Use # a class to give it a better repr. class _MissingType: pass diff --git a/tools/efro/dataclassio/_inputter.py b/tools/efro/dataclassio/_inputter.py index 9a9756471..597d2f9aa 100644 --- a/tools/efro/dataclassio/_inputter.py +++ b/tools/efro/dataclassio/_inputter.py @@ -20,6 +20,7 @@ Codec, _parse_annotated, EXTRA_ATTRS_ATTR, + LOSSY_ATTR, _is_valid_for_codec, _get_origin, SIMPLE_TYPES, @@ -46,6 +47,7 @@ def __init__( coerce_to_float: bool, allow_unknown_attrs: bool = True, discard_unknown_attrs: bool = False, + lossy: bool = False, ): self._cls = cls self._codec = codec @@ -53,6 +55,7 @@ def __init__( self._allow_unknown_attrs = allow_unknown_attrs self._discard_unknown_attrs = discard_unknown_attrs self._soft_default_validator: _Outputter | None = None + self._lossy = lossy if not allow_unknown_attrs and discard_unknown_attrs: raise ValueError( @@ -66,11 +69,12 @@ def run(self, values: dict) -> Any: outcls: type[Any] # If we're dealing with a multi-type subclass which is NOT a - # dataclass, we must rely on its stored type to figure out - # what type of dataclass we're going to. If we are a dataclass - # then we already know what type we're going to so we can - # survive without this, which is often necessary when reading - # old data that doesn't have a type id attr yet. + # dataclass (generally a custom multitype base class), then we + # must rely on its stored type enum to figure out what type of + # dataclass we're going to create. If we *are* dealing with a + # dataclass then we already know what type we're going to so we + # can survive without this, which is often necessary when + # reading old data that doesn't have a type id attr yet. if issubclass(self._cls, IOMultiType) and not dataclasses.is_dataclass( self._cls ): @@ -81,7 +85,40 @@ def run(self, values: dict) -> Any: f' {values}.' ) type_id_enum = self._cls.get_type_id_type() - enum_val = type_id_enum(type_id_val) + try: + enum_val = type_id_enum(type_id_val) + except ValueError as exc: + + # Check the fallback even if not in lossy mode, as we + # inform the user of its existence in errors in that + # case. + fallback = self._cls.get_unknown_type_fallback() + + # Sanity check that fallback is correct type. + assert isinstance(fallback, self._cls | None) + + # If we're in lossy mode, provide the fallback value. + if self._lossy: + if fallback is not None: + # Ok; they provided a fallback. Flag it as lossy + # to prevent it from being written back out by + # default, and return it. + setattr(fallback, LOSSY_ATTR, True) + return fallback + else: + # If we're *not* in lossy mode, inform the user if + # we *would* have succeeded if we were. This is + # useful for debugging these sorts of situations. + if fallback is not None: + raise ValueError( + 'Failed loading unrecognized multitype object.' + ' Note that the multitype provides a fallback' + ' and thus would succeed in lossy mode.' + ) from exc + + # Otherwise the error stands as-is. + raise + outcls = self._cls.get_type(enum_val) else: outcls = self._cls @@ -100,6 +137,17 @@ def run(self, values: dict) -> Any: if is_ext: out.did_input() + # If we're running in lossy mode, flag the object as such so we + # don't allow writing it back out and potentially accidentally + # losing data. + # + # FIXME - We are currently only flagging this at the top level, + # but this will not prevent sub-objects from being written out. + # Is that worth worrying about? Though perfect is the enemy of + # good I suppose. + if self._lossy: + setattr(out, LOSSY_ATTR, True) + return out def _value_from_input( @@ -183,22 +231,28 @@ def _value_from_input( # dataclass (all dataclasses inheriting from the multi-type # should just be processed as dataclasses). if issubclass(origin, IOMultiType): - return self._dataclass_from_input( - _get_multitype_type(anntype, fieldpath, value), - fieldpath, - value, - ) + return self._multitype_obj(anntype, fieldpath, value) if issubclass(origin, Enum): try: return origin(value) - except ValueError: - # If a fallback enum was provided in ioattrs, return - # that for unrecognized values. + except ValueError as exc: + # If a fallback enum was provided in ioattrs AND we're + # in lossy mode, return that for unrecognized values. If + # one was provided but we're *not* in lossy mode, note + # that we could have loaded it if lossy mode was + # enabled. if ioattrs is not None and ioattrs.enum_fallback is not None: + # Sanity check; make sure fallback is valid. assert type(ioattrs.enum_fallback) is origin - return ioattrs.enum_fallback - # Otherwise the error stands. + if self._lossy: + return ioattrs.enum_fallback + raise ValueError( + 'Failed to load Enum. Note that it has a fallback' + ' value and thus would succeed in lossy mode.' + ) from exc + + # Otherwise the error stands as-is. raise if issubclass(origin, datetime.datetime): @@ -243,16 +297,17 @@ def _dataclass_from_input( """Given a dict, instantiates a dataclass of the given type. The dict must be in the json-friendly format as emitted from - dataclass_to_dict. This means that sequence values such as tuples or - sets should be passed as lists, enums should be passed as their - associated values, and nested dataclasses should be passed as dicts. + dataclass_to_dict. This means that sequence values such as + tuples or sets should be passed as lists, enums should be passed + as their associated values, and nested dataclasses should be + passed as dicts. """ try: return self._do_dataclass_from_input(cls, fieldpath, values) except Exception as exc: - # Extended data types can choose to sub default data in case - # of failures (generally not a good idea but occasionally - # useful). + # Extended data types can choose to substitute default data + # in case of failures (generally not a good idea but + # occasionally useful). if issubclass(cls, IOExtendedData): fallback = cls.handle_input_error(exc) if fallback is None: @@ -304,8 +359,8 @@ def _do_dataclass_from_input( # However we do want to make sure the class we're loading # doesn't itself use this same name, as this could lead to # tricky breakage. We can't verify this for types at prep - # time because IOMultiTypes are lazy-loaded, so this is - # the best we can do. + # time because IOMultiTypes are lazy-loaded, so this is the + # best we can do. if type_id_store_name in fields_by_name: raise RuntimeError( f"{cls} contains a '{type_id_store_name}' field" @@ -574,12 +629,7 @@ def _sequence_from_input( # values to determine which type to load for each element. if issubclass(childanntype, IOMultiType): return seqtype( - self._dataclass_from_input( - _get_multitype_type(childanntype, fieldpath, i), - fieldpath, - i, - ) - for i in value + self._multitype_obj(childanntype, fieldpath, i) for i in value ) return seqtype( @@ -587,6 +637,21 @@ def _sequence_from_input( for i in value ) + def _multitype_obj(self, anntype: Any, fieldpath: str, value: Any) -> Any: + try: + mttype = _get_multitype_type(anntype, fieldpath, value) + except ValueError: + if self._lossy: + out = anntype.get_unknown_type_fallback() + if out is not None: + # Ok; they provided a fallback. Make sure its of our + # expected type and return it. + assert isinstance(out, anntype) + return out + raise + + return self._dataclass_from_input(mttype, fieldpath, value) + def _tuple_from_input( self, cls: type, diff --git a/tools/efro/dataclassio/_outputter.py b/tools/efro/dataclassio/_outputter.py index df6b72abb..e3404bda3 100644 --- a/tools/efro/dataclassio/_outputter.py +++ b/tools/efro/dataclassio/_outputter.py @@ -21,6 +21,7 @@ Codec, _parse_annotated, EXTRA_ATTRS_ATTR, + LOSSY_ATTR, _is_valid_for_codec, _get_origin, SIMPLE_TYPES, @@ -61,6 +62,14 @@ def run(self) -> Any: # isinstance call below fails. assert dataclasses.is_dataclass(self._obj) + # If this data has been flagged as lossy, don't allow outputting + # it. This hopefully helps avoid unintentional data + # modification/loss. + if getattr(obj, LOSSY_ATTR, False): + raise ValueError( + 'Object has been flagged as lossy; output is disallowed.' + ) + # For special extended data types, call their 'will_output' callback. # FIXME - should probably move this into _process_dataclass so it # can work on nested values. diff --git a/tools/efro/dataclassio/templatemultitype.py b/tools/efro/dataclassio/templatemultitype.py new file mode 100644 index 000000000..9e9185323 --- /dev/null +++ b/tools/efro/dataclassio/templatemultitype.py @@ -0,0 +1,63 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Template for an IOMultitype setup. + +To use this template, simply copy the contents of this module somewhere +and then replace 'TemplateMultiType' with 'YourType'. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, assert_never, override + +from enum import Enum +from dataclasses import dataclass + +from efro.dataclassio import ioprepped, IOMultiType + +if TYPE_CHECKING: + pass + + +class TemplateMultiTypeTypeID(Enum): + """Type ID for each of our subclasses.""" + + TEST = 'test' + + +class TemplateMultiType(IOMultiType[TemplateMultiTypeTypeID]): + """Top level class for our multitype.""" + + @override + @classmethod + def get_type_id(cls) -> TemplateMultiTypeTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type( + cls, type_id: TemplateMultiTypeTypeID + ) -> type[TemplateMultiType]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + + t = TemplateMultiTypeTypeID + if type_id is t.TEST: + return Test + + # Important to make sure we provide all types. + assert_never(type_id) + + +@ioprepped +@dataclass +class Test(TemplateMultiType): + """Just a test.""" + + @override + @classmethod + def get_type_id(cls) -> TemplateMultiTypeTypeID: + return TemplateMultiTypeTypeID.TEST diff --git a/tools/efro/message/_protocol.py b/tools/efro/message/_protocol.py index 1d18ea575..8f0c3c36b 100644 --- a/tools/efro/message/_protocol.py +++ b/tools/efro/message/_protocol.py @@ -294,7 +294,14 @@ def _from_dict( raise UnregisteredMessageIDError( f'Got unregistered {opname} id of {m_id}.' ) - return dataclass_from_dict(msgtype, msgdict) + + # Explicitly allow any fallbacks we define for our enums and + # multitypes. This allows us to build message types that remain + # loadable even when containing unrecognized future + # enums/multitype data. Be aware that this flags the object as + # 'lossy' however which prevents it from being reserialized by + # default. + return dataclass_from_dict(msgtype, msgdict, lossy=True) def _get_module_header( self, diff --git a/tools/efro/util.py b/tools/efro/util.py index 16acc167b..dfc85e350 100644 --- a/tools/efro/util.py +++ b/tools/efro/util.py @@ -6,6 +6,7 @@ import os import time +import random import weakref import functools import datetime @@ -75,8 +76,6 @@ def explicit_bool(val: bool) -> bool: # pylint: disable=no-else-return if TYPE_CHECKING: # infer this! - import random - return random.random() < 0.5 else: return val @@ -982,3 +981,15 @@ def extract_arg( del args[argindex : argindex + 2] return val + + +def weighted_choice(*args: tuple[T, float]) -> T: + """Given object/weight pairs as args, returns a random object. + + Intended as a shorthand way to call random.choices on a few explicit + options. + """ + items: tuple[T] + weights: tuple[float] + items, weights = zip(*args) + return random.choices(items, weights=weights)[0]