diff --git a/.efrocachemap b/.efrocachemap index 48f8ee57..87cd4596 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -421,43 +421,43 @@ "build/assets/ba_data/audio/zoeOw.ogg": "74befe45a8417e95b6a2233c51992a26", "build/assets/ba_data/audio/zoePickup01.ogg": "48ab8cddfcde36a750856f3f81dd20c8", "build/assets/ba_data/audio/zoeScream01.ogg": "2b468aedfa8741090247f04eb9e6df55", - "build/assets/ba_data/data/langdata.json": "99ad02cbe90ab3e46168ddf277633c68", - "build/assets/ba_data/data/languages/arabic.json": "dcb913b3d89fe665adbf35e697d2e5d0", + "build/assets/ba_data/data/langdata.json": "65b989c0d893d8981992f66ff81a8a97", + "build/assets/ba_data/data/languages/arabic.json": "3be73283cb8009cc2c95e53df36e8b86", "build/assets/ba_data/data/languages/belarussian.json": "3d5523d0004293aa2df02f3f6f3b84f8", - "build/assets/ba_data/data/languages/chinese.json": "d4a89bf007a0624c6852c512d1d38cdc", - "build/assets/ba_data/data/languages/chinesetraditional.json": "319565f8a15667488f48dbce59278e39", + "build/assets/ba_data/data/languages/chinese.json": "fc69790c41e6750d20a7719afc5a7527", + "build/assets/ba_data/data/languages/chinesetraditional.json": "86671be47e2b5d0badeb3b90a3db6402", "build/assets/ba_data/data/languages/croatian.json": "b23619cb396ac16640c47458f884b16a", "build/assets/ba_data/data/languages/czech.json": "61bcfce75c0d53d2f2af709cee42187a", "build/assets/ba_data/data/languages/danish.json": "8e57db30c5250df2abff14a822f83ea7", "build/assets/ba_data/data/languages/dutch.json": "b0900d572c9141897d53d6574c471343", - "build/assets/ba_data/data/languages/english.json": "eae3a17af2efc2388f96c1e05c024455", + "build/assets/ba_data/data/languages/english.json": "5a73dea22df1117d58a79459def62ff5", "build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880", - "build/assets/ba_data/data/languages/filipino.json": "fabaedeefc28cf02ec365ef37046ec0b", - "build/assets/ba_data/data/languages/french.json": "b7d11199756f0eb4f1a745ceee652b2a", + "build/assets/ba_data/data/languages/filipino.json": "a291d4d3619adc82c5c4096bbfefe28a", + "build/assets/ba_data/data/languages/french.json": "73a01df9b44b3fb030750a1b5f56f07b", "build/assets/ba_data/data/languages/german.json": "198b9860c5b9df7b8e3e30b03d8755cb", - "build/assets/ba_data/data/languages/gibberish.json": "085f438ccdb7553bbb7cf5d68e9cd300", - "build/assets/ba_data/data/languages/greek.json": "ad3c0d38f34d809824892d6f22808dbf", + "build/assets/ba_data/data/languages/gibberish.json": "d6810f99fc9055b5203c382a83bc5128", + "build/assets/ba_data/data/languages/greek.json": "d28d1092fbb00ed857cbd53124c0dc78", "build/assets/ba_data/data/languages/hindi.json": "54cd56bade6922b40989a8ac5e0c17f6", - "build/assets/ba_data/data/languages/hungarian.json": "0cf994bf9fedf004c9a1011bee179a07", + "build/assets/ba_data/data/languages/hungarian.json": "3a974ea6e6ffccca41aed308ad5a7a26", "build/assets/ba_data/data/languages/indonesian.json": "ed9038bf4b9216f93eb73e753e162706", - "build/assets/ba_data/data/languages/italian.json": "017f2b20584838d9f5ad88822a4caf62", + "build/assets/ba_data/data/languages/italian.json": "ffc58952260b63fdf88805a2d9a68257", "build/assets/ba_data/data/languages/korean.json": "4e3524327a0174250aff5e1ef4c0c597", "build/assets/ba_data/data/languages/malay.json": "f6ce0426d03a62612e3e436ed5d1be1f", - "build/assets/ba_data/data/languages/persian.json": "87f9dfc085f0ff715b17a8082fc79483", - "build/assets/ba_data/data/languages/polish.json": "3ce5635118b34ea89d6e0f83c8c7e9df", - "build/assets/ba_data/data/languages/portuguese.json": "0accd9462f0acbc5f869f33a2a7a24cd", - "build/assets/ba_data/data/languages/romanian.json": "ef68520f749cf3641d4e4225a6166349", - "build/assets/ba_data/data/languages/russian.json": "e7b90ce1eee8247e164ed7d1470f76bd", + "build/assets/ba_data/data/languages/persian.json": "ededb9c015afb58b1324a096ea740f72", + "build/assets/ba_data/data/languages/polish.json": "62b56ace320191985689bfbcfacd56ea", + "build/assets/ba_data/data/languages/portuguese.json": "2be5c25e55946197bd0e0f646d444b2c", + "build/assets/ba_data/data/languages/romanian.json": "55a8744e466801013ea131266a856924", + "build/assets/ba_data/data/languages/russian.json": "c7c5bfc6f82d74e49ac746d187314ba7", "build/assets/ba_data/data/languages/serbian.json": "d7452dd72ac0e51680cb39b5ebaa1c69", "build/assets/ba_data/data/languages/slovak.json": "3c08c748c96c71bd9e1d7291fb8817b6", - "build/assets/ba_data/data/languages/spanish.json": "63f80b2c37a1d8092304db1a10ecf789", + "build/assets/ba_data/data/languages/spanish.json": "4b262447f703eb4c6683b54af6b7b592", "build/assets/ba_data/data/languages/swedish.json": "5142a96597d17d8344be96a603da64ac", - "build/assets/ba_data/data/languages/tamil.json": "b9fcc523639f55e05c7f4e7914f3321a", + "build/assets/ba_data/data/languages/tamil.json": "5ececa2dde2bbe33ad61e580fa5b79ad", "build/assets/ba_data/data/languages/thai.json": "1d665629361f302693dead39de8fa945", - "build/assets/ba_data/data/languages/turkish.json": "60691e93b58e1612901e8558b967697b", - "build/assets/ba_data/data/languages/ukrainian.json": "6207f0e4048f9391526321d879a5d61e", - "build/assets/ba_data/data/languages/venetian.json": "9bb01eea11d1627c0ceb876d26d86d0c", - "build/assets/ba_data/data/languages/vietnamese.json": "4bc1ba96ef291fed6d6409ad8cf7357f", + "build/assets/ba_data/data/languages/turkish.json": "1c0a5c0c0c115107fb0752c92907f584", + "build/assets/ba_data/data/languages/ukrainian.json": "23a98e5722d3e71e809a8a0063daa603", + "build/assets/ba_data/data/languages/venetian.json": "a1315f5233ebbee1464683ac55d5d9d5", + "build/assets/ba_data/data/languages/vietnamese.json": "5ae84265600b6cfda45c9bed18724e1d", "build/assets/ba_data/data/maps/big_g.json": "1dd301d490643088a435ce75df971054", "build/assets/ba_data/data/maps/bridgit.json": "6aea74805f4880cc11237c5734a24422", "build/assets/ba_data/data/maps/courtyard.json": "4b836554c8949bcd2ae382f5e3c1a9cc", @@ -1388,10 +1388,10 @@ "build/assets/ba_data/textures/clayStroke.ktx": "8c648180e290a4acfff1196fa358f042", "build/assets/ba_data/textures/clayStroke.pvr": "84c31a1a8ab4ea051a5b6a804dd952df", "build/assets/ba_data/textures/clayStroke_preview.png": "ec248139546f51c007148ef0250a5e0c", - "build/assets/ba_data/textures/coin.dds": "e69e7ba8ff87d5ed9ca7c7ab96d13546", - "build/assets/ba_data/textures/coin.ktx": "d1de4b2fd4fe028b889ed53a829f9a72", - "build/assets/ba_data/textures/coin.pvr": "b8477ed7afdc5e460843e4d8413a2c08", - "build/assets/ba_data/textures/coin_preview.png": "2bdae85f94cfa39eafdfc4330db90605", + "build/assets/ba_data/textures/coin.dds": "ae86318038e6badeb5e9a7943e466c99", + "build/assets/ba_data/textures/coin.ktx": "6202185bbf5158191b267c2c85b40dd9", + "build/assets/ba_data/textures/coin.pvr": "d4b729057e06c2db64dd3fcae37588fb", + "build/assets/ba_data/textures/coin_preview.png": "ee84407488f8812e08875224316f3fad", "build/assets/ba_data/textures/controllerIcon.dds": "9c805344b3d663d9ecf5a41d70289fc9", "build/assets/ba_data/textures/controllerIcon.ktx": "b5cf73a08e788f4e2a609d516715bd2b", "build/assets/ba_data/textures/controllerIcon.pvr": "2f9193b6eb0b7c20113d4c21b7e1ef8d", @@ -2495,10 +2495,10 @@ "build/assets/ba_data/textures/uiAtlas.dds": "76a861c711ea4700ad2f484adb53a837", "build/assets/ba_data/textures/uiAtlas.ktx": "4bd0550d4297b6b290ceceb0d3dae490", "build/assets/ba_data/textures/uiAtlas.pvr": "e7601e1d8cb5a03324b7e240a46eb62f", - "build/assets/ba_data/textures/uiAtlas2.dds": "ac969ae516faf8e57800dd37801550ef", - "build/assets/ba_data/textures/uiAtlas2.ktx": "744c37bbee308e1580e2ad569539b314", - "build/assets/ba_data/textures/uiAtlas2.pvr": "10ee6675a5c7892efa6924bdb4ce5331", - "build/assets/ba_data/textures/uiAtlas2_preview.png": "22077f30b690f0c21cfa03549cd55d2b", + "build/assets/ba_data/textures/uiAtlas2.dds": "6d4b31087da911fa4fe4ccbd3935cc63", + "build/assets/ba_data/textures/uiAtlas2.ktx": "7679ce6fab577500b956973ea10a2697", + "build/assets/ba_data/textures/uiAtlas2.pvr": "70383679a25fa1739beb2b2b97f4fbee", + "build/assets/ba_data/textures/uiAtlas2_preview.png": "6a48c238db2453ee0333cf554bd68054", "build/assets/ba_data/textures/uiAtlas_preview.png": "314c4fe9ba75ff03bb5d75d5003c2462", "build/assets/ba_data/textures/upButton.dds": "96f0df23bb2055a30d0073d146138a8e", "build/assets/ba_data/textures/upButton.ktx": "a5cc08ddb7e67a1b9e5e067d06fbbfcd", @@ -4096,33 +4096,33 @@ "build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1", "build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", - "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "bf270e4b3ae3fdecff554052ab6b5b00", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "78116aab2b62c8ca18dfc534a3db829e", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "ef3f99b303627e36866d119950745815", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "d93df4793c2798d489a12d848af2daa0", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "e4ac642421fc80630950dc39b0d5b488", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "51ea783b32a84a10e97275c9163dc767", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "607c744559cf66f7cc565ad6c0f21d90", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "5d869b3cd51a85de3613f39d4180196e", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "d1c78f94885520e6ae8a7f3afe4d6162", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "58b78d9fbbd6c4b9515b35f9914ccd7e", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "1ee3373fb1f9a0fedc7f0f4bd574bac9", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "d3da9a35e807bc10f45f3aa38eea2a95", - "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "c26823caeca6398af29accbbebbb0159", - "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "cf58fa232755c64c71035dce8074707f", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "f077a737910c340fd42ce91e5249447e", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "37d37d17cb9878f630da859903fc8e47", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "dde17d5f07a4837ef331fe9012ed4976", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "53d938dd0541c62f5e31a871002c9291", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "2791119fd7b475228aa8cb02d6db7a96", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "bbcad92bb94abfe2dde3b568f6bb3595", - "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "273ffd46a618fe347a570e6bd01d54a4", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "0a166c970637de5e318e4f07e8c0aeae", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "71fee3398234820d038235f02ea7ed6a", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "682d622e82fbdf7d41f170146ae8cf6f", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "ed9a7e820ab3a59d6326b898973d5cff", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "4efa411d0d43debb7c8db1db1d9a4658", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "8892f774f2d079604d77241adaf3e529", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "c479f3ab633f0b19f3e733943b3c1c3b", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "2797e7b22388c68b2e41a1eb7c820a62", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "f58b978d2978fdf6e2f7f0cc13fbb2c7", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "1ad711461c2a65ecb1fbab1aac2aacae", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "ec90ed3ec92a6db3f8b9651f2464d8d4", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "12cc0539e86a408b01ac3b3a74cff98a", + "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "1f59835f71e01827f056f503302c45c4", + "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "c747103d9fe3206f94b54ed9ae5d4aa4", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "603f77208cb0df9161b84387631b8778", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "00a72972b6b0531b176406ad0e8d243f", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "af0063f60bb84a65ca71df83d00b9507", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "817b90e953f655e83cd8e69d1feb5526", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "c132d48c3b560ac0dcf090a3bce8ba26", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "b3c44cd6282d4100b730e997baa3f28e", + "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "73ad3303fe1a82005918fbc5dae3446c", "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "fa659b5d6119acba6570c92ce4d35ae2", - "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "273ffd46a618fe347a570e6bd01d54a4", + "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "73ad3303fe1a82005918fbc5dae3446c", "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "fa659b5d6119acba6570c92ce4d35ae2", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "712db7e1fcd065a3a22faddc0def8670", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "ab8467107ed371eb542e0317a7e7395d", "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "4a4c19120f810ff5b4c7afbf11c23cf6", - "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "712db7e1fcd065a3a22faddc0def8670", + "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "ab8467107ed371eb542e0317a7e7395d", "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "4a4c19120f810ff5b4c7afbf11c23cf6", "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "bbee8acd115ca24fc14146a9fc47c676", "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "8b68b56dda9a9f421823e653b752445a", @@ -4132,14 +4132,14 @@ "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "3e5c5fd0a09f55ba7b05ce1e2ec7171e", "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "1659535e95e3047fda529543e265ac97", "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "3e5c5fd0a09f55ba7b05ce1e2ec7171e", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "953223ec1a24ef6ba5cb7f883727af75", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "b390389ff9bb586326603b8b74f686d7", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "c8b6d94ac084539a7370c6528582cc2b", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "8e857d5edefbd2bd10837cd8224eb690", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "23d724d38988f6d41dc2c8fc818b74b1", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "b5011a047d8c23d20d1bc4405012ff1d", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "b524a2838f1d3c540afa2d63335bb036", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "e51f7a0643a02a6962c70bf884e0b8ae", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "d4aca724c3184c6c2c82e7b11c1a352a", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "f4fbab89e11f52329d65a9afcabc2e5e", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "09733e1291806ece13f108ad6fb72b68", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "099e20f57cfa2c36bf7b44e389d01d9a", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "aae5ce60761c700d23a480cac83a2df5", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "1fdc6ee85373af9df3a0b5eb601990af", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "809e62f1d2ac54c239a3a13c2d2c8ee7", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "68b49b66a76b9ab0d1b17de2bab19c04", "src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c", "src/assets/ba_data/python/babase/_mgen/enums.py": "cb299985623bbcc86015cb103a424ae6", "src/ballistica/base/mgen/pyembed/binding_base.inc": "efa61468cf098f77cc6a234461d8b86d", @@ -4150,5 +4150,5 @@ "src/ballistica/core/mgen/python_modules_monolithic.h": "fb967ed1c7db0c77d8deb4f00a7103c5", "src/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc": "c25b263f2a31fb5ebe057db07d144879", "src/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc": "44a45492db057bf7f7158c3b0fa11f0f", - "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "f5f054050d2b2fcd3763a4833fb32269" + "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "1081c8d6a7ed45cbe211b26b6a3b8af0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a42ac1b..98301646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,41 @@ -### 1.7.37 (build 21949, api 8, 2024-07-31) +### 1.7.37 (build 21968, api 8, 2024-08-29) +- Playlist customization no longer requires pro. +- Soundtrack customization no longer requires pro. +- Switching over to the new 'toolbar mode' UI that has been in the works for + several years. This includes a number of handy things such as consistent + buttons and widgets for league status, currencies, inventory, and the store. + It also adds a fixed back button on phones that should be easier to hit and a + dock for earned treasure chests at the bottom of the screen (will finally use + those treasure chest textures!). This is a substantial change so please holler + if you run into anything that looks broken or doesn't behave as you think it + should. +- When running in 'small' UI mode (phones) the engine now uses 1300x600 as its + virtual resolution. This gives a wider 19.5:9 aspect ratio which lines up with + most modern smartphones, so people with such phones should no longer see + wasted space on the sides of their screen. The virtual resolution on 'medium' + and 'large' is now 1280x720. This gives the same 16:9 aspect ratio as the old + resolution (1207x680) but is a cleaner number. The 16:9 aspect ratio still + works well for tablets monitors, and TVs. When writing a UI, always be sure to + test it on 'small', 'medium', and 'large' modes to make sure it fits on screen + and feels similar in scale to the rest of the UI. Ideally when 'ui_v2' rolls + around we can make it possible to build UIs that adapt better to screen sizes + so things like fixed aspect ratios will no longer be necessary. +- Split the main menu UI into two classes: `bauiv1.mainmenu.MainMenuWindow` and + `bauiv1.ingamemenu.InGameMenuWindow`. +- Removed some bits of `bauiv1` which were never fully implemented and which I + feel were a flawed/outdated design. This includes `UILocation`, + `UILocationWindow`, `UIEntry`, and `UIController`. The whole purpose of these + was to add a higher level layer to the UI to make things like saving/restoring + UI states easier, but I now plan to use `WindowState` classes to accomplish + much of that in a more backward-compatible way. More on that below. +- Added a new `bauiv1.Window` subclass called `bauiv1.MainWindow` which handles + what was previously called the 'main-menu-window' system which was a bit + ad-hoc and messy. MainMenuWindows have a built-in stack system so things like + back-button handling are more automatic and windows don't have to hard-code + where they came from. There are also other benefits such as better state + saving/restoring. When writing a MainWindow, pretty much all navigation should + only need too use methods: `can_change_main_window()`, `main_window_back()`, + and `main_window_replace()`. - Finally got things updated so language testing works again, and made it a bit spiffier while at it. You now simply point the game at your test language and it will update dynamically as you make edits; no need to download any files. @@ -11,9 +48,30 @@ doing more heavy downloading with Asset Packages coming online so its time to upgrade to a more modern web client library than Python's basic built in urllib stuff. -- Pasting a single line of text followed by newlines now works. Previously it - would complain that multiple lines of text aren't supported, but now it - just ignores the trailing newlines. +- Pasting a single line of text followed by newlines to the dev console now + works. Previously it would complain that multiple lines of text aren't + supported, but now it just ignores the trailing newlines. +- Added an 'AppModes' tab to the dev console, allowing switching between any + AppModes defined in the current build for testing. Currently this is just + SceneV1AppMode and EmptyAppMode. This will become more useful in the future + when things like SquadsAppMode (Squads mode) or RemoteAppMode (the revamped + BSRemote app) happen. +- Added a 'UI' tab to the dev console allowing debugging virtual screen bounds + and testing different UI scales dynamically. +- Renamed `SceneV1AppMode` to `ClassicAppMode` and relocated it from the + `scene_v1` featureset to the `classic` one. This makes more logical sense + since `classic` is more about app operation and `scene_v1` is more about + gameplay, though realistically it doesn't matter since those two featuresets + are hopelessly entangled. Future parallels such as `squads` and `scene_v2` + featuresets should be more independent of eachother. +- Removed the warning when calling `ba*.screenmessage` in a game context. + Hopefully most code has been ported at this point and it has done its job. As + a final reminder, `ba*.screenmessage()` will only show messages locally now; + you need to use something like `bascenev1.broadcastmessage()` to show things + to everyone in a game. +- Removed `efro.util.enum_by_value()` which was a workaround for a Python bug + that has been fixed for a few versions now. Instaed of + `enum_by_value(MyEnumType, foo)` you can simply do `MyEnumType(foo)`. ### 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 ce5f81f4..ef00fe33 100644 --- a/ballisticakit-cmake/CMakeLists.txt +++ b/ballisticakit-cmake/CMakeLists.txt @@ -209,8 +209,8 @@ set(BALLISTICA_SOURCES ${BA_SRC_ROOT}/ballistica/base/app_adapter/app_adapter_vr.h ${BA_SRC_ROOT}/ballistica/base/app_mode/app_mode.cc ${BA_SRC_ROOT}/ballistica/base/app_mode/app_mode.h - ${BA_SRC_ROOT}/ballistica/base/app_mode/app_mode_empty.cc - ${BA_SRC_ROOT}/ballistica/base/app_mode/app_mode_empty.h + ${BA_SRC_ROOT}/ballistica/base/app_mode/empty_app_mode.cc + ${BA_SRC_ROOT}/ballistica/base/app_mode/empty_app_mode.h ${BA_SRC_ROOT}/ballistica/base/assets/asset.cc ${BA_SRC_ROOT}/ballistica/base/assets/asset.h ${BA_SRC_ROOT}/ballistica/base/assets/assets.cc @@ -463,6 +463,8 @@ set(BALLISTICA_SOURCES ${BA_SRC_ROOT}/ballistica/classic/python/classic_python.h ${BA_SRC_ROOT}/ballistica/classic/python/methods/python_methods_classic.cc ${BA_SRC_ROOT}/ballistica/classic/python/methods/python_methods_classic.h + ${BA_SRC_ROOT}/ballistica/classic/support/classic_app_mode.cc + ${BA_SRC_ROOT}/ballistica/classic/support/classic_app_mode.h ${BA_SRC_ROOT}/ballistica/classic/support/stress_test.cc ${BA_SRC_ROOT}/ballistica/classic/support/stress_test.h ${BA_SRC_ROOT}/ballistica/classic/support/v1_account.cc @@ -657,8 +659,6 @@ set(BALLISTICA_SOURCES ${BA_SRC_ROOT}/ballistica/scene_v1/support/player_spec.h ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene.cc ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene.h - ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_app_mode.cc - ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_app_mode.h ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_context.cc ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_context.h ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc @@ -752,8 +752,6 @@ set(BALLISTICA_SOURCES ${BA_SRC_ROOT}/ballistica/ui_v1/python/methods/python_methods_ui_v1.h ${BA_SRC_ROOT}/ballistica/ui_v1/python/ui_v1_python.cc ${BA_SRC_ROOT}/ballistica/ui_v1/python/ui_v1_python.h - ${BA_SRC_ROOT}/ballistica/ui_v1/support/root_ui.cc - ${BA_SRC_ROOT}/ballistica/ui_v1/support/root_ui.h ${BA_SRC_ROOT}/ballistica/ui_v1/ui_v1.cc ${BA_SRC_ROOT}/ballistica/ui_v1/ui_v1.h ${BA_SRC_ROOT}/ballistica/ui_v1/widget/button_widget.cc diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj index e04617c4..4041abab 100644 --- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj @@ -201,8 +201,8 @@ - - + + @@ -455,6 +455,8 @@ + + @@ -649,8 +651,6 @@ - - @@ -744,8 +744,6 @@ - - diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters index 25638b88..578def22 100644 --- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters @@ -37,10 +37,10 @@ ballistica\base\app_mode - + ballistica\base\app_mode - + ballistica\base\app_mode @@ -799,6 +799,12 @@ ballistica\classic\python\methods + + ballistica\classic\support + + + ballistica\classic\support + ballistica\classic\support @@ -1381,12 +1387,6 @@ ballistica\scene_v1\support - - ballistica\scene_v1\support - - - ballistica\scene_v1\support - ballistica\scene_v1\support @@ -1666,12 +1666,6 @@ ballistica\ui_v1\python - - ballistica\ui_v1\support - - - ballistica\ui_v1\support - ballistica\ui_v1 @@ -2054,7 +2048,6 @@ - diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj index 29dbe54a..978f56bb 100644 --- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj @@ -196,8 +196,8 @@ - - + + @@ -450,6 +450,8 @@ + + @@ -644,8 +646,6 @@ - - @@ -739,8 +739,6 @@ - - diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters index 25638b88..578def22 100644 --- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters @@ -37,10 +37,10 @@ ballistica\base\app_mode - + ballistica\base\app_mode - + ballistica\base\app_mode @@ -799,6 +799,12 @@ ballistica\classic\python\methods + + ballistica\classic\support + + + ballistica\classic\support + ballistica\classic\support @@ -1381,12 +1387,6 @@ ballistica\scene_v1\support - - ballistica\scene_v1\support - - - ballistica\scene_v1\support - ballistica\scene_v1\support @@ -1666,12 +1666,6 @@ ballistica\ui_v1\python - - ballistica\ui_v1\support - - - ballistica\ui_v1\support - ballistica\ui_v1 @@ -2054,7 +2048,6 @@ - diff --git a/config/projectconfig.json b/config/projectconfig.json index 85dc6c6e..c0ece20e 100644 --- a/config/projectconfig.json +++ b/config/projectconfig.json @@ -19,7 +19,7 @@ "src/ballistica/core/platform/android/android_gl3.c" ], "default_app_modes": [ - "bascenev1.SceneV1AppMode", + "baclassic.ClassicAppMode", "babase.EmptyAppMode" ], "efrocache_repository_url": "https://files.ballistica.net/cache/ba1", diff --git a/config/requirements.txt b/config/requirements.txt index 229ecbd0..95c86a2f 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -1,20 +1,20 @@ cpplint==1.6.1 -dmgbuild==1.6.1 +dmgbuild==1.6.2 filelock==3.15.4 -furo==2024.7.18 -mypy==1.11.1 -pbxproj==4.2.0 -pdoc==14.6.0 +furo==2024.8.6 +mypy==1.11.2 +pbxproj==4.2.1 +pdoc==14.6.1 pur==7.3.2 pylint==3.2.6 pylsp-mypy==0.6.8 pytest==8.3.2 python-daemon==3.0.1 python-lsp-black==2.0.0 -python-lsp-server==1.11.0 +python-lsp-server==1.12.0 requests==2.32.3 -Sphinx==7.4.7 -tomlkit==0.13.0 +Sphinx==8.0.2 +tomlkit==0.13.2 types-certifi==2021.10.8.3 types-filelock==3.2.7 types-requests==2.32.0.20240712 diff --git a/config/toolconfigsrc/dir-locals.el b/config/toolconfigsrc/dir-locals.el index 389432c7..3e3a0693 100644 --- a/config/toolconfigsrc/dir-locals.el +++ b/config/toolconfigsrc/dir-locals.el @@ -12,6 +12,7 @@ (project-vc-ignores . ("docs" "submodules" "src/external" + "ballisticakit-android/BallisticaKit/src/main/cpp/src" "src/assets/ba_data/python-site-packages" "src/assets/pylib-android" "src/assets/pylib-apple" diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json index 34a6bcb0..a65c4972 100644 --- a/src/assets/.asset_manifest_public.json +++ b/src/assets/.asset_manifest_public.json @@ -68,14 +68,14 @@ "ba_data/python/baclassic/__pycache__/_achievement.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/_ads.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/_analytics.cpython-312.opt-1.pyc", - "ba_data/python/baclassic/__pycache__/_appdelegate.cpython-312.opt-1.pyc", + "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__/_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", "ba_data/python/baclassic/__pycache__/_servermode.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/_store.cpython-312.opt-1.pyc", - "ba_data/python/baclassic/__pycache__/_subsystem.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/_tips.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/_tournament.cpython-312.opt-1.pyc", "ba_data/python/baclassic/__pycache__/macmusicapp.cpython-312.opt-1.pyc", @@ -84,14 +84,14 @@ "ba_data/python/baclassic/_achievement.py", "ba_data/python/baclassic/_ads.py", "ba_data/python/baclassic/_analytics.py", - "ba_data/python/baclassic/_appdelegate.py", + "ba_data/python/baclassic/_appmode.py", + "ba_data/python/baclassic/_appsubsystem.py", "ba_data/python/baclassic/_benchmark.py", "ba_data/python/baclassic/_input.py", "ba_data/python/baclassic/_music.py", "ba_data/python/baclassic/_net.py", "ba_data/python/baclassic/_servermode.py", "ba_data/python/baclassic/_store.py", - "ba_data/python/baclassic/_subsystem.py", "ba_data/python/baclassic/_tips.py", "ba_data/python/baclassic/_tournament.py", "ba_data/python/baclassic/macmusicapp.py", @@ -123,18 +123,17 @@ "ba_data/python/baenv.py", "ba_data/python/baplus/__init__.py", "ba_data/python/baplus/__pycache__/__init__.cpython-312.opt-1.pyc", + "ba_data/python/baplus/__pycache__/_appsubsystem.cpython-312.opt-1.pyc", "ba_data/python/baplus/__pycache__/_cloud.cpython-312.opt-1.pyc", "ba_data/python/baplus/__pycache__/_hooks.cpython-312.opt-1.pyc", - "ba_data/python/baplus/__pycache__/_subsystem.cpython-312.opt-1.pyc", + "ba_data/python/baplus/_appsubsystem.py", "ba_data/python/baplus/_cloud.py", "ba_data/python/baplus/_hooks.py", - "ba_data/python/baplus/_subsystem.py", "ba_data/python/bascenev1/__init__.py", "ba_data/python/bascenev1/__pycache__/__init__.cpython-312.opt-1.pyc", "ba_data/python/bascenev1/__pycache__/_activity.cpython-312.opt-1.pyc", "ba_data/python/bascenev1/__pycache__/_activitytypes.cpython-312.opt-1.pyc", "ba_data/python/bascenev1/__pycache__/_actor.cpython-312.opt-1.pyc", - "ba_data/python/bascenev1/__pycache__/_appmode.cpython-312.opt-1.pyc", "ba_data/python/bascenev1/__pycache__/_campaign.cpython-312.opt-1.pyc", "ba_data/python/bascenev1/__pycache__/_collision.cpython-312.opt-1.pyc", "ba_data/python/bascenev1/__pycache__/_coopgame.cpython-312.opt-1.pyc", @@ -169,7 +168,6 @@ "ba_data/python/bascenev1/_activity.py", "ba_data/python/bascenev1/_activitytypes.py", "ba_data/python/bascenev1/_actor.py", - "ba_data/python/bascenev1/_appmode.py", "ba_data/python/bascenev1/_campaign.py", "ba_data/python/bascenev1/_collision.py", "ba_data/python/bascenev1/_coopgame.py", @@ -349,42 +347,44 @@ "ba_data/python/bascenev1lib/tutorial.py", "ba_data/python/batemplatefs/__init__.py", "ba_data/python/batemplatefs/__pycache__/__init__.cpython-312.opt-1.pyc", + "ba_data/python/batemplatefs/__pycache__/_appsubsystem.cpython-312.opt-1.pyc", "ba_data/python/batemplatefs/__pycache__/_hooks.cpython-312.opt-1.pyc", - "ba_data/python/batemplatefs/__pycache__/_subsystem.cpython-312.opt-1.pyc", + "ba_data/python/batemplatefs/_appsubsystem.py", "ba_data/python/batemplatefs/_hooks.py", - "ba_data/python/batemplatefs/_subsystem.py", "ba_data/python/bauiv1/__init__.py", "ba_data/python/bauiv1/__pycache__/__init__.cpython-312.opt-1.pyc", + "ba_data/python/bauiv1/__pycache__/_appsubsystem.cpython-312.opt-1.pyc", "ba_data/python/bauiv1/__pycache__/_hooks.cpython-312.opt-1.pyc", "ba_data/python/bauiv1/__pycache__/_keyboard.cpython-312.opt-1.pyc", - "ba_data/python/bauiv1/__pycache__/_subsystem.cpython-312.opt-1.pyc", "ba_data/python/bauiv1/__pycache__/_uitypes.cpython-312.opt-1.pyc", "ba_data/python/bauiv1/__pycache__/onscreenkeyboard.cpython-312.opt-1.pyc", + "ba_data/python/bauiv1/_appsubsystem.py", "ba_data/python/bauiv1/_hooks.py", "ba_data/python/bauiv1/_keyboard.py", - "ba_data/python/bauiv1/_subsystem.py", "ba_data/python/bauiv1/_uitypes.py", "ba_data/python/bauiv1/onscreenkeyboard.py", "ba_data/python/bauiv1lib/__init__.py", "ba_data/python/bauiv1lib/__pycache__/__init__.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/achievements.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/appinvite.cpython-312.opt-1.pyc", + "ba_data/python/bauiv1lib/__pycache__/benchmarks.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/characterpicker.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/colorpicker.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/config.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/confirm.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/connectivity.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/continues.cpython-312.opt-1.pyc", - "ba_data/python/bauiv1lib/__pycache__/creditslist.cpython-312.opt-1.pyc", - "ba_data/python/bauiv1lib/__pycache__/debug.cpython-312.opt-1.pyc", + "ba_data/python/bauiv1lib/__pycache__/credits.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/discord.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/feedback.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/fileselector.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/getremote.cpython-312.opt-1.pyc", - "ba_data/python/bauiv1lib/__pycache__/gettickets.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/gettokens.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/helpui.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/iconpicker.cpython-312.opt-1.pyc", + "ba_data/python/bauiv1lib/__pycache__/inbox.cpython-312.opt-1.pyc", + "ba_data/python/bauiv1lib/__pycache__/ingamemenu.cpython-312.opt-1.pyc", + "ba_data/python/bauiv1lib/__pycache__/inventory.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/kiosk.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/mainmenu.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/party.cpython-312.opt-1.pyc", @@ -422,6 +422,7 @@ "ba_data/python/bauiv1lib/account/viewer.py", "ba_data/python/bauiv1lib/achievements.py", "ba_data/python/bauiv1lib/appinvite.py", + "ba_data/python/bauiv1lib/benchmarks.py", "ba_data/python/bauiv1lib/characterpicker.py", "ba_data/python/bauiv1lib/colorpicker.py", "ba_data/python/bauiv1lib/config.py", @@ -438,8 +439,7 @@ "ba_data/python/bauiv1lib/coop/gamebutton.py", "ba_data/python/bauiv1lib/coop/level.py", "ba_data/python/bauiv1lib/coop/tournamentbutton.py", - "ba_data/python/bauiv1lib/creditslist.py", - "ba_data/python/bauiv1lib/debug.py", + "ba_data/python/bauiv1lib/credits.py", "ba_data/python/bauiv1lib/discord.py", "ba_data/python/bauiv1lib/feedback.py", "ba_data/python/bauiv1lib/fileselector.py", @@ -456,10 +456,12 @@ "ba_data/python/bauiv1lib/gather/privatetab.py", "ba_data/python/bauiv1lib/gather/publictab.py", "ba_data/python/bauiv1lib/getremote.py", - "ba_data/python/bauiv1lib/gettickets.py", "ba_data/python/bauiv1lib/gettokens.py", "ba_data/python/bauiv1lib/helpui.py", "ba_data/python/bauiv1lib/iconpicker.py", + "ba_data/python/bauiv1lib/inbox.py", + "ba_data/python/bauiv1lib/ingamemenu.py", + "ba_data/python/bauiv1lib/inventory.py", "ba_data/python/bauiv1lib/keyboard/__init__.py", "ba_data/python/bauiv1lib/keyboard/__pycache__/__init__.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/keyboard/__pycache__/englishkeyboard.cpython-312.opt-1.pyc", diff --git a/src/assets/Makefile b/src/assets/Makefile index ec7bbaf9..1e474a92 100644 --- a/src/assets/Makefile +++ b/src/assets/Makefile @@ -198,28 +198,27 @@ SCRIPT_TARGETS_PY_PUBLIC = \ $(BUILD_DIR)/ba_data/python/baclassic/_achievement.py \ $(BUILD_DIR)/ba_data/python/baclassic/_ads.py \ $(BUILD_DIR)/ba_data/python/baclassic/_analytics.py \ - $(BUILD_DIR)/ba_data/python/baclassic/_appdelegate.py \ + $(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/_input.py \ $(BUILD_DIR)/ba_data/python/baclassic/_music.py \ $(BUILD_DIR)/ba_data/python/baclassic/_net.py \ $(BUILD_DIR)/ba_data/python/baclassic/_servermode.py \ $(BUILD_DIR)/ba_data/python/baclassic/_store.py \ - $(BUILD_DIR)/ba_data/python/baclassic/_subsystem.py \ $(BUILD_DIR)/ba_data/python/baclassic/_tips.py \ $(BUILD_DIR)/ba_data/python/baclassic/_tournament.py \ $(BUILD_DIR)/ba_data/python/baclassic/macmusicapp.py \ $(BUILD_DIR)/ba_data/python/baclassic/osmusic.py \ $(BUILD_DIR)/ba_data/python/baenv.py \ $(BUILD_DIR)/ba_data/python/baplus/__init__.py \ + $(BUILD_DIR)/ba_data/python/baplus/_appsubsystem.py \ $(BUILD_DIR)/ba_data/python/baplus/_cloud.py \ $(BUILD_DIR)/ba_data/python/baplus/_hooks.py \ - $(BUILD_DIR)/ba_data/python/baplus/_subsystem.py \ $(BUILD_DIR)/ba_data/python/bascenev1/__init__.py \ $(BUILD_DIR)/ba_data/python/bascenev1/_activity.py \ $(BUILD_DIR)/ba_data/python/bascenev1/_activitytypes.py \ $(BUILD_DIR)/ba_data/python/bascenev1/_actor.py \ - $(BUILD_DIR)/ba_data/python/bascenev1/_appmode.py \ $(BUILD_DIR)/ba_data/python/bascenev1/_campaign.py \ $(BUILD_DIR)/ba_data/python/bascenev1/_collision.py \ $(BUILD_DIR)/ba_data/python/bascenev1/_coopgame.py \ @@ -325,12 +324,12 @@ SCRIPT_TARGETS_PY_PUBLIC = \ $(BUILD_DIR)/ba_data/python/bascenev1lib/session/__init__.py \ $(BUILD_DIR)/ba_data/python/bascenev1lib/tutorial.py \ $(BUILD_DIR)/ba_data/python/batemplatefs/__init__.py \ + $(BUILD_DIR)/ba_data/python/batemplatefs/_appsubsystem.py \ $(BUILD_DIR)/ba_data/python/batemplatefs/_hooks.py \ - $(BUILD_DIR)/ba_data/python/batemplatefs/_subsystem.py \ $(BUILD_DIR)/ba_data/python/bauiv1/__init__.py \ + $(BUILD_DIR)/ba_data/python/bauiv1/_appsubsystem.py \ $(BUILD_DIR)/ba_data/python/bauiv1/_hooks.py \ $(BUILD_DIR)/ba_data/python/bauiv1/_keyboard.py \ - $(BUILD_DIR)/ba_data/python/bauiv1/_subsystem.py \ $(BUILD_DIR)/ba_data/python/bauiv1/_uitypes.py \ $(BUILD_DIR)/ba_data/python/bauiv1/onscreenkeyboard.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__init__.py \ @@ -342,6 +341,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \ $(BUILD_DIR)/ba_data/python/bauiv1lib/account/viewer.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/achievements.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/appinvite.py \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/benchmarks.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/characterpicker.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/colorpicker.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/config.py \ @@ -353,8 +353,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \ $(BUILD_DIR)/ba_data/python/bauiv1lib/coop/gamebutton.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/coop/level.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/coop/tournamentbutton.py \ - $(BUILD_DIR)/ba_data/python/bauiv1lib/creditslist.py \ - $(BUILD_DIR)/ba_data/python/bauiv1lib/debug.py \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/credits.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/discord.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/feedback.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/fileselector.py \ @@ -365,10 +364,12 @@ SCRIPT_TARGETS_PY_PUBLIC = \ $(BUILD_DIR)/ba_data/python/bauiv1lib/gather/privatetab.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/gather/publictab.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/getremote.py \ - $(BUILD_DIR)/ba_data/python/bauiv1lib/gettickets.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/gettokens.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/helpui.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/iconpicker.py \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/inbox.py \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/ingamemenu.py \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/inventory.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/keyboard/__init__.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/keyboard/englishkeyboard.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/kiosk.py \ @@ -476,28 +477,27 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_achievement.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_ads.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_analytics.cpython-312.opt-1.pyc \ - $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_appdelegate.cpython-312.opt-1.pyc \ + $(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__/_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 \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_servermode.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_store.cpython-312.opt-1.pyc \ - $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_subsystem.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_tips.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_tournament.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/macmusicapp.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/osmusic.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/__pycache__/baenv.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baplus/__pycache__/__init__.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baplus/__pycache__/_appsubsystem.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baplus/__pycache__/_cloud.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baplus/__pycache__/_hooks.cpython-312.opt-1.pyc \ - $(BUILD_DIR)/ba_data/python/baplus/__pycache__/_subsystem.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/__init__.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_activity.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_activitytypes.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_actor.cpython-312.opt-1.pyc \ - $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_appmode.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_campaign.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_collision.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_coopgame.cpython-312.opt-1.pyc \ @@ -603,12 +603,12 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ $(BUILD_DIR)/ba_data/python/bascenev1lib/session/__pycache__/__init__.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bascenev1lib/__pycache__/tutorial.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/batemplatefs/__pycache__/__init__.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/batemplatefs/__pycache__/_appsubsystem.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/batemplatefs/__pycache__/_hooks.cpython-312.opt-1.pyc \ - $(BUILD_DIR)/ba_data/python/batemplatefs/__pycache__/_subsystem.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/__init__.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_appsubsystem.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_hooks.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_keyboard.cpython-312.opt-1.pyc \ - $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_subsystem.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_uitypes.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/onscreenkeyboard.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/__init__.cpython-312.opt-1.pyc \ @@ -620,6 +620,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ $(BUILD_DIR)/ba_data/python/bauiv1lib/account/__pycache__/viewer.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/achievements.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/appinvite.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/benchmarks.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/characterpicker.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/colorpicker.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/config.cpython-312.opt-1.pyc \ @@ -631,8 +632,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ $(BUILD_DIR)/ba_data/python/bauiv1lib/coop/__pycache__/gamebutton.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/coop/__pycache__/level.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/coop/__pycache__/tournamentbutton.cpython-312.opt-1.pyc \ - $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/creditslist.cpython-312.opt-1.pyc \ - $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/debug.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/credits.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/discord.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/feedback.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/fileselector.cpython-312.opt-1.pyc \ @@ -643,10 +643,12 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ $(BUILD_DIR)/ba_data/python/bauiv1lib/gather/__pycache__/privatetab.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/gather/__pycache__/publictab.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/getremote.cpython-312.opt-1.pyc \ - $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/gettickets.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/gettokens.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/helpui.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/iconpicker.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/inbox.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/ingamemenu.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/inventory.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/keyboard/__pycache__/__init__.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/keyboard/__pycache__/englishkeyboard.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/kiosk.cpython-312.opt-1.pyc \ diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py index 61b75aed..50a08100 100644 --- a/src/assets/ba_data/python/babase/__init__.py +++ b/src/assets/ba_data/python/babase/__init__.py @@ -21,6 +21,7 @@ from efro.util import set_canonical_module_names import _babase from _babase import ( add_clean_frame_callback, + allows_ticket_sales, android_get_external_files_dir, appname, appnameupper, @@ -86,6 +87,7 @@ from _babase import ( overlay_web_browser_is_supported, overlay_web_browser_open_url, print_load_info, + push_back_press, pushcall, quit, reload_media, @@ -188,6 +190,7 @@ __all__ = [ 'AccountV2Subsystem', 'ActivityNotFoundError', 'ActorNotFoundError', + 'allows_ticket_sales', 'add_clean_frame_callback', 'android_get_external_files_dir', 'app', @@ -305,6 +308,7 @@ __all__ = [ 'print_error', 'print_exception', 'print_load_info', + 'push_back_press', 'pushcall', 'quit', 'QuitType', diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py index 58ba2fce..f852fbd6 100644 --- a/src/assets/ba_data/python/babase/_app.py +++ b/src/assets/ba_data/python/babase/_app.py @@ -36,9 +36,9 @@ if TYPE_CHECKING: # __FEATURESET_APP_SUBSYSTEM_IMPORTS_BEGIN__ # This section generated by batools.appmodule; do not edit. - from baclassic import ClassicSubsystem - from baplus import PlusSubsystem - from bauiv1 import UIV1Subsystem + from baclassic import ClassicAppSubsystem + from baplus import PlusAppSubsystem + from bauiv1 import UIV1AppSubsystem # __FEATURESET_APP_SUBSYSTEM_IMPORTS_END__ @@ -137,11 +137,11 @@ class App: # Ask our default app modes to handle it. # (generated from 'default_app_modes' in projectconfig). - import bascenev1 + import baclassic import babase for appmode in [ - bascenev1.SceneV1AppMode, + baclassic.ClassicAppMode, babase.EmptyAppMode, ]: if appmode.can_handle_intent(intent): @@ -151,6 +151,24 @@ class App: # __DEFAULT_APP_MODE_SELECTION_END__ + @override + def testable_app_modes(self) -> list[type[AppMode]]: + # pylint: disable=cyclic-import + + # __DEFAULT_TESTABLE_APP_MODES_BEGIN__ + # This section generated by batools.appmodule; do not edit. + + # Return all our default_app_modes as testable. + # (generated from 'default_app_modes' in projectconfig). + import baclassic + import babase + + return [ + baclassic.ClassicAppMode, + babase.EmptyAppMode, + ] + # __DEFAULT_TESTABLE_APP_MODES_END__ + def __init__(self) -> None: """(internal) @@ -208,8 +226,9 @@ class App: self._config: babase.AppConfig | None = None self._pending_intent: AppIntent | None = None self._intent: AppIntent | None = None - self._mode: AppMode | None = None self._mode_selector: babase.AppModeSelector | None = None + self._mode_instances: dict[type[AppMode], AppMode] = {} + self._mode: AppMode | None = None self._shutdown_task: asyncio.Task[None] | None = None self._shutdown_tasks: list[Coroutine[None, None, None]] = [ self._wait_for_shutdown_suppressions(), @@ -289,7 +308,7 @@ class App: """ assert _babase.in_logic_thread() - # Hold a strong reference to the task until it is done. + # We hold a strong reference to the task until it is done. # Otherwise it is possible for it to be garbage collected and # disappear midway if the caller does not hold on to the # returned task, which seems like a great way to introduce @@ -387,19 +406,19 @@ class App: # This section generated by batools.appmodule; do not edit. @property - def classic(self) -> ClassicSubsystem | None: + def classic(self) -> ClassicAppSubsystem | None: """Our classic subsystem (if available).""" return self._get_subsystem_property( 'classic', self._create_classic_subsystem ) # type: ignore @staticmethod - def _create_classic_subsystem() -> ClassicSubsystem | None: + def _create_classic_subsystem() -> ClassicAppSubsystem | None: # pylint: disable=cyclic-import try: - from baclassic import ClassicSubsystem + from baclassic import ClassicAppSubsystem - return ClassicSubsystem() + return ClassicAppSubsystem() except ImportError: return None except Exception: @@ -407,19 +426,19 @@ class App: return None @property - def plus(self) -> PlusSubsystem | None: + def plus(self) -> PlusAppSubsystem | None: """Our plus subsystem (if available).""" return self._get_subsystem_property( 'plus', self._create_plus_subsystem ) # type: ignore @staticmethod - def _create_plus_subsystem() -> PlusSubsystem | None: + def _create_plus_subsystem() -> PlusAppSubsystem | None: # pylint: disable=cyclic-import try: - from baplus import PlusSubsystem + from baplus import PlusAppSubsystem - return PlusSubsystem() + return PlusAppSubsystem() except ImportError: return None except Exception: @@ -427,19 +446,19 @@ class App: return None @property - def ui_v1(self) -> UIV1Subsystem: + def ui_v1(self) -> UIV1AppSubsystem: """Our ui_v1 subsystem (always available).""" return self._get_subsystem_property( 'ui_v1', self._create_ui_v1_subsystem ) # type: ignore @staticmethod - def _create_ui_v1_subsystem() -> UIV1Subsystem: + def _create_ui_v1_subsystem() -> UIV1AppSubsystem: # pylint: disable=cyclic-import - from bauiv1 import UIV1Subsystem + from bauiv1 import UIV1AppSubsystem - return UIV1Subsystem() + return UIV1AppSubsystem() # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_END__ @@ -611,17 +630,32 @@ class App: self._update_state() def _set_intent(self, intent: AppIntent) -> None: + from babase._appmode import AppMode + # This should be happening in a bg thread. assert not _babase.in_logic_thread() try: # Ask the selector what app-mode to use for this intent. if self.mode_selector is None: raise RuntimeError('No AppModeSelector set.') - modetype = self.mode_selector.app_mode_for_intent(intent) - # NOTE: Since intents are somewhat high level things, should - # we do some universal thing like a screenmessage saying - # 'The app cannot handle that request' on failure? + modetype: type[AppMode] | None + + # Special case - for testing we may force a specific + # app-mode to handle this intent instead of going through our + # usual selector. + forced_mode_type = getattr(intent, '_force_app_mode_handler', None) + if isinstance(forced_mode_type, type) and issubclass( + forced_mode_type, AppMode + ): + modetype = forced_mode_type + else: + modetype = self.mode_selector.app_mode_for_intent(intent) + + # NOTE: Since intents are somewhat high level things, + # perhaps we should do some universal thing like a + # screenmessage saying 'The app cannot handle the request' + # on failure. if modetype is None: raise RuntimeError( @@ -640,7 +674,9 @@ class App: # Ok; seems legit. Now instantiate the mode if necessary and # kick back to the logic thread to apply. - mode = modetype() + mode = self._mode_instances.get(modetype) + if mode is None: + self._mode_instances[modetype] = mode = modetype() _babase.pushcall( partial(self._apply_intent, intent, mode), from_other_thread=True, @@ -661,7 +697,7 @@ class App: return # If the app-mode for this intent is different than the active - # one, switch. + # one, switch modes. if type(mode) is not type(self._mode): if self._mode is None: is_initial_mode = True @@ -673,6 +709,18 @@ class App: logging.exception( 'Error deactivating app-mode %s.', self._mode ) + + # Reset all subsystems. We assume subsystems won't be added + # at this point so we can use the list directly. + assert self._subsystem_registration_ended + for subsystem in self._subsystems: + try: + subsystem.reset() + except Exception: + logging.exception( + 'Error in reset for subsystem %s.', subsystem + ) + self._mode = mode try: mode.on_activate() @@ -750,8 +798,8 @@ class App: self.meta.start_scan(scan_complete_cb=self._on_meta_scan_complete) # Inform all app subsystems in the same order they were inited. - # Operate on a copy here because subsystems can still be added - # at this point. + # Operate on a copy of the list here because subsystems can + # still be added at this point. for subsystem in self._subsystems.copy(): try: subsystem.on_app_loading() diff --git a/src/assets/ba_data/python/babase/_appmode.py b/src/assets/ba_data/python/babase/_appmode.py index cfe01f7b..26a8b6f8 100644 --- a/src/assets/ba_data/python/babase/_appmode.py +++ b/src/assets/ba_data/python/babase/_appmode.py @@ -31,7 +31,7 @@ class AppMode: AppExperience associated with the AppMode must be supported by the current app and runtime environment. """ - # FIXME: check AppExperience. + # TODO: check AppExperience. return cls._supports_intent(intent) @classmethod diff --git a/src/assets/ba_data/python/babase/_appmodeselector.py b/src/assets/ba_data/python/babase/_appmodeselector.py index 9dff2cbc..dcd60602 100644 --- a/src/assets/ba_data/python/babase/_appmodeselector.py +++ b/src/assets/ba_data/python/babase/_appmodeselector.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: class AppModeSelector: - """Defines which AppModes to use to handle given AppIntents. + """Defines which AppModes are available or used to handle given AppIntents. Category: **App Classes** @@ -29,4 +29,16 @@ class AppModeSelector: This may be called in a background thread, so avoid any calls limited to logic thread use/etc. """ - raise NotImplementedError('app_mode_for_intent() should be overridden.') + raise NotImplementedError() + + def testable_app_modes(self) -> list[type[AppMode]]: + """Return a list of modes to appear in the dev-console app-mode ui. + + The user can switch between these app modes for testing. App-modes + will be passed an AppIntentDefault when selected by the user. + + Note that in normal circumstances AppModes should never be + selected explicitly by the user but rather determined implicitly + based on AppIntents. + """ + raise NotImplementedError() diff --git a/src/assets/ba_data/python/babase/_appsubsystem.py b/src/assets/ba_data/python/babase/_appsubsystem.py index 78ba01d5..b1636af2 100644 --- a/src/assets/ba_data/python/babase/_appsubsystem.py +++ b/src/assets/ba_data/python/babase/_appsubsystem.py @@ -53,3 +53,10 @@ class AppSubsystem: def do_apply_app_config(self) -> None: """Called when the app config should be applied.""" + + def reset(self) -> None: + """Reset the subsystem to a default state. + + This is called when switching app modes, but may be called + at other times too. + """ diff --git a/src/assets/ba_data/python/babase/_devconsole.py b/src/assets/ba_data/python/babase/_devconsole.py index d4e70996..e4380f4e 100644 --- a/src/assets/ba_data/python/babase/_devconsole.py +++ b/src/assets/ba_data/python/babase/_devconsole.py @@ -13,6 +13,8 @@ import _babase if TYPE_CHECKING: from typing import Callable, Any, Literal + from babase import AppMode + class DevConsoleTab: """Defines behavior for a tab in the dev-console.""" @@ -101,6 +103,102 @@ class DevConsoleTabPython(DevConsoleTab): self.python_terminal() +class DevConsoleTabAppModes(DevConsoleTab): + """Tab to switch app modes.""" + + @override + def refresh(self) -> None: + from functools import partial + + modes = _babase.app.mode_selector.testable_app_modes() + self.text( + 'Available AppModes:', + scale=0.8, + pos=(15, 55), + h_anchor='left', + h_align='left', + v_align='none', + ) + for i, mode in enumerate(modes): + self.button( + f'{mode.__module__}.{mode.__qualname__}', + pos=(10 + i * 260, 10), + size=(250, 40), + h_anchor='left', + label_scale=0.6, + call=partial(self._set_app_mode, mode), + ) + + @staticmethod + def _set_app_mode(mode: type[AppMode]) -> None: + from babase._appintent import AppIntentDefault + + intent = AppIntentDefault() + + # Use private functionality to force a specific app-mode to + # handle this intent. Note that this should never be done + # outside of this explicit testing case. It is the app's job to + # determine which app-mode should be used to handle a given + # intent. + setattr(intent, '_force_app_mode_handler', mode) + + _babase.app.set_intent(intent) + + +class DevConsoleTabUI(DevConsoleTab): + """Tab to debug/test UI stuff.""" + + @override + def refresh(self) -> None: + + self.text( + 'UI Testing: Make sure all static UI fits in the' + ' virtual screen at all UI scales (not counting things' + ' that follow screen edges).', + scale=0.8, + pos=(15, 55), + h_anchor='left', + h_align='left', + v_align='none', + ) + + ui_overlay = _babase.get_draw_ui_bounds() + self.button( + 'Hide Virtual Screen' if ui_overlay else 'Show Virtual Screen', + pos=(10, 10), + size=(200, 30), + h_anchor='left', + label_scale=0.6, + call=self.toggle_ui_overlay, + ) + x = 320 + self.text( + 'UI Scale:', + pos=(x - 10, 15), + h_anchor='left', + h_align='right', + v_align='none', + scale=0.6, + ) + + bwidth = 100 + for sz in ('small', 'medium', 'large'): + self.button( + sz, + pos=(x, 10), + size=(bwidth, 30), + h_anchor='left', + label_scale=0.6, + call=lambda: _babase.screenmessage('UNDER CONSTRUCTION.'), + ) + x += bwidth + 10 + + def toggle_ui_overlay(self) -> None: + """Toggle UI overlay drawing.""" + _babase.set_draw_ui_bounds(not _babase.get_draw_ui_bounds()) + self.request_refresh() + + class DevConsoleTabTest(DevConsoleTab): """Test dev-console tab.""" @@ -157,7 +255,9 @@ class DevConsoleSubsystem: # All tabs in the dev-console. Add your own stuff here via # plugins or whatnot. self.tabs: list[DevConsoleTabEntry] = [ - DevConsoleTabEntry('Python', DevConsoleTabPython) + DevConsoleTabEntry('Python', DevConsoleTabPython), + DevConsoleTabEntry('AppModes', DevConsoleTabAppModes), + DevConsoleTabEntry('UI', DevConsoleTabUI), ] if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1': self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest)) diff --git a/src/assets/ba_data/python/babase/_emptyappmode.py b/src/assets/ba_data/python/babase/_emptyappmode.py index 6745d21c..801aa513 100644 --- a/src/assets/ba_data/python/babase/_emptyappmode.py +++ b/src/assets/ba_data/python/babase/_emptyappmode.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: class EmptyAppMode(AppMode): - """An empty app mode that can be used as a fallback/etc.""" + """An AppMode that does not do much at all.""" @override @classmethod @@ -32,17 +32,17 @@ class EmptyAppMode(AppMode): @override def handle_intent(self, intent: AppIntent) -> None: if isinstance(intent, AppIntentExec): - _babase.empty_app_mode_handle_intent_exec(intent.code) + _babase.empty_app_mode_handle_app_intent_exec(intent.code) return assert isinstance(intent, AppIntentDefault) - _babase.empty_app_mode_handle_intent_default() + _babase.empty_app_mode_handle_app_intent_default() @override def on_activate(self) -> None: # Let the native layer do its thing. - _babase.on_empty_app_mode_activate() + _babase.empty_app_mode_activate() @override def on_deactivate(self) -> None: # Let the native layer do its thing. - _babase.on_empty_app_mode_deactivate() + _babase.empty_app_mode_deactivate() diff --git a/src/assets/ba_data/python/baclassic/__init__.py b/src/assets/ba_data/python/baclassic/__init__.py index 51f50da5..8d9bbb81 100644 --- a/src/assets/ba_data/python/baclassic/__init__.py +++ b/src/assets/ba_data/python/baclassic/__init__.py @@ -1,15 +1,15 @@ # Released under the MIT License. See LICENSE for details. # -"""Classic ballistica components. +"""Components for the classic BombSquad experience. -This package is used as a 'dumping ground' for functionality that is -necessary to keep legacy parts of the app working, but which may no -longer be the best way to do things going forward. +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 -ClassicSubsystem. This allows type-checked code to go 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 @@ -19,20 +19,28 @@ classic not being present. # ba_meta require api 8 # Note: Code relying on classic should import things from here *only* -# for type-checking and use the versions in app.classic at runtime; that -# way type-checking will cleanly cover the classic-not-present case -# (app.classic being None). +# for type-checking and use the versions in ba*.app.classic at runtime; +# that way type-checking will cleanly cover the classic-not-present case +# (ba*.app.classic being None). import logging -from baclassic._subsystem import ClassicSubsystem +from efro.util import set_canonical_module_names + +from baclassic._appmode import ClassicAppMode +from baclassic._appsubsystem import ClassicAppSubsystem from baclassic._achievement import Achievement, AchievementSubsystem __all__ = [ - 'ClassicSubsystem', + 'ClassicAppMode', + 'ClassicAppSubsystem', 'Achievement', 'AchievementSubsystem', ] +# We want stuff here to show up as packagename.Foo instead of +# packagename._submodule.Foo. +set_canonical_module_names(globals()) + # Sanity check: we want to keep ballistica's dependencies and # bootstrapping order clearly defined; let's check a few particular # modules to make sure they never directly or indirectly import us diff --git a/src/assets/ba_data/python/baclassic/_appdelegate.py b/src/assets/ba_data/python/baclassic/_appdelegate.py deleted file mode 100644 index 1dd1a6b0..00000000 --- a/src/assets/ba_data/python/baclassic/_appdelegate.py +++ /dev/null @@ -1,46 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Defines AppDelegate class for handling high level app functionality.""" -from __future__ import annotations - -from typing import TYPE_CHECKING - -import babase - -if TYPE_CHECKING: - from typing import Callable - import bascenev1 - - -class AppDelegate: - """Defines handlers for high level app functionality. - - Category: App Classes - """ - - def create_default_game_settings_ui( - self, - gameclass: type[bascenev1.GameActivity], - sessiontype: type[bascenev1.Session], - settings: dict | None, - completion_call: Callable[[dict | None], None], - ) -> None: - """Launch a UI to configure the given game config. - - It should manipulate the contents of config and call completion_call - when done. - """ - # Replace the main window once we come up successfully. - from bauiv1lib.playlist.editgame import PlaylistEditGameWindow - - assert babase.app.classic is not None - babase.app.ui_v1.clear_main_menu_window(transition='out_left') - babase.app.ui_v1.set_main_menu_window( - PlaylistEditGameWindow( - gameclass, - sessiontype, - settings, - completion_call=completion_call, - ).get_root_widget(), - from_window=False, # Disable check since we don't know. - ) diff --git a/src/assets/ba_data/python/baclassic/_appmode.py b/src/assets/ba_data/python/baclassic/_appmode.py new file mode 100644 index 00000000..bcb3b3c0 --- /dev/null +++ b/src/assets/ba_data/python/baclassic/_appmode.py @@ -0,0 +1,310 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Contains ClassicAppMode.""" +from __future__ import annotations + +import logging +from functools import partial +from typing import TYPE_CHECKING, override + +from bacommon.app import AppExperience +from babase import ( + app, + AppMode, + AppIntentExec, + AppIntentDefault, + invoke_main_menu, + screenmessage, +) + +import _baclassic + +if TYPE_CHECKING: + from babase import AppIntent + from bauiv1 import UIV1AppSubsystem, MainWindow + + +class ClassicAppMode(AppMode): + """AppMode for the classic BombSquad experience.""" + + @override + @classmethod + def get_app_experience(cls) -> AppExperience: + return AppExperience.MELEE + + @override + @classmethod + def _supports_intent(cls, intent: AppIntent) -> bool: + # We support default and exec intents currently. + return isinstance(intent, AppIntentExec | AppIntentDefault) + + @override + def handle_intent(self, intent: AppIntent) -> None: + if isinstance(intent, AppIntentExec): + _baclassic.classic_app_mode_handle_app_intent_exec(intent.code) + return + assert isinstance(intent, AppIntentDefault) + _baclassic.classic_app_mode_handle_app_intent_default() + + @override + def on_activate(self) -> None: + # Let the native layer do its thing. + _baclassic.classic_app_mode_activate() + + # Wire up the root ui to do what we want. + ui = app.ui_v1 + ui.root_ui_calls[ui.RootUIElement.ACCOUNT_BUTTON] = ( + self._root_ui_account_press + ) + ui.root_ui_calls[ui.RootUIElement.MENU_BUTTON] = ( + self._root_ui_menu_press + ) + ui.root_ui_calls[ui.RootUIElement.SQUAD_BUTTON] = ( + self._root_ui_squad_press + ) + ui.root_ui_calls[ui.RootUIElement.SETTINGS_BUTTON] = ( + self._root_ui_settings_press + ) + ui.root_ui_calls[ui.RootUIElement.STORE_BUTTON] = ( + self._root_ui_store_press + ) + ui.root_ui_calls[ui.RootUIElement.INVENTORY_BUTTON] = ( + self._root_ui_inventory_press + ) + ui.root_ui_calls[ui.RootUIElement.GET_TOKENS_BUTTON] = ( + self._root_ui_get_tokens_press + ) + ui.root_ui_calls[ui.RootUIElement.INBOX_BUTTON] = ( + self._root_ui_inbox_press + ) + ui.root_ui_calls[ui.RootUIElement.TICKETS_METER] = ( + self._root_ui_tickets_meter_press + ) + ui.root_ui_calls[ui.RootUIElement.TOKENS_METER] = ( + self._root_ui_tokens_meter_press + ) + ui.root_ui_calls[ui.RootUIElement.TROPHY_METER] = ( + self._root_ui_trophy_meter_press + ) + ui.root_ui_calls[ui.RootUIElement.LEVEL_METER] = ( + self._root_ui_level_meter_press + ) + ui.root_ui_calls[ui.RootUIElement.ACHIEVEMENTS_BUTTON] = ( + self._root_ui_achievements_press + ) + ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_1] = partial( + self._root_ui_chest_slot_pressed, 1 + ) + ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_2] = partial( + self._root_ui_chest_slot_pressed, 2 + ) + ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_3] = partial( + self._root_ui_chest_slot_pressed, 3 + ) + ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_4] = partial( + self._root_ui_chest_slot_pressed, 4 + ) + + @override + def on_deactivate(self) -> None: + # Let the native layer do its thing. + _baclassic.classic_app_mode_deactivate() + + @override + def on_app_active_changed(self) -> None: + # If we've gone inactive, bring up the main menu, which has the + # side effect of pausing the action (when possible). + if not app.active: + invoke_main_menu() + + def _jump_to_main_window(self, window: MainWindow) -> None: + """Jump to a window with the main menu as its parent.""" + from bauiv1lib.mainmenu import MainMenuWindow + + ui = app.ui_v1 + + old_window = ui.get_main_window() + if isinstance(old_window, MainMenuWindow): + old_window.main_window_replace(window) + else: + # Blow away the window stack. + ui.clear_main_window() + + ui.set_main_window( + window, + from_window=False, # Disable from-check. + back_state=MainMenuWindow.do_get_main_window_state(), + ) + + def _root_ui_menu_press(self) -> None: + from babase import push_back_press + + ui = app.ui_v1 + + # If *any* main-window is up, kill it. + old_window = ui.get_main_window() + if old_window is not None: + ui.clear_main_window() + return + + push_back_press() + + def _root_ui_account_press(self) -> None: + import bauiv1 + from bauiv1lib.account.settings import AccountSettingsWindow + + ui = app.ui_v1 + + # If the window is already showing, back out of it. + current_main_window = ui.get_main_window() + if isinstance(current_main_window, AccountSettingsWindow): + current_main_window.main_window_back() + return + + self._jump_to_main_window( + AccountSettingsWindow( + origin_widget=bauiv1.get_special_widget('account_button') + ) + ) + + def _root_ui_squad_press(self) -> None: + import bauiv1 + + btn = bauiv1.get_special_widget('squad_button') + center = btn.get_screen_space_center() + if bauiv1.app.classic is not None: + bauiv1.app.classic.party_icon_activate(center) + else: + logging.warning('party_icon_activate: no classic.') + + def _root_ui_settings_press(self) -> None: + import bauiv1 + from bauiv1lib.settings.allsettings import AllSettingsWindow + + ui = app.ui_v1 + + # If the window is already showing, back out of it. + current_main_window = ui.get_main_window() + if isinstance(current_main_window, AllSettingsWindow): + current_main_window.main_window_back() + return + + self._jump_to_main_window( + AllSettingsWindow( + origin_widget=bauiv1.get_special_widget('settings_button') + ) + ) + + def _root_ui_achievements_press(self) -> None: + import bauiv1 + from bauiv1lib.achievements import AchievementsWindow + + btn = bauiv1.get_special_widget('achievements_button') + + AchievementsWindow(position=btn.get_screen_space_center()) + + def _root_ui_inbox_press(self) -> None: + import bauiv1 + from bauiv1lib.inbox import InboxWindow + + btn = bauiv1.get_special_widget('inbox_button') + + InboxWindow(position=btn.get_screen_space_center()) + + def _root_ui_store_press(self) -> None: + import bauiv1 + from bauiv1lib.store.browser import StoreBrowserWindow + + ui = app.ui_v1 + + # If the window is already showing, back out of it. + current_main_window = ui.get_main_window() + if isinstance(current_main_window, StoreBrowserWindow): + current_main_window.main_window_back() + return + + self._jump_to_main_window( + StoreBrowserWindow( + origin_widget=bauiv1.get_special_widget('store_button') + ) + ) + + def _root_ui_tickets_meter_press(self) -> None: + import bauiv1 + from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow + + ResourceTypeInfoWindow( + 'tickets', origin_widget=bauiv1.get_special_widget('tickets_meter') + ) + + def _root_ui_tokens_meter_press(self) -> None: + import bauiv1 + from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow + + ResourceTypeInfoWindow( + 'tokens', origin_widget=bauiv1.get_special_widget('tokens_meter') + ) + + def _root_ui_trophy_meter_press(self) -> None: + import bauiv1 + from bauiv1lib.account import show_sign_in_prompt + from bauiv1lib.league.rankwindow import LeagueRankWindow + + ui = app.ui_v1 + + # If the window is already showing, back out of it. + current_main_window = ui.get_main_window() + if isinstance(current_main_window, LeagueRankWindow): + current_main_window.main_window_back() + return + + plus = bauiv1.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': + show_sign_in_prompt() + return + + self._jump_to_main_window( + LeagueRankWindow( + origin_widget=bauiv1.get_special_widget('trophy_meter') + ) + ) + + def _root_ui_level_meter_press(self) -> None: + import bauiv1 + from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow + + ResourceTypeInfoWindow( + 'xp', origin_widget=bauiv1.get_special_widget('level_meter') + ) + + def _root_ui_inventory_press(self) -> None: + import bauiv1 + from bauiv1lib.inventory import InventoryWindow + + ui = app.ui_v1 + + # If the window is already showing, back out of it. + current_main_window = ui.get_main_window() + if isinstance(current_main_window, InventoryWindow): + current_main_window.main_window_back() + return + + self._jump_to_main_window( + InventoryWindow( + origin_widget=bauiv1.get_special_widget('inventory_button') + ) + ) + + def _root_ui_get_tokens_press(self) -> None: + import bauiv1 + from bauiv1lib.gettokens import GetTokensWindow + + GetTokensWindow( + origin_widget=bauiv1.get_special_widget('get_tokens_button') + ) + + def _root_ui_chest_slot_pressed(self, index: int) -> None: + print(f'CHEST {index} PRESSED') + screenmessage('UNDER CONSTRUCTION.') diff --git a/src/assets/ba_data/python/baclassic/_subsystem.py b/src/assets/ba_data/python/baclassic/_appsubsystem.py similarity index 80% rename from src/assets/ba_data/python/baclassic/_subsystem.py rename to src/assets/ba_data/python/baclassic/_appsubsystem.py index 16611ec9..a0fcb72c 100644 --- a/src/assets/ba_data/python/baclassic/_subsystem.py +++ b/src/assets/ba_data/python/baclassic/_appsubsystem.py @@ -29,12 +29,11 @@ if TYPE_CHECKING: from bascenev1lib.actor import spazappearance from bauiv1lib.party import PartyWindow - from baclassic._appdelegate import AppDelegate from baclassic._servermode import ServerController from baclassic._net import MasterServerCallback -class ClassicSubsystem(babase.AppSubsystem): +class ClassicAppSubsystem(babase.AppSubsystem): """Subsystem for classic functionality in the app. The single shared instance of this app can be accessed at @@ -111,8 +110,12 @@ class ClassicSubsystem(babase.AppSubsystem): self.did_menu_intro = False # FIXME: Move to mainmenu class. self.main_menu_window_refresh_check_count = 0 # FIXME: Mv to mainmenu. self.invite_confirm_windows: list[Any] = [] # FIXME: Don't use Any. - self.delegate: AppDelegate | None = None self.party_window: weakref.ref[PartyWindow] | None = None + self.main_menu_resume_callbacks: list = [] + # Switch our overall game selection UI flow between Play and + # Private-party playlist selection modes; should do this in + # a more elegant way once we revamp high level UI stuff a bit. + self.selecting_private_party_playlist: bool = False # Store. self.store_layout: dict[str, list[dict[str, Any]]] | None = None @@ -120,6 +123,16 @@ class ClassicSubsystem(babase.AppSubsystem): self.pro_sale_start_time: int | None = None self.pro_sale_start_val: int | None = None + def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: + """(internal)""" + + # If there's no main window up, just call immediately. + if not babase.app.ui_v1.has_main_window(): + with babase.ContextRef.empty(): + call() + else: + self.main_menu_resume_callbacks.append(call) + @property def platform(self) -> str: """Name of the current platform. @@ -154,8 +167,6 @@ class ClassicSubsystem(babase.AppSubsystem): from bascenev1lib.actor import spazappearance from bascenev1lib import maps as stdmaps - from baclassic._appdelegate import AppDelegate - plus = babase.app.plus assert plus is not None @@ -164,8 +175,6 @@ class ClassicSubsystem(babase.AppSubsystem): self.music.on_app_loading() - self.delegate = AppDelegate() - # Non-test, non-debug builds should generally be blessed; warn if not. # (so I don't accidentally release a build that can't play tourneys) if not env.debug and not env.test and not plus.is_blessed(): @@ -374,7 +383,7 @@ class ClassicSubsystem(babase.AppSubsystem): assert plus is not None if reset_ui: - babase.app.ui_v1.clear_main_menu_window() + babase.app.ui_v1.clear_main_window() if isinstance(bascenev1.get_foreground_host_session(), MainMenuSession): # It may be possible we're on the main menu but the screen is faded @@ -684,13 +693,13 @@ class ClassicSubsystem(babase.AppSubsystem): babase.Call(ServerDialogWindow, sddata), ) - def ticket_icon_press(self) -> None: - """(internal)""" - from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow + # def root_ui_ticket_icon_press(self) -> None: + # """(internal)""" + # from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow - ResourceTypeInfoWindow( - origin_widget=bauiv1.get_special_widget('tickets_info_button') - ) + # ResourceTypeInfoWindow( + # origin_widget=bauiv1.get_special_widget('tickets_meter') + # ) def show_url_window(self, address: str) -> None: """(internal)""" @@ -781,6 +790,9 @@ class ClassicSubsystem(babase.AppSubsystem): assert app.env.gui + # Play explicit swish sound so it occurs due to keypresses/etc. + # This means we have to disable it for any button or else we get + # double. bauiv1.getsound('swish').play() # If it exists, dismiss it; otherwise make a new one. @@ -794,18 +806,132 @@ class ClassicSubsystem(babase.AppSubsystem): def device_menu_press(self, device_id: int | None) -> None: """(internal)""" - from bauiv1lib.mainmenu import MainMenuWindow + from bauiv1lib.ingamemenu import InGameMenuWindow from bauiv1 import set_ui_input_device assert babase.app is not None - in_main_menu = babase.app.ui_v1.has_main_menu_window() + in_main_menu = babase.app.ui_v1.has_main_window() if not in_main_menu: set_ui_input_device(device_id) + # Hack(ish). We play swish sound here so it happens for + # device presses, but this means we need to disable default + # swish sounds for any menu buttons or we'll get double. if babase.app.env.gui: bauiv1.getsound('swish').play() - babase.app.ui_v1.set_main_menu_window( - MainMenuWindow().get_root_widget(), + babase.app.ui_v1.set_main_window( + InGameMenuWindow(), from_window=False, # Disable check here. + is_top_level=True, ) + + def invoke_main_menu_ui(self) -> None: + """Bring up main menu ui.""" + # Bring up the last place we were, or start at the main menu otherwise. + app = bauiv1.app + env = app.env + with bascenev1.ContextRef.empty(): + from bauiv1lib import specialoffer + + assert app.classic is not None + if app.env.headless: + # UI stuff fails now in headless builds; avoid it. + pass + else: + # main_menu_location = ( + # bascenev1.app.ui_v1.get_main_menu_location() + # ) + + # When coming back from a kiosk-mode game, jump to + # the kiosk start screen. + if env.demo or env.arcade: + # pylint: disable=cyclic-import + from bauiv1lib.kiosk import KioskWindow + + app.ui_v1.set_main_window( + KioskWindow(), from_window=False # Disable check here. + ) + # ..or in normal cases go back to the main menu + else: + # if main_menu_location == 'Gather': + # # pylint: disable=cyclic-import + # from bauiv1lib.gather import GatherWindow + + # app.ui_v1.set_main_window( + # GatherWindow(transition=None), + # from_window=False, # Disable check here. + # ) + # elif main_menu_location == 'Watch': + # # pylint: disable=cyclic-import + # from bauiv1lib.watch import WatchWindow + + # app.ui_v1.set_main_window( + # WatchWindow(transition=None), + # from_window=False, # Disable check here. + # ) + # elif main_menu_location == 'Team Game Select': + # # pylint: disable=cyclic-import + # from bauiv1lib.playlist.browser import ( + # PlaylistBrowserWindow, + # ) + + # app.ui_v1.set_main_window( + # PlaylistBrowserWindow( + # sessiontype=bascenev1.DualTeamSession, + # transition=None, + # ), + # from_window=False, # Disable check here. + # ) + # elif main_menu_location == 'Free-for-All Game Select': + # # pylint: disable=cyclic-import + # from bauiv1lib.playlist.browser import ( + # PlaylistBrowserWindow, + # ) + + # app.ui_v1.set_main_window( + # PlaylistBrowserWindow( + # sessiontype=bascenev1.FreeForAllSession, + # transition=None, + # ), + # from_window=False, # Disable check here. + # ) + # elif main_menu_location == 'Coop Select': + # # pylint: disable=cyclic-import + # from bauiv1lib.coop.browser import CoopBrowserWindow + + # app.ui_v1.set_main_window( + # CoopBrowserWindow(transition=None), + # from_window=False, # Disable check here. + # ) + # elif main_menu_location == 'Benchmarks & Stress Tests': + # # pylint: disable=cyclic-import + # from bauiv1lib.debug import DebugWindow + + # app.ui_v1.set_main_window( + # DebugWindow(transition=None), + # from_window=False, # Disable check here. + # ) + # else: + # pylint: disable=cyclic-import + from bauiv1lib.mainmenu import MainMenuWindow + + app.ui_v1.set_main_window( + MainMenuWindow(transition=None), + from_window=False, # Disable check. + is_top_level=True, + ) + + # attempt to show any pending offers immediately. + # If that doesn't work, try again in a few seconds + # (we may not have heard back from the server) + # ..if that doesn't work they'll just have to wait + # until the next opportunity. + if not specialoffer.show_offer(): + + def try_again() -> None: + if not specialoffer.show_offer(): + # Try one last time.. + bauiv1.apptimer(2.0, specialoffer.show_offer) + + bauiv1.apptimer(2.0, try_again) diff --git a/src/assets/ba_data/python/baclassic/_music.py b/src/assets/ba_data/python/baclassic/_music.py index 8f439d79..58246379 100644 --- a/src/assets/ba_data/python/baclassic/_music.py +++ b/src/assets/ba_data/python/baclassic/_music.py @@ -16,6 +16,8 @@ from bascenev1 import MusicType if TYPE_CHECKING: from typing import Callable, Any + import bauiv1 + class MusicPlayMode(Enum): """Influences behavior when playing music. @@ -389,7 +391,7 @@ class MusicPlayer: callback: Callable[[Any], None], current_entry: Any, selection_target_name: str, - ) -> Any: + ) -> bauiv1.MainWindow: """Summons a UI to select a new soundtrack entry.""" return self.on_select_entry( callback, current_entry, selection_target_name @@ -432,11 +434,12 @@ class MusicPlayer: callback: Callable[[Any], None], current_entry: Any, selection_target_name: str, - ) -> Any: + ) -> bauiv1.MainWindow: """Present a GUI to select an entry. The callback should be called with a valid entry or None to signify that the default soundtrack should be used..""" + raise NotImplementedError() # Subclasses should override the following: diff --git a/src/assets/ba_data/python/baclassic/macmusicapp.py b/src/assets/ba_data/python/baclassic/macmusicapp.py index 6b4b9b49..48a41e5a 100644 --- a/src/assets/ba_data/python/baclassic/macmusicapp.py +++ b/src/assets/ba_data/python/baclassic/macmusicapp.py @@ -15,6 +15,8 @@ from baclassic._music import MusicPlayer if TYPE_CHECKING: from typing import Callable, Any + import bauiv1 + class MacMusicAppMusicPlayer(MusicPlayer): """A music-player that utilizes the macOS Music.app for playback. @@ -33,7 +35,7 @@ class MacMusicAppMusicPlayer(MusicPlayer): callback: Callable[[Any], None], current_entry: Any, selection_target_name: str, - ) -> Any: + ) -> bauiv1.MainWindow: # pylint: disable=cyclic-import from bauiv1lib.soundtrack import entrytypeselect as etsel diff --git a/src/assets/ba_data/python/baclassic/osmusic.py b/src/assets/ba_data/python/baclassic/osmusic.py index 919df491..8a79bf73 100644 --- a/src/assets/ba_data/python/baclassic/osmusic.py +++ b/src/assets/ba_data/python/baclassic/osmusic.py @@ -16,6 +16,8 @@ from baclassic._music import MusicPlayer if TYPE_CHECKING: from typing import Callable, Any + import bauiv1 + class OSMusicPlayer(MusicPlayer): """Music player that talks to internal C++ layer for functionality. @@ -39,7 +41,7 @@ class OSMusicPlayer(MusicPlayer): callback: Callable[[Any], None], current_entry: Any, selection_target_name: str, - ) -> Any: + ) -> bauiv1.MainWindow: # pylint: disable=cyclic-import from bauiv1lib.soundtrack.entrytypeselect import ( SoundtrackEntryTypeSelectWindow, diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index a0875246..c8940edb 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -52,7 +52,7 @@ if TYPE_CHECKING: # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 21949 +TARGET_BALLISTICA_BUILD = 21968 TARGET_BALLISTICA_VERSION = '1.7.37' diff --git a/src/assets/ba_data/python/baplus/__init__.py b/src/assets/ba_data/python/baplus/__init__.py index 4040f7b4..9b2dba24 100644 --- a/src/assets/ba_data/python/baplus/__init__.py +++ b/src/assets/ba_data/python/baplus/__init__.py @@ -5,23 +5,23 @@ This code concerns sensitive things like accounts and master-server communication so the native C++ parts of it remain closed. Native precompiled static libraries of this portion are provided for those who -want to compile the rest of the engine, and a fully open-source engine -can also be built by removing this 'plus' feature-set. +want to compile the rest of the engine, or a fully open-source app +can also be built by removing this feature-set. """ from __future__ import annotations -# Note: there's not much here. -# All comms with this feature-set should go through app.plus. +# Note: there's not much here. Most interaction with this feature-set +# should go through ba*.app.plus. import logging from baplus._cloud import CloudSubsystem -from baplus._subsystem import PlusSubsystem +from baplus._appsubsystem import PlusAppSubsystem __all__ = [ 'CloudSubsystem', - 'PlusSubsystem', + 'PlusAppSubsystem', ] # Sanity check: we want to keep ballistica's dependencies and diff --git a/src/assets/ba_data/python/baplus/_subsystem.py b/src/assets/ba_data/python/baplus/_appsubsystem.py similarity index 99% rename from src/assets/ba_data/python/baplus/_subsystem.py rename to src/assets/ba_data/python/baplus/_appsubsystem.py index c5ef5ff6..6733699c 100644 --- a/src/assets/ba_data/python/baplus/_subsystem.py +++ b/src/assets/ba_data/python/baplus/_appsubsystem.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from baplus._cloud import CloudSubsystem -class PlusSubsystem(AppSubsystem): +class PlusAppSubsystem(AppSubsystem): """Subsystem for plus functionality in the app. The single shared instance of this app can be accessed at diff --git a/src/assets/ba_data/python/bascenev1/__init__.py b/src/assets/ba_data/python/bascenev1/__init__.py index b3513480..43c6550a 100644 --- a/src/assets/ba_data/python/bascenev1/__init__.py +++ b/src/assets/ba_data/python/bascenev1/__init__.py @@ -1,6 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Ballistica scene api version 1. Basically all gameplay related code.""" +"""Gameplay-centric api for classic BombSquad.""" # ba_meta require api 8 @@ -18,6 +18,7 @@ import logging from efro.util import set_canonical_module_names from babase import ( + add_clean_frame_callback, app, AppIntent, AppIntentDefault, @@ -149,7 +150,6 @@ from _bascenev1 import ( from bascenev1._activity import Activity from bascenev1._activitytypes import JoinActivity, ScoreScreenActivity from bascenev1._actor import Actor -from bascenev1._appmode import SceneV1AppMode from bascenev1._campaign import init_campaigns, Campaign from bascenev1._collision import Collision, getcollision from bascenev1._coopgame import CoopGameActivity @@ -249,6 +249,7 @@ __all__ = [ 'Actor', 'animate', 'animate_array', + 'add_clean_frame_callback', 'app', 'AppIntent', 'AppIntentDefault', @@ -410,7 +411,6 @@ __all__ = [ 'seek_replay', 'safecolor', 'screenmessage', - 'SceneV1AppMode', 'ScoreConfig', 'ScoreScreenActivity', 'ScoreType', diff --git a/src/assets/ba_data/python/bascenev1/_appmode.py b/src/assets/ba_data/python/bascenev1/_appmode.py deleted file mode 100644 index 72a77aee..00000000 --- a/src/assets/ba_data/python/bascenev1/_appmode.py +++ /dev/null @@ -1,60 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Provides AppMode functionality.""" -from __future__ import annotations - -from typing import TYPE_CHECKING, override - -from bacommon.app import AppExperience -from babase import ( - app, - AppMode, - AppIntentExec, - AppIntentDefault, - invoke_main_menu, -) - -import _bascenev1 - -if TYPE_CHECKING: - from babase import AppIntent - - -class SceneV1AppMode(AppMode): - """Our app-mode.""" - - @override - @classmethod - def get_app_experience(cls) -> AppExperience: - return AppExperience.MELEE - - @override - @classmethod - def _supports_intent(cls, intent: AppIntent) -> bool: - # We support default and exec intents currently. - return isinstance(intent, AppIntentExec | AppIntentDefault) - - @override - def handle_intent(self, intent: AppIntent) -> None: - if isinstance(intent, AppIntentExec): - _bascenev1.handle_app_intent_exec(intent.code) - return - assert isinstance(intent, AppIntentDefault) - _bascenev1.handle_app_intent_default() - - @override - def on_activate(self) -> None: - # Let the native layer do its thing. - _bascenev1.on_app_mode_activate() - - @override - def on_deactivate(self) -> None: - # Let the native layer do its thing. - _bascenev1.on_app_mode_deactivate() - - @override - def on_app_active_changed(self) -> None: - # If we've gone inactive, bring up the main menu, which has the - # side effect of pausing the action (when possible). - if not app.active: - invoke_main_menu() diff --git a/src/assets/ba_data/python/bascenev1/_gameactivity.py b/src/assets/ba_data/python/bascenev1/_gameactivity.py index 15177334..de6ecc05 100644 --- a/src/assets/ba_data/python/bascenev1/_gameactivity.py +++ b/src/assets/ba_data/python/bascenev1/_gameactivity.py @@ -87,11 +87,19 @@ class GameActivity(Activity[PlayerT, TeamT]): bascenev1.GameActivity.get_supported_maps() they can just rely on the default implementation here which calls those methods. """ + # pylint: disable=cyclic-import + from bauiv1lib.playlist.editgame import PlaylistEditGameWindow + assert babase.app.classic is not None - delegate = babase.app.classic.delegate - assert delegate is not None - delegate.create_default_game_settings_ui( - cls, sessiontype, settings, completion_call + babase.app.ui_v1.clear_main_window() + babase.app.ui_v1.set_main_window( + PlaylistEditGameWindow( + cls, + sessiontype, + settings, + completion_call=completion_call, + ), + from_window=False, # Disable check since we don't know. ) @classmethod @@ -465,7 +473,7 @@ class GameActivity(Activity[PlayerT, TeamT]): # Only attempt this if we're not currently paused # and there appears to be no UI. assert babase.app.classic is not None - hmmw = babase.app.ui_v1.has_main_menu_window() + hmmw = babase.app.ui_v1.has_main_window() if not gnode.paused and not hmmw: self._is_waiting_for_continue = True with babase.ContextRef.empty(): diff --git a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py index 4b92b69c..646b60a1 100644 --- a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py +++ b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py @@ -336,12 +336,13 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): def request_ui(self) -> None: """Set up a callback to show our UI at the next opportune time.""" - assert bui.app.classic is not None + classic = bui.app.classic + assert classic is not None # We don't want to just show our UI in case the user already has the # main menu up, so instead we add a callback for when the menu # closes; if we're still alive, we'll come up then. # If there's no main menu this gets called immediately. - bui.app.ui_v1.add_main_menu_close_callback(bui.WeakCall(self.show_ui)) + classic.add_main_menu_close_callback(bui.WeakCall(self.show_ui)) def show_ui(self) -> None: """Show the UI for restarting, playing the next Level, etc.""" diff --git a/src/assets/ba_data/python/bascenev1lib/mainmenu.py b/src/assets/ba_data/python/bascenev1lib/mainmenu.py index 84343e35..15d5e24f 100644 --- a/src/assets/ba_data/python/bascenev1lib/mainmenu.py +++ b/src/assets/ba_data/python/bascenev1lib/mainmenu.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Session and Activity for displaying the main menu bg.""" -# pylint: disable=too-many-lines from __future__ import annotations @@ -48,48 +47,15 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): def on_transition_in(self) -> None: # pylint: disable=too-many-locals # pylint: disable=too-many-statements - # pylint: disable=too-many-branches super().on_transition_in() random.seed(123) app = bs.app env = app.env assert app.classic is not None - plus = bui.app.plus + plus = bs.app.plus assert plus is not None - # FIXME: We shouldn't be doing things conditionally based on whether - # the host is VR mode or not (clients may differ in that regard). - # Any differences need to happen at the engine level so everyone - # sees things in their own optimal way. - vr_mode = bs.app.env.vr - - if not bs.app.ui_v1.use_toolbars: - color = (1.0, 1.0, 1.0, 1.0) if vr_mode else (0.5, 0.6, 0.5, 0.6) - - # FIXME: Need a node attr for vr-specific-scale. - scale = ( - 0.9 - if (app.ui_v1.uiscale is bs.UIScale.SMALL or vr_mode) - else 0.7 - ) - self.my_name = bs.NodeActor( - bs.newnode( - 'text', - attrs={ - 'v_attach': 'bottom', - 'h_align': 'center', - 'color': color, - 'flatness': 1.0, - 'shadow': 1.0 if vr_mode else 0.5, - 'scale': scale, - 'position': (0, 10), - 'vr_depth': -10, - 'text': '\xa9 2011-2024 Eric Froemling', - }, - ) - ) - # Throw up some text that only clients can see so they know that the # host is navigating menus while they're just staring at an # empty-ish screen. @@ -109,74 +75,17 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): }, ) ) - if not app.classic.main_menu_did_initial_transition and hasattr( - self, 'my_name' + if ( + not app.classic.main_menu_did_initial_transition + and self.my_name is not None ): - assert self.my_name is not None assert self.my_name.node bs.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0}) - # FIXME: We shouldn't be doing things conditionally based on whether - # the host is vr mode or not (clients may not be or vice versa). - # Any differences need to happen at the engine level so everyone sees - # things in their own optimal way. - vr_mode = app.env.vr - uiscale = app.ui_v1.uiscale - - # In cases where we're doing lots of dev work lets always show the - # build number. - force_show_build_number = False - - if not bs.app.ui_v1.use_toolbars: - if env.debug or env.test or force_show_build_number: - if env.debug: - text = bs.Lstr( - value='${V} (${B}) (${D})', - subs=[ - ('${V}', app.env.engine_version), - ('${B}', str(app.env.engine_build_number)), - ('${D}', bs.Lstr(resource='debugText')), - ], - ) - else: - text = bs.Lstr( - value='${V} (${B})', - subs=[ - ('${V}', app.env.engine_version), - ('${B}', str(app.env.engine_build_number)), - ], - ) - else: - text = bs.Lstr( - value='${V}', subs=[('${V}', app.env.engine_version)] - ) - scale = 0.9 if (uiscale is bs.UIScale.SMALL or vr_mode) else 0.7 - color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7) - self.version = bs.NodeActor( - bs.newnode( - 'text', - attrs={ - 'v_attach': 'bottom', - 'h_attach': 'right', - 'h_align': 'right', - 'flatness': 1.0, - 'vr_depth': -10, - 'shadow': 1.0 if vr_mode else 0.5, - 'color': color, - 'scale': scale, - 'position': (-260, 10) if vr_mode else (-10, 10), - 'text': text, - }, - ) - ) - if not app.classic.main_menu_did_initial_transition: - assert self.version.node - bs.animate(self.version.node, 'opacity', {2.3: 0, 3.0: 1.0}) - # Throw in test build info. self.beta_info = self.beta_info_2 = None - if env.test and not (env.demo or env.arcade): - pos = (230, 35) + if env.test: + pos = (230, -5) self.beta_info = bs.NodeActor( bs.newnode( 'text', @@ -292,125 +201,20 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): self._update() # Hopefully this won't hitch but lets space these out anyway. - bui.add_clean_frame_callback(bs.WeakCall(self._start_preloads)) + bs.add_clean_frame_callback(bs.WeakCall(self._start_preloads)) random.seed() - if not (env.demo or env.arcade) and not app.ui_v1.use_toolbars: - self._news = NewsDisplay(self) + # Need to update this for toolbar mode; currenly doesn't fit. + if bool(False): + if not (env.demo or env.arcade): + self._news = NewsDisplay(self) self._attract_mode_timer = bs.Timer( 3.12, self._update_attract_mode, repeat=True ) - # Bring up the last place we were, or start at the main menu otherwise. - with bs.ContextRef.empty(): - from bauiv1lib import specialoffer - - assert bs.app.classic is not None - if bui.app.env.headless: - # UI stuff fails now in headless builds; avoid it. - pass - elif bool(False): - uicontroller = bs.app.ui_v1.controller - assert uicontroller is not None - uicontroller.show_main_menu() - else: - main_menu_location = bs.app.ui_v1.get_main_menu_location() - - # When coming back from a kiosk-mode game, jump to - # the kiosk start screen. - if env.demo or env.arcade: - # pylint: disable=cyclic-import - from bauiv1lib.kiosk import KioskWindow - - bs.app.ui_v1.set_main_menu_window( - KioskWindow().get_root_widget(), - from_window=False, # Disable check here. - ) - # ..or in normal cases go back to the main menu - else: - if main_menu_location == 'Gather': - # pylint: disable=cyclic-import - from bauiv1lib.gather import GatherWindow - - bs.app.ui_v1.set_main_menu_window( - GatherWindow(transition=None).get_root_widget(), - from_window=False, # Disable check here. - ) - elif main_menu_location == 'Watch': - # pylint: disable=cyclic-import - from bauiv1lib.watch import WatchWindow - - bs.app.ui_v1.set_main_menu_window( - WatchWindow(transition=None).get_root_widget(), - from_window=False, # Disable check here. - ) - elif main_menu_location == 'Team Game Select': - # pylint: disable=cyclic-import - from bauiv1lib.playlist.browser import ( - PlaylistBrowserWindow, - ) - - bs.app.ui_v1.set_main_menu_window( - PlaylistBrowserWindow( - sessiontype=bs.DualTeamSession, transition=None - ).get_root_widget(), - from_window=False, # Disable check here. - ) - elif main_menu_location == 'Free-for-All Game Select': - # pylint: disable=cyclic-import - from bauiv1lib.playlist.browser import ( - PlaylistBrowserWindow, - ) - - bs.app.ui_v1.set_main_menu_window( - PlaylistBrowserWindow( - sessiontype=bs.FreeForAllSession, - transition=None, - ).get_root_widget(), - from_window=False, # Disable check here. - ) - elif main_menu_location == 'Coop Select': - # pylint: disable=cyclic-import - from bauiv1lib.coop.browser import CoopBrowserWindow - - bs.app.ui_v1.set_main_menu_window( - CoopBrowserWindow( - transition=None - ).get_root_widget(), - from_window=False, # Disable check here. - ) - elif main_menu_location == 'Benchmarks & Stress Tests': - # pylint: disable=cyclic-import - from bauiv1lib.debug import DebugWindow - - bs.app.ui_v1.set_main_menu_window( - DebugWindow(transition=None).get_root_widget(), - from_window=False, # Disable check here. - ) - else: - # pylint: disable=cyclic-import - from bauiv1lib.mainmenu import MainMenuWindow - - bs.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition=None).get_root_widget(), - from_window=False, # Disable check here. - ) - - # attempt to show any pending offers immediately. - # If that doesn't work, try again in a few seconds - # (we may not have heard back from the server) - # ..if that doesn't work they'll just have to wait - # until the next opportunity. - if not specialoffer.show_offer(): - - def try_again() -> None: - if not specialoffer.show_offer(): - # Try one last time.. - bui.apptimer(2.0, specialoffer.show_offer) - - bui.apptimer(2.0, try_again) + app.classic.invoke_main_menu_ui() app.classic.main_menu_did_initial_transition = True @@ -442,7 +246,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): lang = app.lang.language if lang != self._language: self._language = lang - y = 20 + y = -15 base_scale = 1.1 self._word_actors = [] base_delay = 1.0 @@ -536,7 +340,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): self._make_word( 'B', x - 50, - y - 23 + 0.8 * y_extra, + y - 14 + 0.8 * y_extra, scale=1.3 * base_scale, delay=delay, vr_depth_offset=3, @@ -568,7 +372,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): self._make_word( 'S', x, - y - 25 + 0.8 * y_extra, + y - 15 + 0.8 * y_extra, scale=1.35 * base_scale, delay=delay, vr_depth_offset=14, @@ -676,9 +480,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): ) self._word_actors.append(word_obj) - # Add a bit of stop-motion-y jitter to the logo - # (unless we're in VR mode in which case its best to - # leave things still). + # Add a bit of stop-motion-y jitter to the logo (unless we're in + # VR mode in which case its best to leave things still). if not bs.app.env.vr: cmb: bs.Node | None cmb2: bs.Node | None @@ -763,6 +566,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): vr_depth_offset: float = 0.0, ) -> None: # pylint: disable=too-many-locals + # Temp easter goodness. if custom_texture is None: custom_texture = self._get_custom_logo_tex_name() @@ -794,9 +598,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): self._logo_node = logo.node self._word_actors.append(logo) - # Add a bit of stop-motion-y jitter to the logo - # (unless we're in VR mode in which case its best to - # leave things still). + # Add a bit of stop-motion-y jitter to the logo (unless we're in + # VR mode in which case its best to leave things still). assert logo.node if not bs.app.env.vr: cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) @@ -879,8 +682,8 @@ class NewsDisplay: self._used_phrases: list[str] = [] self._phrase_change_timer: bs.Timer | None = None - # If we're signed in, fetch news immediately. - # Otherwise wait until we are signed in. + # If we're signed in, fetch news immediately. Otherwise wait + # until we are signed in. self._fetch_timer: bs.Timer | None = bs.Timer( 1.0, bs.WeakCall(self._try_fetching_news), repeat=True ) @@ -913,8 +716,8 @@ class NewsDisplay: app = bs.app assert app.classic is not None - # If our news is way out of date, lets re-request it; - # otherwise, rotate our phrase. + # If our news is way out of date, lets re-request it; otherwise, + # rotate our phrase. assert app.classic.main_menu_last_news_fetch_time is not None if time.time() - app.classic.main_menu_last_news_fetch_time > 600.0: self._fetch_news() @@ -981,17 +784,16 @@ class NewsDisplay: self._text.node.text = val def _got_news(self, news: str) -> None: - # Run this stuff in the context of our activity since we - # need to make nodes and stuff.. should fix the serverget - # call so it. + # Run this stuff in the context of our activity since we need to + # make nodes and stuff.. should fix the serverget call so it. activity = self._activity() if activity is None or activity.expired: return with activity.context: self._phrases.clear() - # Show upcoming achievements in non-vr versions - # (currently too hard to read in vr). + # Show upcoming achievements in non-vr versions (currently + # too hard to read in vr). self._used_phrases = (['__ACH__'] if not bs.app.env.vr else []) + [ s for s in news.split('
\n') if s != '' ] diff --git a/src/assets/ba_data/python/batemplatefs/__init__.py b/src/assets/ba_data/python/batemplatefs/__init__.py index 58ad2cd3..379f75e3 100644 --- a/src/assets/ba_data/python/batemplatefs/__init__.py +++ b/src/assets/ba_data/python/batemplatefs/__init__.py @@ -7,9 +7,9 @@ # Package up various private bits (including stuff from our native # module) into a nice clean public API. from _batemplatefs import hello_again_world -from batemplatefs._subsystem import TemplateFsSubsystem +from batemplatefs._appsubsystem import TemplateFsAppSubsystem __all__ = [ - 'TemplateFsSubsystem', + 'TemplateFsAppSubsystem', 'hello_again_world', ] diff --git a/src/assets/ba_data/python/batemplatefs/_subsystem.py b/src/assets/ba_data/python/batemplatefs/_appsubsystem.py similarity index 61% rename from src/assets/ba_data/python/batemplatefs/_subsystem.py rename to src/assets/ba_data/python/batemplatefs/_appsubsystem.py index 2645fbf8..2bc9fe3d 100644 --- a/src/assets/ba_data/python/batemplatefs/_subsystem.py +++ b/src/assets/ba_data/python/batemplatefs/_appsubsystem.py @@ -1,6 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Provides the TemplateFs subsystem.""" +"""Provides the TemplateFs App-Subsystem.""" from __future__ import annotations from typing import TYPE_CHECKING @@ -9,11 +9,11 @@ if TYPE_CHECKING: pass -class TemplateFsSubsystem: +class TemplateFsAppSubsystem: """Subsystem for TemplateFs functionality in the app. - The single shared instance of this app can be accessed at - babase.app.templatefs. Note that it is possible for babase.app.templatefs + The single shared instance of this class can be accessed at + ba*.app.templatefs. Note that it is possible for ba*.app.templatefs to be None if the TemplateFs feature-set is not enabled, and code should handle that case gracefully. """ diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py index 2e3144ba..d3a454d2 100644 --- a/src/assets/ba_data/python/bauiv1/__init__.py +++ b/src/assets/ba_data/python/bauiv1/__init__.py @@ -19,6 +19,7 @@ import logging from efro.util import set_canonical_module_names from babase import ( add_clean_frame_callback, + allows_ticket_sales, app, AppIntent, AppIntentDefault, @@ -113,7 +114,6 @@ from _bauiv1 import ( Mesh, rowwidget, scrollwidget, - set_party_icon_always_visible, set_party_window_open, Sound, Texture, @@ -123,11 +123,18 @@ from _bauiv1 import ( widget, ) from bauiv1._keyboard import Keyboard -from bauiv1._uitypes import Window, uicleanupcheck -from bauiv1._subsystem import UIV1Subsystem +from bauiv1._uitypes import ( + Window, + MainWindowState, + BasicMainWindowState, + uicleanupcheck, + MainWindow, +) +from bauiv1._appsubsystem import UIV1AppSubsystem __all__ = [ 'add_clean_frame_callback', + 'allows_ticket_sales', 'app', 'AppIntent', 'AppIntentDefault', @@ -140,6 +147,7 @@ __all__ = [ 'AppTime', 'apptimer', 'AppTimer', + 'BasicMainWindowState', 'buttonwidget', 'Call', 'fullscreen_control_available', @@ -189,6 +197,8 @@ __all__ = [ 'LoginAdapter', 'LoginInfo', 'Lstr', + 'MainWindow', + 'MainWindowState', 'Mesh', 'native_review_request', 'native_review_request_supported', @@ -212,7 +222,6 @@ __all__ = [ 'scrollwidget', 'set_analytics_screen', 'set_low_level_config_value', - 'set_party_icon_always_visible', 'set_party_window_open', 'set_ui_input_device', 'Sound', @@ -225,7 +234,7 @@ __all__ = [ 'uibounds', 'uicleanupcheck', 'UIScale', - 'UIV1Subsystem', + 'UIV1AppSubsystem', 'unlock_all_input', 'WeakCall', 'widget', diff --git a/src/assets/ba_data/python/bauiv1/_appsubsystem.py b/src/assets/ba_data/python/bauiv1/_appsubsystem.py new file mode 100644 index 00000000..c58b44ba --- /dev/null +++ b/src/assets/ba_data/python/bauiv1/_appsubsystem.py @@ -0,0 +1,400 @@ +# Released under the MIT License. See LICENSE for details. +# +"""User interface related functionality.""" + +from __future__ import annotations + +import logging +import inspect +import weakref +from enum import Enum +from typing import TYPE_CHECKING, override + +from efro.util import empty_weakref +import babase + +import _bauiv1 + +if TYPE_CHECKING: + from typing import Any, Callable + + from bauiv1._uitypes import ( + UICleanupCheck, + Window, + MainWindow, + MainWindowState, + ) + import bauiv1 + + +class UIV1AppSubsystem(babase.AppSubsystem): + """Consolidated UI functionality for the app. + + Category: **App Classes** + + To use this class, access the single instance of it at 'ba.app.ui'. + """ + + class RootUIElement(Enum): + """Stuff provided by the root ui.""" + + MENU_BUTTON = 'menu_button' + SQUAD_BUTTON = 'squad_button' + ACCOUNT_BUTTON = 'account_button' + SETTINGS_BUTTON = 'settings_button' + INBOX_BUTTON = 'inbox_button' + STORE_BUTTON = 'store_button' + INVENTORY_BUTTON = 'inventory_button' + ACHIEVEMENTS_BUTTON = 'achievements_button' + GET_TOKENS_BUTTON = 'get_tokens_button' + TICKETS_METER = 'tickets_meter' + TOKENS_METER = 'tokens_meter' + TROPHY_METER = 'trophy_meter' + LEVEL_METER = 'level_meter' + CHEST_SLOT_1 = 'chest_slot_1' + CHEST_SLOT_2 = 'chest_slot_2' + CHEST_SLOT_3 = 'chest_slot_3' + CHEST_SLOT_4 = 'chest_slot_4' + + def __init__(self) -> None: + from bauiv1._uitypes import MainWindow + + super().__init__() + env = babase.env() + + # We hold only a weak ref to the current main Window; we want it + # to be able to disappear on its own. That being said, we do + # expect MainWindows to keep themselves alive until replaced by + # another MainWindow and we complain if they don't. + self._main_window = empty_weakref(MainWindow) + self._main_window_widget: bauiv1.Widget | None = None + self.main_window_group_id: str | None = None + + self.quit_window: bauiv1.Widget | None = None + + # The following should probably go away or move to classic. + # self._main_menu_location: str | None = None + + # For storing arbitrary class-level state data for Windows or + # other UI related classes. + self.window_states: dict[type, Any] = {} + + uiscalestr = babase.app.config.get('UI Scale', env['ui_scale']) + if uiscalestr == 'auto': + uiscalestr = env['ui_scale'] + + self._uiscale: babase.UIScale + if uiscalestr == 'large': + self._uiscale = babase.UIScale.LARGE + elif uiscalestr == 'medium': + self._uiscale = babase.UIScale.MEDIUM + elif uiscalestr == 'small': + self._uiscale = babase.UIScale.SMALL + else: + logging.error("Invalid UIScale '%s'.", uiscalestr) + self._uiscale = babase.UIScale.MEDIUM + + self.cleanupchecks: list[UICleanupCheck] = [] + self.upkeeptimer: babase.AppTimer | None = None + + self.title_color = (0.72, 0.7, 0.75) + self.heading_color = (0.72, 0.7, 0.75) + self.infotextcolor = (0.7, 0.9, 0.7) + + # Elements in our root UI will call anything here when activated. + self.root_ui_calls: dict[ + UIV1AppSubsystem.RootUIElement, Callable[[], None] + ] = {} + + @property + def available(self) -> bool: + """Can uiv1 currently be used? + + Code that may run in headless mode, before the UI has been spun up, + while other ui systems are active, etc. can check this to avoid + likely erroring. + """ + return _bauiv1.is_available() + + @override + def reset(self) -> None: + from bauiv1._uitypes import MainWindow + + self.root_ui_calls.clear() + self._main_window = empty_weakref(MainWindow) + self._main_window_widget = None + self.main_window_group_id = None + + @property + def uiscale(self) -> babase.UIScale: + """Current ui scale for the app.""" + return self._uiscale + + @override + def on_app_loading(self) -> None: + from bauiv1._uitypes import ui_upkeep + + # IMPORTANT: If tweaking UI stuff, make sure it behaves for small, + # medium, and large UI modes. (doesn't run off screen, etc). + # The overrides below can be used to test with different sizes. + # Generally small is used on phones, medium is used on tablets/tvs, + # and large is on desktop computers or perhaps large tablets. When + # possible, run in windowed mode and resize the window to assure + # this holds true at all aspect ratios. + + # UPDATE: A better way to test this is now by setting the environment + # variable BA_UI_SCALE to "small", "medium", or "large". + # This will affect system UIs not covered by the values below such + # as screen-messages. The below values remain functional, however, + # for cases such as Android where environment variables can't be set + # easily. + + if bool(False): # force-test ui scale + self._uiscale = babase.UIScale.SMALL + with babase.ContextRef.empty(): + babase.pushcall( + lambda: babase.screenmessage( + f'FORCING UISCALE {self._uiscale.name} FOR TESTING', + color=(1, 0, 1), + log=True, + ) + ) + + # Kick off our periodic UI upkeep. + # FIXME: Can probably kill this if we do immediate UI death checks. + self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True) + + def do_main_window_back(self, window: MainWindow) -> None: + """Sets the main menu window automatically from a parent WindowState.""" + + main_window = self._main_window() + back_state = ( + None if main_window is None else main_window.main_window_back_state + ) + if back_state is None: + raise RuntimeError( + f'Main window {main_window} provides no back-state;' + f' cannot use auto-back.' + ) + backwin = back_state.create_window(transition='in_left') + backwin.main_window_back_state = back_state.parent + self.set_main_window(backwin, from_window=window, is_back=True) + + def get_main_window(self) -> bauiv1.MainWindow | None: + """Return main window, if any.""" + return self._main_window() + + def set_main_window( + self, + window: bauiv1.MainWindow, + from_window: bauiv1.MainWindow | None | bool = True, + is_back: bool = False, + group_id: str | None = None, + is_top_level: bool = False, + back_state: MainWindowState | None = None, + ) -> None: + """Set the current 'main' window, replacing any existing. + + If 'from_window' is passed as a bauiv1.Widget or bauiv1.Window + or None, a warning will be issued if it that value does not + match the current main window. This can help identify flawed + code that can lead to bad UI states. A value of False will + disable the check, which is necessary in some cases when the + current main window is not known. + + When navigating somewhere from a cancel or back-button, pass + is_back=True; this will prevent the new main window from itself + being registered as a new location on the stack that can be + returned to. + + If a 'group_id' string is provided and the window being replaced + has the same group-id, the WindowState stack is left unchanged, + effectively replacing the previous window with the new one in + the stack. This can be useful in cases where tab-bar-like UIs + allow flipping between sibling windows with the back button + always leading to a shared parent. + """ + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + from bauiv1._uitypes import MainWindow + + from_window_widget: bauiv1.Widget | None + + # We used to accept Widgets but now want MainWindows. + assert isinstance(window, MainWindow) + window_weakref = weakref.ref(window) + window_widget = window.get_root_widget() + + if isinstance(from_window, MainWindow): + from_window_widget = from_window.get_root_widget() + else: + from_window_widget = None + + existing = self._main_window_widget + + try: + if isinstance(from_window, bool): + # For default val True we warn that the arg wasn't + # passed. False can be explicitly passed to disable this + # check. + if from_window is True: + caller_frame = inspect.stack()[1] + caller_filename = caller_frame.filename + caller_line_number = caller_frame.lineno + logging.warning( + 'set_main_window() should be passed a' + " 'from_window' value to help ensure proper UI behavior" + ' (%s line %i).', + caller_filename, + caller_line_number, + ) + else: + # For everything else, warn if what they passed wasn't + # the previous main menu widget. + if from_window_widget is not existing: + caller_frame = inspect.stack()[1] + caller_filename = caller_frame.filename + caller_line_number = caller_frame.lineno + logging.warning( + "set_main_window() was passed 'from_window' %s" + ' but existing main-menu-window is %s. (%s line %i).', + from_window_widget, + existing, + caller_filename, + caller_line_number, + ) + except Exception: + # Prevent any bugs in these checks from causing problems. + logging.exception('Error checking from_window') + + # Once the above code leads to us fixing all leftover window + # bugs at the source, we can kill the code below. + + # Let's grab the location where we were called from to report if + # we have to force-kill the existing window (which normally + # should not happen). + frameline = None + try: + frame = inspect.currentframe() + if frame is not None: + frame = frame.f_back + if frame is not None: + frameinfo = inspect.getframeinfo(frame) + frameline = f'{frameinfo.filename} {frameinfo.lineno}' + except Exception: + logging.exception('Error calcing line for set_main_window') + + # NOTE: disabling this for now since hopefully our new system + # will be bulletproof enough to avoid this. Can turn it back on + # if that's not the case. + + # With our legacy main-menu system, the caller is responsible + # for clearing out the old main menu window when assigning the + # new. However there are corner cases where that doesn't happen + # and we get old windows stuck under the new main one. So let's + # guard against that. However, we can't simply delete the + # existing main window when a new one is assigned because the + # user may transition the old out *after* the assignment. Sigh. + # So, as a happy medium, let's check in on the old after a short + # bit of time and kill it if its still alive. That will be a bit + # ugly on screen but at least should un-break things. + def _delay_kill() -> None: + import time + + if existing: + print( + f'Killing old main_menu_window' + f' when called at: {frameline} t={time.time():.3f}' + ) + existing.delete() + + if bool(False): + babase.apptimer(1.0, _delay_kill) + + if is_back: + pass + else: + # When navigating forward, generate a back-window-state from + # the outgoing window. + + # Exception is when we were passed a group and it matches + # the existing group; in that case we just keep the existing + # back-state. + if group_id is not None and group_id == self.main_window_group_id: + assert not is_top_level + print(f'GOT GROUP ID MATCH {group_id}; KEEPING BACK STATE.') + oldwin = self._main_window() + if oldwin is None: + # We currenty only hold weak refs to windows so + # that they are free to die on their own, but we + # expect the main menu window to keep itself + # alive as long as its the main one. Holler if + # that seems to not be happening. + logging.warning( + 'set_main_window: no existing MainWindow found' + ' (and is_top_level is False); should not happen.' + ' a MainWindow should keep itself alive as long' + ' as it is main.' + ) + window.main_window_back_state = None + else: + window.main_window_back_state = ( + oldwin.main_window_back_state + ) + else: + if is_top_level: + # Top level windows don't have or expect anywhere to go + # back to. + # self._main_window_back_state = None + window.main_window_back_state = None + elif back_state is not None: + window.main_window_back_state = back_state + else: + oldwin = self._main_window() + if oldwin is None: + # We currenty only hold weak refs to windows so + # that they are free to die on their own, but we + # expect the main menu window to keep itself + # alive as long as its the main one. Holler if + # that seems to not be happening. + logging.warning( + 'set_main_window: No old MainWindow found' + ' and is_top_level is False;' + ' this should not happen.' + ) + window.main_window_back_state = None + else: + oldwinstate = oldwin.get_main_window_state() + + # Store our previous back state on this new one. + oldwinstate.parent = oldwin.main_window_back_state + window.main_window_back_state = oldwinstate + + self._main_window = window_weakref + self._main_window_widget = window_widget + self.main_window_group_id = group_id + + def has_main_window(self) -> bool: + """Return whether a main menu window is present.""" + return bool(self._main_window_widget) + + def clear_main_window(self) -> None: + """Clear any existing main window.""" + from bauiv1._uitypes import MainWindow + + main_window = self._main_window() + if main_window: + main_window.main_window_close() + else: + # Fallback; if we have a widget but no window, nuke the widget. + if self._main_window_widget: + logging.error( + 'Have _main_window_widget but no main_window' + ' on clear_main_window; unexpected.' + ) + self._main_window_widget.delete() + + self._main_window = empty_weakref(MainWindow) + self._main_window_widget = None + self.main_window_group_id = None diff --git a/src/assets/ba_data/python/bauiv1/_hooks.py b/src/assets/ba_data/python/bauiv1/_hooks.py index b26b9fd3..979f43fe 100644 --- a/src/assets/ba_data/python/bauiv1/_hooks.py +++ b/src/assets/ba_data/python/bauiv1/_hooks.py @@ -15,49 +15,130 @@ if TYPE_CHECKING: from typing import Sequence import babase - - -def ticket_icon_press() -> None: - from babase import app - - if app.classic is None: - logging.exception('Classic not present.') - return - - app.classic.ticket_icon_press() - - -def trophy_icon_press() -> None: - print('TROPHY ICON PRESSED') - - -def level_icon_press() -> None: - print('LEVEL ICON PRESSED') - - -def coin_icon_press() -> None: - print('COIN ICON PRESSED') + import bauiv1 def empty_call() -> None: pass -def back_button_press() -> None: - _bauiv1.back_press() +def _root_ui_button_press( + rootuitype: bauiv1.UIV1AppSubsystem.RootUIElement, +) -> None: + import babase + + ui = babase.app.ui_v1 + call = ui.root_ui_calls.get(rootuitype) + if call is not None: + call() -def friends_button_press() -> None: - print('FRIEND BUTTON PRESSED!') +def root_ui_account_button_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.ACCOUNT_BUTTON) -def party_icon_activate(origin: Sequence[float]) -> None: - from babase import app +def root_ui_inbox_button_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem - if app.classic is not None: - app.classic.party_icon_activate(origin) - else: - logging.warning('party_icon_activate: no classic.') + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.INBOX_BUTTON) + + +def root_ui_settings_button_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.SETTINGS_BUTTON) + + +def root_ui_achievements_button_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.ACHIEVEMENTS_BUTTON) + + +def root_ui_store_button_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.STORE_BUTTON) + + +def root_ui_chest_slot_1_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.CHEST_SLOT_1) + + +def root_ui_chest_slot_2_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.CHEST_SLOT_2) + + +def root_ui_chest_slot_3_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.CHEST_SLOT_3) + + +def root_ui_chest_slot_4_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.CHEST_SLOT_4) + + +def root_ui_inventory_button_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.INVENTORY_BUTTON) + + +def root_ui_ticket_icon_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.TICKETS_METER) + + +def root_ui_get_tokens_button_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.GET_TOKENS_BUTTON) + + +def root_ui_tokens_meter_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.TOKENS_METER) + + +def root_ui_trophy_meter_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.TROPHY_METER) + + +def root_ui_level_icon_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.LEVEL_METER) + + +def root_ui_menu_button_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.MENU_BUTTON) + + +def root_ui_back_button_press() -> None: + # Native layer handles this directly. (technically we could wire + # this up to not even come through Python). + _bauiv1.root_ui_back_press() + + +def root_ui_squad_button_press() -> None: + from bauiv1._appsubsystem import UIV1AppSubsystem + + _root_ui_button_press(UIV1AppSubsystem.RootUIElement.SQUAD_BUTTON) def quit_window(quit_type: babase.QuitType) -> None: diff --git a/src/assets/ba_data/python/bauiv1/_subsystem.py b/src/assets/ba_data/python/bauiv1/_subsystem.py deleted file mode 100644 index c9bd7f50..00000000 --- a/src/assets/ba_data/python/bauiv1/_subsystem.py +++ /dev/null @@ -1,250 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""User interface related functionality.""" - -from __future__ import annotations - -import logging -import inspect -from typing import TYPE_CHECKING, override - -import babase - -import _bauiv1 - -if TYPE_CHECKING: - from typing import Any, Callable - - from bauiv1._uitypes import UICleanupCheck, UIController - import bauiv1 - - -class UIV1Subsystem(babase.AppSubsystem): - """Consolidated UI functionality for the app. - - Category: **App Classes** - - To use this class, access the single instance of it at 'ba.app.ui'. - """ - - def __init__(self) -> None: - super().__init__() - env = babase.env() - - self.controller: UIController | None = None - - self._main_menu_window: bauiv1.Widget | None = None - self._main_menu_location: str | None = None - self.quit_window: bauiv1.Widget | None = None - - # From classic. - self.main_menu_resume_callbacks: list = [] # Can probably go away. - - self._uiscale: babase.UIScale - - interfacetype = babase.app.config.get('UI Scale', env['ui_scale']) - if interfacetype == 'auto': - interfacetype = env['ui_scale'] - - if interfacetype == 'large': - self._uiscale = babase.UIScale.LARGE - elif interfacetype == 'medium': - self._uiscale = babase.UIScale.MEDIUM - elif interfacetype == 'small': - self._uiscale = babase.UIScale.SMALL - else: - raise RuntimeError(f'Invalid UIScale value: {interfacetype}') - - self.window_states: dict[type, Any] = {} # FIXME: Kill this. - self.main_menu_selection: str | None = None # FIXME: Kill this. - self.have_party_queue_window = False - self.cleanupchecks: list[UICleanupCheck] = [] - self.upkeeptimer: babase.AppTimer | None = None - self.use_toolbars = _bauiv1.toolbar_test() - - self.title_color = (0.72, 0.7, 0.75) - self.heading_color = (0.72, 0.7, 0.75) - self.infotextcolor = (0.7, 0.9, 0.7) - - # Switch our overall game selection UI flow between Play and - # Private-party playlist selection modes; should do this in - # a more elegant way once we revamp high level UI stuff a bit. - self.selecting_private_party_playlist: bool = False - - @property - def available(self) -> bool: - """Can uiv1 currently be used? - - Code that may run in headless mode, before the UI has been spun up, - while other ui systems are active, etc. can check this to avoid - likely erroring. - """ - return _bauiv1.is_available() - - @property - def uiscale(self) -> babase.UIScale: - """Current ui scale for the app.""" - return self._uiscale - - @override - def on_app_loading(self) -> None: - from bauiv1._uitypes import UIController, ui_upkeep - - # IMPORTANT: If tweaking UI stuff, make sure it behaves for small, - # medium, and large UI modes. (doesn't run off screen, etc). - # The overrides below can be used to test with different sizes. - # Generally small is used on phones, medium is used on tablets/tvs, - # and large is on desktop computers or perhaps large tablets. When - # possible, run in windowed mode and resize the window to assure - # this holds true at all aspect ratios. - - # UPDATE: A better way to test this is now by setting the environment - # variable BA_UI_SCALE to "small", "medium", or "large". - # This will affect system UIs not covered by the values below such - # as screen-messages. The below values remain functional, however, - # for cases such as Android where environment variables can't be set - # easily. - - if bool(False): # force-test ui scale - self._uiscale = babase.UIScale.SMALL - with babase.ContextRef.empty(): - babase.pushcall( - lambda: babase.screenmessage( - f'FORCING UISCALE {self._uiscale.name} FOR TESTING', - color=(1, 0, 1), - log=True, - ) - ) - - self.controller = UIController() - - # Kick off our periodic UI upkeep. - # FIXME: Can probably kill this if we do immediate UI death checks. - self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True) - - def set_main_menu_window( - self, - window: bauiv1.Widget, - from_window: bauiv1.Widget | None | bool = True, - ) -> None: - """Set the current 'main' window, replacing any existing. - - If 'from_window' is passed as a bauiv1.Widget or None, a warning - will be issued if it that value does not match the current main - window. This can help clean up flawed code that can lead to bad - UI states. A value of False will disable the check. - """ - - existing = self._main_menu_window - - try: - if isinstance(from_window, bool): - # For default val True we warn that the arg wasn't - # passed. False can be explicitly passed to disable this - # check. - if from_window is True: - caller_frame = inspect.stack()[1] - caller_filename = caller_frame.filename - caller_line_number = caller_frame.lineno - logging.warning( - 'set_main_menu_window() should be passed a' - " 'from_window' value to help ensure proper UI behavior" - ' (%s line %i).', - caller_filename, - caller_line_number, - ) - else: - # For everything else, warn if what they passed wasn't - # the previous main menu widget. - if from_window is not existing: - caller_frame = inspect.stack()[1] - caller_filename = caller_frame.filename - caller_line_number = caller_frame.lineno - logging.warning( - "set_main_menu_window() was passed 'from_window' %s" - ' but existing main-menu-window is %s. (%s line %i).', - from_window, - existing, - caller_filename, - caller_line_number, - ) - except Exception: - # Prevent any bugs in these checks from causing problems. - logging.exception('Error checking from_window') - - # Once the above code leads to us fixing all leftover window bugs - # at the source, we can kill the code below. - - # Let's grab the location where we were called from to report - # if we have to force-kill the existing window (which normally - # should not happen). - frameline = None - try: - frame = inspect.currentframe() - if frame is not None: - frame = frame.f_back - if frame is not None: - frameinfo = inspect.getframeinfo(frame) - frameline = f'{frameinfo.filename} {frameinfo.lineno}' - except Exception: - logging.exception('Error calcing line for set_main_menu_window') - - # With our legacy main-menu system, the caller is responsible for - # clearing out the old main menu window when assigning the new. - # However there are corner cases where that doesn't happen and we get - # old windows stuck under the new main one. So let's guard against - # that. However, we can't simply delete the existing main window when - # a new one is assigned because the user may transition the old out - # *after* the assignment. Sigh. So, as a happy medium, let's check in - # on the old after a short bit of time and kill it if its still alive. - # That will be a bit ugly on screen but at least should un-break - # things. - def _delay_kill() -> None: - import time - - if existing: - print( - f'Killing old main_menu_window' - f' when called at: {frameline} t={time.time():.3f}' - ) - existing.delete() - - babase.apptimer(1.0, _delay_kill) - self._main_menu_window = window - - def clear_main_menu_window(self, transition: str | None = None) -> None: - """Clear any existing 'main' window with the provided transition.""" - assert transition is None or not transition.endswith('_in') - if self._main_menu_window: - if ( - transition is not None - and not self._main_menu_window.transitioning_out - ): - _bauiv1.containerwidget( - edit=self._main_menu_window, transition=transition - ) - else: - self._main_menu_window.delete() - self._main_menu_window = None - - def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: - """(internal)""" - - # If there's no main menu up, just call immediately. - if not self.has_main_menu_window(): - with babase.ContextRef.empty(): - call() - else: - self.main_menu_resume_callbacks.append(call) - - def has_main_menu_window(self) -> bool: - """Return whether a main menu window is present.""" - return bool(self._main_menu_window) - - def set_main_menu_location(self, location: str) -> None: - """Set the location represented by the current main menu window.""" - self._main_menu_location = location - - def get_main_menu_location(self) -> str | None: - """Return the current named main menu location, if any.""" - return self._main_menu_location diff --git a/src/assets/ba_data/python/bauiv1/_uitypes.py b/src/assets/ba_data/python/bauiv1/_uitypes.py index 6431a554..0106213c 100644 --- a/src/assets/ba_data/python/bauiv1/_uitypes.py +++ b/src/assets/ba_data/python/bauiv1/_uitypes.py @@ -6,6 +6,7 @@ from __future__ import annotations import os import weakref +import logging from dataclasses import dataclass from typing import TYPE_CHECKING, override @@ -14,7 +15,7 @@ import babase import _bauiv1 if TYPE_CHECKING: - from typing import Any, Type + from typing import Any, Type, Literal, Callable import bauiv1 @@ -27,6 +28,9 @@ class Window: """A basic window. Category: User Interface Classes + + Essentially wraps a ContainerWidget with some higher level + functionality. """ def __init__(self, root_widget: bauiv1.Widget, cleanupcheck: bool = True): @@ -41,6 +45,192 @@ class Window: return self._root_widget +class MainWindow(Window): + """A special window that can be used as a main window.""" + + def __init__( + self, + root_widget: bauiv1.Widget, + transition: str | None, + origin_widget: bauiv1.Widget | None, + cleanupcheck: bool = True, + ): + """Create a MainWindow given a root widget and transition info. + + Automatically handles in and out transitions on the provided widget, + so there is no need to set transitions when creating it. + """ + # TODO - move to MainWindow + # A back-state supplied by the ui system. + self.main_window_back_state: MainWindowState | None = None + + self._main_window_transition = transition + self._main_window_origin_widget = origin_widget + super().__init__(root_widget, cleanupcheck) + + scale_origin: tuple[float, float] | None + if origin_widget is not None: + self._main_window_transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._main_window_transition_out = 'out_right' + scale_origin = None + _bauiv1.containerwidget( + edit=root_widget, + transition=transition, + scale_origin_stack_offset=scale_origin, + ) + + def main_window_close(self) -> None: + """Get window transitioning out if still alive.""" + + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + + # Transition ourself out. + try: + self.on_main_window_close() + except Exception: + logging.exception('Error in on_main_window_close() for %s.', self) + + _bauiv1.containerwidget( + edit=self._root_widget, transition=self._main_window_transition_out + ) + + def can_change_main_window(self) -> bool: + """Is this MainWindow allowed to change the global main window? + + It is a good idea to make sure this is True before calling + either main_window_back() or main_window_replace(). This + prevents fluke UI breakage such as multiple simultaneous events + causing a MainWindow to spawn multiple replacements for itself. + """ + # We are allowed to change main windows if we are the current one + # AND our underlying widget is still alive and not transitioning out. + return ( + babase.app.ui_v1.get_main_window() is self + and bool(self._root_widget) + and not self._root_widget.transitioning_out + ) + + def main_window_back(self) -> None: + """Move back in the main window stack.""" + + # Users should always check can_change_main_window() before + # calling us. Error if it seems they did not. + if not self.can_change_main_window(): + raise RuntimeError( + 'main_window_back() should only be called' + ' if can_change_main_window() returns True' + ' (it currently is False).' + ) + + # Get the 'back' window coming in. + babase.app.ui_v1.do_main_window_back(self) + + self.main_window_close() + + def main_window_replace( + self, new_window: MainWindow, group_id: str | None = None + ) -> None: + """Replace ourself with a new MainWindow.""" + + # Users should always check can_change_main_window() before + # creating new MainWindows and passing them in here. Error if it + # seems they did not. + if not self.can_change_main_window(): + raise RuntimeError( + 'main_window_replace() should only be called' + ' if can_change_main_window() returns True' + ' (it currently is False).' + ) + + # If we're navigating within a group, we want it to look like we're + # backing out of the old one and going into the new one. + if ( + group_id is not None + and babase.app.ui_v1.main_window_group_id == group_id + ): + transition = self._main_window_transition_out + else: + # Otherwise just shove the old out the left to give the feel + # that we're adding to the nav stack. + transition = 'out_left' + + # Transition ourself out. + try: + self.on_main_window_close() + except Exception: + logging.exception('Error in on_main_window_close() for %s.', self) + + _bauiv1.containerwidget(edit=self._root_widget, transition=transition) + babase.app.ui_v1.set_main_window( + new_window, from_window=self, group_id=group_id + ) + + def on_main_window_close(self) -> None: + """Called before transitioning out a main window. + + A good opportunity to save window state/etc. + """ + + def get_main_window_state(self) -> MainWindowState: + """Return a WindowState to recreate this window, if supported.""" + # TODO - change to NotImplementedError when moved to MainWindow. + raise RuntimeError('FIXME NOT IMPLEMENTED') + + +class MainWindowState: + """Persistent state for a specific main-window and its ancestors. + + This allows MainWindows to be automatically recreated for back-button + purposes, when switching app-modes, etc. + """ + + def __init__(self, parent: MainWindowState | None = None) -> None: + # The window that back/cancel navigation should take us to. + self.parent = parent + + def create_window( + self, + transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, + origin_widget: bauiv1.Widget | None = None, + ) -> MainWindow: + """Create a window based on this state. + + WindowState child classes should override this to recreate their + particular type of window. + """ + raise NotImplementedError() + + +class BasicMainWindowState(MainWindowState): + """A basic MainWindowState holding a lambda to recreate a MainWindow.""" + + def __init__( + self, + create_call: Callable[ + [ + Literal['in_right', 'in_left', 'in_scale'] | None, + bauiv1.Widget | None, + ], + bauiv1.MainWindow, + ], + ) -> None: + super().__init__() + self.create_call = create_call + + @override + def create_window( + self, + transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, + origin_widget: bauiv1.Widget | None = None, + ) -> bauiv1.MainWindow: + return self.create_call(transition, origin_widget) + + @dataclass class UICleanupCheck: """Holds info about a uicleanupcheck target.""" @@ -50,127 +240,8 @@ class UICleanupCheck: widget_death_time: float | None -class UILocation: - """Defines a specific 'place' in the UI the user can navigate to. - - Category: User Interface Classes - """ - - def __init__(self) -> None: - pass - - def save_state(self) -> None: - """Serialize this instance's state to a dict.""" - - def restore_state(self) -> None: - """Restore this instance's state from a dict.""" - - def push_location(self, location: str) -> None: - """Push a new location to the stack and transition to it.""" - - -class UILocationWindow(UILocation): - """A UILocation consisting of a single root window widget. - - Category: User Interface Classes - """ - - def __init__(self) -> None: - super().__init__() - self._root_widget: bauiv1.Widget | None = None - - def get_root_widget(self) -> bauiv1.Widget: - """Return the root widget for this window.""" - assert self._root_widget is not None - return self._root_widget - - -class UIEntry: - """State for a UILocation on the stack.""" - - def __init__(self, name: str, controller: UIController): - self._name = name - self._state = None - self._args = None - self._instance: UILocation | None = None - self._controller = weakref.ref(controller) - - def create(self) -> None: - """Create an instance of our UI.""" - cls = self._get_class() - self._instance = cls() - - def destroy(self) -> None: - """Transition out our UI if it exists.""" - if self._instance is None: - return - print('WOULD TRANSITION OUT', self._name) - - def _get_class(self) -> Type[UILocation]: - """Returns the UI class our name points to.""" - # pylint: disable=cyclic-import - - # TEMP HARD CODED - WILL REPLACE THIS WITH BA_META LOOKUPS. - if self._name == 'mainmenu': - # Shut pylint up. - if bool(False): - return UILocation - raise RuntimeError('FIXME UNIMPLEMENTED') - # from bauiv1lib import mainmenu - # return cast(Type[UILocation], mainmenu.MainMenuWindow) - - raise ValueError('unknown ui class ' + str(self._name)) - - -class UIController: - """Wrangles bauiv1.UILocations. - - Category: User Interface Classes - """ - - def __init__(self) -> None: - # FIXME: document why we have separate stacks for game and menu... - self._main_stack_game: list[UIEntry] = [] - self._main_stack_menu: list[UIEntry] = [] - - # This points at either the game or menu stack. - self._main_stack: list[UIEntry] | None = None - - # There's only one of these since we don't need to preserve its state - # between sessions. - self._dialog_stack: list[UIEntry] = [] - - def show_main_menu(self, in_game: bool = True) -> None: - """Show the main menu, clearing other UIs from location stacks.""" - self._main_stack = [] - self._dialog_stack = [] - self._main_stack = ( - self._main_stack_game if in_game else self._main_stack_menu - ) - self._main_stack.append(UIEntry('mainmenu', self)) - self._update_ui() - - def _update_ui(self) -> None: - """Instantiate the topmost ui in our stacks.""" - - # First tell any existing UIs to get outta here. - for stack in (self._dialog_stack, self._main_stack): - assert stack is not None - for entry in stack: - entry.destroy() - - # Now create the topmost one if there is one. - entrynew = ( - self._dialog_stack[-1] - if self._dialog_stack - else self._main_stack[-1] if self._main_stack else None - ) - if entrynew is not None: - entrynew.create() - - def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None: - """Add a check to ensure a widget-owning object gets cleaned up properly. + """Checks to ensure a widget-owning object gets cleaned up properly. Category: User Interface Functions @@ -233,8 +304,10 @@ def ui_upkeep() -> None: 'WARNING:', obj, 'is still alive 5 second after its widget died;' - ' you might have a memory leak. See efro.debug module' - ' for tools to help debug this.', + ' you might have a memory leak. Look for circular' + ' references or outside things referencing your window' + ' instance. See efro.debug module' + ' for tools that can help debug this sort of thing.', ) else: remainingchecks.append(check) diff --git a/src/assets/ba_data/python/bauiv1lib/account/__init__.py b/src/assets/ba_data/python/bauiv1lib/account/__init__.py index 5ce26500..d34e9ef5 100644 --- a/src/assets/ba_data/python/bauiv1lib/account/__init__.py +++ b/src/assets/ba_data/python/bauiv1lib/account/__init__.py @@ -7,32 +7,15 @@ from __future__ import annotations import bauiv1 as bui -def show_sign_in_prompt(account_type: str | None = None) -> None: +def show_sign_in_prompt() -> None: """Bring up a prompt telling the user they must sign in.""" from bauiv1lib.confirm import ConfirmWindow - from bauiv1lib.account import settings + from bauiv1lib.account.settings import AccountSettingsWindow - if account_type == 'Google Play': - - def _do_sign_in() -> None: - plus = bui.app.plus - assert plus is not None - plus.sign_in_v1('Google Play') - - ConfirmWindow( - bui.Lstr(resource='notSignedInGooglePlayErrorText'), - _do_sign_in, - ok_text=bui.Lstr(resource='accountSettingsWindow.signInText'), - width=460, - height=130, - ) - else: - ConfirmWindow( - bui.Lstr(resource='notSignedInErrorText'), - lambda: settings.AccountSettingsWindow( - modal=True, close_once_signed_in=True - ), - ok_text=bui.Lstr(resource='accountSettingsWindow.signInText'), - width=460, - height=130, - ) + ConfirmWindow( + bui.Lstr(resource='notSignedInErrorText'), + lambda: AccountSettingsWindow(modal=True, close_once_signed_in=True), + ok_text=bui.Lstr(resource='accountSettingsWindow.signInText'), + width=460, + height=130, + ) diff --git a/src/assets/ba_data/python/bauiv1lib/account/settings.py b/src/assets/ba_data/python/bauiv1lib/account/settings.py index 9476f326..9ba34ed1 100644 --- a/src/assets/ba_data/python/bauiv1lib/account/settings.py +++ b/src/assets/ba_data/python/bauiv1lib/account/settings.py @@ -7,6 +7,7 @@ from __future__ import annotations import time import logging +from typing import override from bacommon.cloud import WebLocation from bacommon.login import LoginType @@ -20,12 +21,12 @@ import bauiv1 as bui FORCE_ENABLE_V1_LINKING = False -class AccountSettingsWindow(bui.Window): +class AccountSettingsWindow(bui.MainWindow): """Window for account related functionality.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', modal: bool = False, origin_widget: bui.Widget | None = None, close_once_signed_in: bool = False, @@ -46,16 +47,6 @@ class AccountSettingsWindow(bui.Window): self._explicitly_signed_out_of_gpgs = False - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - self._r = 'accountSettingsWindow' self._modal = modal self._needs_refresh = False @@ -71,10 +62,10 @@ class AccountSettingsWindow(bui.Window): assert app.classic is not None uiscale = app.ui_v1.uiscale - self._width = 860 if uiscale is bui.UIScale.SMALL else 660 - x_offs = 100 if uiscale is bui.UIScale.SMALL else 0 + self._width = 850 if uiscale is bui.UIScale.SMALL else 660 + x_offs = 70 if uiscale is bui.UIScale.SMALL else 0 self._height = ( - 390 + 380 if uiscale is bui.UIScale.SMALL else 430 if uiscale is bui.UIScale.MEDIUM else 490 ) @@ -107,23 +98,28 @@ class AccountSettingsWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, + # transition=transition, + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), scale=( - 2.09 + 2.07 if uiscale is bui.UIScale.SMALL else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( - (0, -19) if uiscale is bui.UIScale.SMALL else (0, 0) + (0, 8) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) - if uiscale is bui.UIScale.SMALL and app.ui_v1.use_toolbars: + if uiscale is bui.UIScale.SMALL: self._back_button = None bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._back + edit=self._root_widget, on_cancel_call=self.main_window_back ) else: self._back_button = btn = bui.buttonwidget( @@ -137,7 +133,7 @@ class AccountSettingsWindow(bui.Window): resource='doneText' if self._modal else 'backText' ), button_type='regular' if self._modal else 'back', - on_activate_call=self._back, + on_activate_call=self.main_window_back, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) if not self._modal: @@ -148,12 +144,15 @@ class AccountSettingsWindow(bui.Window): label=bui.charstr(bui.SpecialChar.BACK), ) + titleyoffs = -12 if uiscale is bui.UIScale.SMALL else 0 + titlescale = 0.6 if uiscale is bui.UIScale.SMALL else 1.0 bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height - 41), + position=(self._width * 0.5, self._height - 41 + titleyoffs), size=(0, 0), text=bui.Lstr(resource=f'{self._r}.titleText'), color=app.ui_v1.title_color, + scale=titlescale, maxwidth=self._width - 340, h_align='center', v_align='center', @@ -175,13 +174,27 @@ class AccountSettingsWindow(bui.Window): self._refresh() self._restore_state() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + def _update(self) -> None: plus = bui.app.plus assert plus is not None # If they want us to close once we're signed in, do so. if self._close_once_signed_in and self._v1_signed_in: - self._back() + self.main_window_back() return # Hmm should update this to use get_account_state_num. @@ -215,7 +228,6 @@ class AccountSettingsWindow(bui.Window): # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=cyclic-import - from bauiv1lib import confirm plus = bui.app.plus assert plus is not None @@ -316,10 +328,6 @@ class AccountSettingsWindow(bui.Window): show_game_service_button = game_center_active game_service_button_space = 60.0 - # Phasing this out. - show_what_is_v2 = False - # show_what_is_v2 = self._v1_signed_in and v1_account_type == 'V2' - # Phasing this out (for V2 accounts at least). show_linked_accounts_text = ( self._v1_signed_in and v1_account_type != 'V2' @@ -328,36 +336,41 @@ class AccountSettingsWindow(bui.Window): # Always show achievements except in the game-center case where # its unified UI covers them. - show_achievements_button = self._v1_signed_in and not game_center_active + # show_achievements_button = + # self._v1_signed_in and not game_center_active + + # Update: No longer showing this since its visible on main + # toolbar. + show_achievements_button = False achievements_button_space = 60.0 - show_achievements_text = ( - self._v1_signed_in and not show_achievements_button - ) + # show_achievements_text = ( + # self._v1_signed_in and not show_achievements_button + # ) + + # Update: No longer showing this since its visible on main + # toolbar. + show_achievements_text = False achievements_text_space = 27.0 show_leaderboards_button = self._v1_signed_in and gpgs_active leaderboards_button_space = 60.0 - show_campaign_progress = self._v1_signed_in + # Update: No longer showing this; trying to get progress type + # stuff out of the account panel. + # show_campaign_progress = self._v1_signed_in + show_campaign_progress = False campaign_progress_space = 27.0 - show_tickets = self._v1_signed_in + # show_tickets = self._v1_signed_in + show_tickets = False tickets_space = 27.0 - show_reset_progress_button = False - reset_progress_button_space = 70.0 - - show_manage_v2_account_button = primary_v2_account is not None - manage_v2_account_button_space = 100.0 + show_manage_account_button = primary_v2_account is not None + manage_account_button_space = 70.0 show_delete_account_button = primary_v2_account is not None - delete_account_button_space = 80.0 - - show_player_profiles_button = self._v1_signed_in - player_profiles_button_space = ( - 70.0 if show_manage_v2_account_button else 100.0 - ) + delete_account_button_space = 70.0 show_link_accounts_button = self._v1_signed_in and ( primary_v2_account is None or FORCE_ENABLE_V1_LINKING @@ -376,7 +389,7 @@ class AccountSettingsWindow(bui.Window): show_sign_out_button = primary_v2_account is not None or ( self._v1_signed_in and v1_account_type == 'Local' ) - sign_out_button_space = 80.0 + sign_out_button_space = 70.0 # We can show cancel if we're either waiting on an adapter to # provide us with v2 credentials or waiting for those @@ -419,12 +432,8 @@ class AccountSettingsWindow(bui.Window): self._sub_height += tickets_space if show_sign_in_benefits: self._sub_height += sign_in_benefits_space - if show_reset_progress_button: - self._sub_height += reset_progress_button_space - if show_manage_v2_account_button: - self._sub_height += manage_v2_account_button_space - if show_player_profiles_button: - self._sub_height += player_profiles_button_space + if show_manage_account_button: + self._sub_height += manage_account_button_space if show_link_accounts_button: self._sub_height += link_accounts_button_space if show_unlink_accounts_button: @@ -452,8 +461,6 @@ class AccountSettingsWindow(bui.Window): v = self._sub_height - 10.0 assert bui.app.classic is not None - self._account_name_what_is_text: bui.Widget | None - self._account_name_what_is_y = 0.0 self._account_name_text: bui.Widget | None if show_signed_in_as: v -= signed_in_as_space * 0.2 @@ -485,32 +492,6 @@ class AccountSettingsWindow(bui.Window): v_align='center', ) - if show_what_is_v2: - self._account_name_what_is_y = v - 23.0 - self._account_name_what_is_text = bui.textwidget( - parent=self._subcontainer, - position=(0.0, self._account_name_what_is_y), - size=(220.0, 60), - text=bui.Lstr( - value='${WHAT} -->', - subs=[('${WHAT}', bui.Lstr(resource='whatIsThisText'))], - ), - scale=0.6, - color=(0.3, 0.7, 0.05), - maxwidth=130.0, - h_align='right', - v_align='center', - autoselect=True, - selectable=True, - on_activate_call=show_what_is_v2_page, - click_activate=True, - glow_type='uniform', - ) - if first_selectable is None: - first_selectable = self._account_name_what_is_text - else: - self._account_name_what_is_text = None - self._refresh_account_name_text() v -= signed_in_as_space * 0.4 @@ -565,7 +546,6 @@ class AccountSettingsWindow(bui.Window): else: self._account_name_text = None - self._account_name_what_is_text = None if self._back_button is None: bbtn = bui.get_special_widget('back_button') @@ -641,11 +621,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn) bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None @@ -683,11 +661,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn) bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None @@ -752,11 +728,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn) bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None @@ -821,21 +795,19 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn) bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None - if show_manage_v2_account_button: + if show_manage_account_button: button_width = 300 - v -= manage_v2_account_button_space - self._manage_v2_button = btn = bui.buttonwidget( + v -= manage_account_button_space + self._manage_button = btn = bui.buttonwidget( parent=self._subcontainer, - position=((self._sub_width - button_width) * 0.5, v + 30), + position=((self._sub_width - button_width) * 0.5, v), autoselect=True, size=(button_width, 60), label=bui.Lstr(resource=f'{self._r}.manageAccountText'), @@ -846,35 +818,10 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) - bui.widget(edit=btn, left_widget=bbtn) - - if show_player_profiles_button: - button_width = 300 - v -= player_profiles_button_space - self._player_profiles_button = btn = bui.buttonwidget( - parent=self._subcontainer, - position=((self._sub_width - button_width) * 0.5, v + 30), - autoselect=True, - size=(button_width, 60), - label=bui.Lstr(resource='playerProfilesWindow.titleText'), - color=(0.55, 0.5, 0.6), - icon=bui.gettexture('cuteSpaz'), - textcolor=(0.75, 0.7, 0.8), - on_activate_call=self._player_profiles_press, + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') ) - if first_selectable is None: - first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) - bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0) + bui.widget(edit=btn, left_widget=bbtn) # the button to go to OS-Specific leaderboards/high-score-lists/etc. if show_game_service_button: @@ -904,11 +851,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn) v -= game_service_button_space * 0.4 else: @@ -959,11 +904,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn) v -= achievements_button_space * 0.15 else: @@ -990,11 +933,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn) v -= leaderboards_button_space * 0.15 else: @@ -1039,41 +980,9 @@ class AccountSettingsWindow(bui.Window): self._tickets_text = None # bit of spacing before the reset/sign-out section - v -= 5 + # v -= 5 button_width = 250 - if show_reset_progress_button: - confirm_text = ( - bui.Lstr(resource=f'{self._r}.resetProgressConfirmText') - if self._can_reset_achievements - else bui.Lstr( - resource=f'{self._r}.resetProgressConfirmNoAchievementsText' - ) - ) - v -= reset_progress_button_space - self._reset_progress_button = btn = bui.buttonwidget( - parent=self._subcontainer, - position=((self._sub_width - button_width) * 0.5, v), - color=(0.55, 0.5, 0.6), - textcolor=(0.75, 0.7, 0.8), - autoselect=True, - size=(button_width, 60), - label=bui.Lstr(resource=f'{self._r}.resetProgressText'), - on_activate_call=lambda: confirm.ConfirmWindow( - text=confirm_text, - width=500, - height=200, - action=self._reset_progress, - ), - ) - if first_selectable is None: - first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) - bui.widget(edit=btn, left_widget=bbtn) self._linked_accounts_text: bui.Widget | None if show_linked_accounts_text: @@ -1133,11 +1042,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) self._unlink_accounts_button: bui.Widget | None @@ -1165,11 +1072,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) self._update_unlink_accounts_button() else: @@ -1235,11 +1140,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) if show_cancel_sign_in_button: @@ -1256,11 +1159,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) if show_delete_account_button: @@ -1277,11 +1178,9 @@ class AccountSettingsWindow(bui.Window): ) if first_selectable is None: first_selectable = btn - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, right_widget=bui.get_special_widget('squad_button') + ) bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) # Whatever the topmost selectable thing is, we want it to scroll all @@ -1322,9 +1221,6 @@ class AccountSettingsWindow(bui.Window): position=self._achievements_button.get_screen_space_center() ) - def _on_what_is_v2_press(self) -> None: - show_what_is_v2_page() - def _on_manage_account_press(self) -> None: self._do_manage_account_press(WebLocation.ACCOUNT_EDITOR) @@ -1502,16 +1398,6 @@ class AccountSettingsWindow(bui.Window): name_str = '??' bui.textwidget(edit=self._account_name_text, text=name_str) - if self._account_name_what_is_text is not None: - swidth = bui.get_string_width(name_str, suppress_warning=True) - # Eww; number-fudging. Need to recalibrate this if - # account name scaling changes. - x = self._sub_width * 0.5 - swidth * 0.75 - 190 - - bui.textwidget( - edit=self._account_name_what_is_text, - position=(x, self._account_name_what_is_y), - ) def _refresh_achievements(self) -> None: assert bui.app.classic is not None @@ -1550,23 +1436,6 @@ class AccountSettingsWindow(bui.Window): AccountUnlinkWindow(origin_widget=self._unlink_accounts_button) - def _player_profiles_press(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.profile.browser import ProfileBrowserWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - bui.app.ui_v1.set_main_menu_window( - ProfileBrowserWindow( - origin_widget=self._player_profiles_button - ).get_root_widget(), - from_window=self._root_widget, - ) - def _cancel_sign_in_press(self) -> None: # If we're waiting on an adapter to give us credentials, abort. self._signing_in_adapter = None @@ -1674,14 +1543,14 @@ class AccountSettingsWindow(bui.Window): bui.getsound('error').play() else: # Success! Plug in these credentials which will begin - # verifying them and set our primary account-handle - # when finished. + # verifying them and set our primary account-handle when + # finished. plus = bui.app.plus assert plus is not None plus.accounts.set_primary_credentials(result.credentials) - # Special case - if the user has explicitly logged out and - # logged in again with GPGS via this button, warn them that + # Special case - if the user has explicitly signed out and + # signed in again with GPGS via this button, warn them that # they need to use the app if they want to switch to a # different GPGS account. if ( @@ -1709,9 +1578,9 @@ class AccountSettingsWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.connectivity import wait_for_connectivity - # If we're still waiting for our master-server connection, - # keep the user informed of this instead of rushing in and - # failing immediately. + # If we're still waiting for our master-server connection, keep + # the user informed of this instead of rushing in and failing + # immediately. wait_for_connectivity(on_connected=self._v2_proxy_sign_in) def _v2_proxy_sign_in(self) -> None: @@ -1721,44 +1590,6 @@ class AccountSettingsWindow(bui.Window): assert self._sign_in_v2_proxy_button is not None V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button) - def _reset_progress(self) -> None: - try: - assert bui.app.classic is not None - # FIXME: This would need to happen server-side these days. - if self._can_reset_achievements: - logging.warning('ach resets not wired up.') - # bui.app.config['Achievements'] = {} - # bui.reset_achievements() - campaign = bui.app.classic.getcampaign('Default') - campaign.reset() # also writes the config.. - campaign = bui.app.classic.getcampaign('Challenges') - campaign.reset() # also writes the config.. - except Exception: - logging.exception('Error resetting co-op campaign progress.') - - bui.getsound('shieldDown').play() - self._refresh() - - def _back(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.mainmenu import MainMenuWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - - if not self._modal: - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) - def _save_state(self) -> None: try: sel = self._root_widget.get_selected_child() @@ -1788,15 +1619,6 @@ class AccountSettingsWindow(bui.Window): logging.exception('Error restoring state for %s.', self) -def show_what_is_v2_page() -> None: - """Show the webpage describing V2 accounts.""" - plus = bui.app.plus - assert plus is not None - - bamasteraddr = plus.get_master_server_address(version=2) - bui.open_url(f'{bamasteraddr}/whatisv2') - - def show_what_is_legacy_unlinking_page() -> None: """Show the webpage describing legacy unlinking.""" plus = bui.app.plus diff --git a/src/assets/ba_data/python/bauiv1lib/achievements.py b/src/assets/ba_data/python/bauiv1lib/achievements.py index 228e7731..00744b3b 100644 --- a/src/assets/ba_data/python/bauiv1lib/achievements.py +++ b/src/assets/ba_data/python/bauiv1lib/achievements.py @@ -40,6 +40,7 @@ class AchievementsWindow(PopupWindow): size=(self._width, self._height), scale=scale, bg_color=bg_color, + edge_buffer_scale=4.0, # Try to keep button unobscured. ) self._cancel_button = bui.buttonwidget( @@ -74,7 +75,7 @@ class AchievementsWindow(PopupWindow): scale=0.6, text=txt_final, maxwidth=200, - color=(1, 1, 1, 0.4), + color=bui.app.ui_v1.title_color, ) self._scrollwidget = bui.scrollwidget( diff --git a/src/assets/ba_data/python/bauiv1lib/appinvite.py b/src/assets/ba_data/python/bauiv1lib/appinvite.py index 4bcbbcb7..48857c33 100644 --- a/src/assets/ba_data/python/bauiv1lib/appinvite.py +++ b/src/assets/ba_data/python/bauiv1lib/appinvite.py @@ -29,7 +29,7 @@ class ShowFriendCodeWindow(bui.Window): color=(0.45, 0.63, 0.15), transition='in_scale', scale=( - 1.7 + 1.5 if uiscale is bui.UIScale.SMALL else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 ), diff --git a/src/assets/ba_data/python/bauiv1lib/debug.py b/src/assets/ba_data/python/bauiv1lib/benchmarks.py similarity index 87% rename from src/assets/ba_data/python/bauiv1lib/debug.py rename to src/assets/ba_data/python/bauiv1lib/benchmarks.py index e57ae074..357be9ee 100644 --- a/src/assets/ba_data/python/bauiv1lib/debug.py +++ b/src/assets/ba_data/python/bauiv1lib/benchmarks.py @@ -5,25 +5,27 @@ from __future__ import annotations import logging -from typing import cast +from typing import cast, override import bauiv1 as bui -class DebugWindow(bui.Window): - """Window for debugging internal values.""" +class BenchmarksAndStressTestsWindow(bui.MainWindow): + """Window for launching benchmarks or stress tests.""" - def __init__(self, transition: str | None = 'in_right'): + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): # pylint: disable=too-many-statements # pylint: disable=cyclic-import from bauiv1lib import popup - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_location('Benchmarks & Stress Tests') uiscale = bui.app.ui_v1.uiscale - self._width = width = 580 + self._width = width = 650 if uiscale is bui.UIScale.SMALL else 580 self._height = height = ( - 350 + 330 if uiscale is bui.UIScale.SMALL else 420 if uiscale is bui.UIScale.MEDIUM else 520 ) @@ -44,28 +46,41 @@ class DebugWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(width, height), - transition=transition, scale=( - 2.35 + 2.32 if uiscale is bui.UIScale.SMALL else 1.55 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, -30) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, ) - self._done_button = btn = bui.buttonwidget( - parent=self._root_widget, - position=(40, height - 67), - size=(120, 60), - scale=0.8, - autoselect=True, - label=bui.Lstr(resource='doneText'), - on_activate_call=self._done, - ) - bui.containerwidget(edit=self._root_widget, cancel_button=btn) + if bui.app.ui_v1.uiscale is bui.UIScale.SMALL: + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + self._done_button = bui.get_special_widget('back_button') + else: + self._done_button = btn = bui.buttonwidget( + parent=self._root_widget, + position=(40, height - 67), + size=(120, 60), + scale=0.8, + autoselect=True, + label=bui.Lstr(resource='doneText'), + on_activate_call=self.main_window_back, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.textwidget( parent=self._root_widget, position=(0, height - 60), @@ -303,6 +318,16 @@ class DebugWindow(bui.Window): ) bui.widget(btn, show_buffer_bottom=50) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + def _stress_test_player_count_decrement(self) -> None: self._stress_test_player_count = max( 1, self._stress_test_player_count - 1 @@ -370,18 +395,3 @@ class DebugWindow(bui.Window): round_duration=self._stress_test_round_duration, ) bui.containerwidget(edit=self._root_widget, transition='out_right') - - def _done(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.settings.advanced import AdvancedSettingsWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - bui.containerwidget(edit=self._root_widget, transition='out_right') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) diff --git a/src/assets/ba_data/python/bauiv1lib/confirm.py b/src/assets/ba_data/python/bauiv1lib/confirm.py index 394f2fe9..d0e848ad 100644 --- a/src/assets/ba_data/python/bauiv1lib/confirm.py +++ b/src/assets/ba_data/python/bauiv1lib/confirm.py @@ -61,7 +61,7 @@ class ConfirmWindow: toolbar_visibility='menu_minimal_no_back', parent=bui.get_special_widget('overlay_stack'), scale=( - 2.1 + 1.9 if uiscale is bui.UIScale.SMALL else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ), diff --git a/src/assets/ba_data/python/bauiv1lib/continues.py b/src/assets/ba_data/python/bauiv1lib/continues.py index 366fcbb4..a0d12339 100644 --- a/src/assets/ba_data/python/bauiv1lib/continues.py +++ b/src/assets/ba_data/python/bauiv1lib/continues.py @@ -38,7 +38,7 @@ class ContinuesWindow(bui.Window): bui.containerwidget( size=(self._width, self._height), background=False, - toolbar_visibility='menu_currency', + toolbar_visibility='menu_store', transition='in_scale', scale=1.5, ) @@ -100,29 +100,8 @@ class ContinuesWindow(bui.Window): self._tickets_text_base: str | None self._tickets_text: bui.Widget | None - if not bui.app.ui_v1.use_toolbars: - self._tickets_text_base = bui.Lstr( - resource='getTicketsWindow.youHaveShortText', - fallback_resource='getTicketsWindow.youHaveText', - ).evaluate() - self._tickets_text = bui.textwidget( - parent=self._root_widget, - text='', - flatness=1.0, - color=(0.2, 1.0, 0.2), - shadow=1.0, - position=( - self._width * 0.5 + width_total_half, - self._height - 50, - ), - size=(0, 0), - scale=0.35, - h_align='right', - v_align='center', - ) - else: - self._tickets_text_base = None - self._tickets_text = None + self._tickets_text_base = None + self._tickets_text = None self._counter_text = bui.textwidget( parent=self._root_widget, @@ -214,7 +193,7 @@ class ContinuesWindow(bui.Window): self._on_cancel() def _on_continue_press(self) -> None: - from bauiv1lib import gettickets + # from bauiv1lib import gettickets plus = bui.app.plus assert plus is not None @@ -238,7 +217,7 @@ class ContinuesWindow(bui.Window): self._counting_down = False bui.textwidget(edit=self._counter_text, text='') bui.getsound('error').play() - gettickets.show_get_tickets_prompt() + # gettickets.show_get_tickets_prompt() return if not self._transitioning_out: bui.getsound('swish').play() diff --git a/src/assets/ba_data/python/bauiv1lib/coop/browser.py b/src/assets/ba_data/python/bauiv1lib/coop/browser.py index 81a936ef..7e914fdd 100644 --- a/src/assets/ba_data/python/bauiv1lib/coop/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/coop/browser.py @@ -7,12 +7,8 @@ from __future__ import annotations import logging -from threading import Thread -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override -from bauiv1lib.store.button import StoreButton -from bauiv1lib.league.rankbutton import LeagueRankButton -from bauiv1lib.store.browser import StoreBrowserWindow import bauiv1 as bui if TYPE_CHECKING: @@ -21,7 +17,7 @@ if TYPE_CHECKING: from bauiv1lib.coop.tournamentbutton import TournamentButton -class CoopBrowserWindow(bui.Window): +class CoopBrowserWindow(bui.MainWindow): """Window for browsing co-op levels/games/etc.""" def __init__( @@ -37,16 +33,18 @@ class CoopBrowserWindow(bui.Window): # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. - Thread(target=self._preload_modules).start() + bui.app.threadpool_submit_no_wait(self._preload_modules) bui.set_analytics_screen('Coop Window') app = bui.app - assert app.classic is not None + classic = app.classic + assert classic is not None cfg = app.config # Quick note to players that tourneys won't work in ballistica - # core builds. (need to split the word so it won't get subbed out) + # core builds. (need to split the word so it won't get subbed + # out) if 'ballistica' + 'kit' == bui.appname(): bui.apptimer( 1.0, @@ -56,16 +54,6 @@ class CoopBrowserWindow(bui.Window): ), ) - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - # Try to recreate the same number of buttons we had last time so our # re-selection code works. self._tournament_button_count = app.config.get('Tournament Rows', 0) @@ -83,16 +71,14 @@ class CoopBrowserWindow(bui.Window): self._hard_button_lock_image: bui.Widget | None = None self._campaign_percent_text: bui.Widget | None = None - assert bui.app.classic is not None - uiscale = bui.app.ui_v1.uiscale + uiscale = app.ui_v1.uiscale self._width = 1520 if uiscale is bui.UIScale.SMALL else 1120 self._x_inset = x_inset = 200 if uiscale is bui.UIScale.SMALL else 0 self._height = ( - 657 + 565 if uiscale is bui.UIScale.SMALL else 730 if uiscale is bui.UIScale.MEDIUM else 800 ) - app.ui_v1.set_main_menu_location('Coop Select') self._r = 'coopSelectWindow' top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 @@ -104,7 +90,7 @@ class CoopBrowserWindow(bui.Window): if ( self._campaign_difficulty == 'hard' - and not app.classic.accounts.have_pro_options() + and not classic.accounts.have_pro_options() ): plus.add_v1_account_transaction( { @@ -119,99 +105,114 @@ class CoopBrowserWindow(bui.Window): root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), toolbar_visibility='menu_full', - scale_origin_stack_offset=scale_origin, stack_offset=( - (0, -15) + (0, -17) if uiscale is bui.UIScale.SMALL else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) ), - transition=transition, scale=( - 1.2 + 1.28 if uiscale is bui.UIScale.SMALL else 0.8 if uiscale is bui.UIScale.MEDIUM else 0.75 ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) - if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: - self._back_button = None + if uiscale is bui.UIScale.SMALL: + self._back_button = bui.get_special_widget('back_button') + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) else: self._back_button = bui.buttonwidget( parent=self._root_widget, - position=( - 75 + x_inset, - self._height - - 87 - - (4 if uiscale is bui.UIScale.SMALL else 0), - ), + position=(75 + x_inset, self._height - 87), size=(120, 60), scale=1.2, autoselect=True, label=bui.Lstr(resource='backText'), button_type='back', + on_activate_call=self.main_window_back, ) - - self._league_rank_button: LeagueRankButton | None - self._store_button: StoreButton | None - self._store_button_widget: bui.Widget | None - self._league_rank_button_widget: bui.Widget | None - - if not app.ui_v1.use_toolbars: - prb = self._league_rank_button = LeagueRankButton( - parent=self._root_widget, - position=( - self._width - (282 + x_inset), - self._height - - 85 - - (4 if uiscale is bui.UIScale.SMALL else 0), - ), - size=(100, 60), - color=(0.4, 0.4, 0.9), - textcolor=(0.9, 0.9, 2.0), - scale=1.05, - on_activate_call=bui.WeakCall(self._switch_to_league_rankings), - ) - self._league_rank_button_widget = prb.get_button() - - sbtn = self._store_button = StoreButton( - parent=self._root_widget, - position=( - self._width - (170 + x_inset), - self._height - - 85 - - (4 if uiscale is bui.UIScale.SMALL else 0), - ), - size=(100, 60), - color=(0.6, 0.4, 0.7), - show_tickets=True, - button_type='square', - sale_scale=0.85, - textcolor=(0.9, 0.7, 1.0), - scale=1.05, - on_activate_call=bui.WeakCall(self._switch_to_score, None), - ) - self._store_button_widget = sbtn.get_button() - bui.widget( + bui.buttonwidget( edit=self._back_button, - right_widget=self._league_rank_button_widget, + button_type='backSmall', + size=(60, 50), + position=( + 75 + x_inset, + self._height - 87 + 6, + ), + label=bui.charstr(bui.SpecialChar.BACK), ) - bui.widget( - edit=self._league_rank_button_widget, - left_widget=self._back_button, + bui.containerwidget( + edit=self._root_widget, cancel_button=self._back_button ) - else: - self._league_rank_button = None - self._store_button = None - self._store_button_widget = None - self._league_rank_button_widget = None + + # self._league_rank_button: LeagueRankButton | None + # self._store_button: StoreButton | None + # self._store_button_widget: bui.Widget | None + # self._league_rank_button_widget: bui.Widget | None + + # if not app.ui_v1.use_toolbars: + # prb = self._league_rank_button = LeagueRankButton( + # parent=self._root_widget, + # position=( + # self._width - (282 + x_inset), + # self._height + # - 85 + # - (4 if uiscale is bui.UIScale.SMALL else 0), + # ), + # size=(100, 60), + # color=(0.4, 0.4, 0.9), + # textcolor=(0.9, 0.9, 2.0), + # scale=1.05, + # on_activate_call=bui.WeakCall( + # self._switch_to_league_rankings), + # ) + # self._league_rank_button_widget = prb.get_button() + + # sbtn = self._store_button = StoreButton( + # parent=self._root_widget, + # position=( + # self._width - (170 + x_inset), + # self._height + # - 85 + # - (4 if uiscale is bui.UIScale.SMALL else 0), + # ), + # size=(100, 60), + # color=(0.6, 0.4, 0.7), + # show_tickets=True, + # button_type='square', + # sale_scale=0.85, + # textcolor=(0.9, 0.7, 1.0), + # scale=1.05, + # on_activate_call=bui.WeakCall(self._switch_to_score, None), + # ) + # self._store_button_widget = sbtn.get_button() + # assert self._back_button is not None + # bui.widget( + # edit=self._back_button, + # right_widget=self._league_rank_button_widget, + # ) + # bui.widget( + # edit=self._league_rank_button_widget, + # left_widget=self._back_button, + # ) + # else: + # self._league_rank_button = None + # self._store_button = None + # self._store_button_widget = None + # self._league_rank_button_widget = None # Move our corner buttons dynamically to keep them out of the way of # the party icon :-( - self._update_corner_button_positions() - self._update_corner_button_positions_timer = bui.AppTimer( - 1.0, bui.WeakCall(self._update_corner_button_positions), repeat=True - ) + # self._update_corner_button_positions() + # self._update_corner_button_positions_timer = bui.AppTimer( + # 1.0, bui.WeakCall( + # self._update_corner_button_positions), repeat=True + # ) self._last_tournament_query_time: float | None = None self._last_tournament_query_response_time: float | None = None @@ -245,31 +246,14 @@ class CoopBrowserWindow(bui.Window): v_align='center', ) - if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: bui.textwidget(edit=txt, text='') - if self._back_button is not None: - bui.buttonwidget( - edit=self._back_button, - button_type='backSmall', - size=(60, 50), - position=( - 75 + x_inset, - self._height - - 87 - - (4 if uiscale is bui.UIScale.SMALL else 0) - + 6, - ), - label=bui.charstr(bui.SpecialChar.BACK), - ) - self._selected_row = cfg.get('Selected Coop Row', None) self._scroll_width = self._width - (130 + 2 * x_inset) self._scroll_height = self._height - ( - 190 - if uiscale is bui.UIScale.SMALL and app.ui_v1.use_toolbars - else 160 + 185 if uiscale is bui.UIScale.SMALL else 160 ) self._subcontainerwidth = 800.0 @@ -280,7 +264,7 @@ class CoopBrowserWindow(bui.Window): highlight=False, position=( (65 + x_inset, 120) - if uiscale is bui.UIScale.SMALL and app.ui_v1.use_toolbars + if uiscale is bui.UIScale.SMALL else (65 + x_inset, 70) ), size=(self._scroll_width, self._scroll_height), @@ -309,17 +293,17 @@ class CoopBrowserWindow(bui.Window): # each one of those tournaments, go ahead and display it as a # starting point. if ( - app.classic.accounts.account_tournament_list is not None - and app.classic.accounts.account_tournament_list[0] + classic.accounts.account_tournament_list is not None + and classic.accounts.account_tournament_list[0] == plus.get_v1_account_state_num() and all( - t_id in app.classic.accounts.tournament_info - for t_id in app.classic.accounts.account_tournament_list[1] + t_id in classic.accounts.tournament_info + for t_id in classic.accounts.account_tournament_list[1] ) ): tourney_data = [ - app.classic.accounts.tournament_info[t_id] - for t_id in app.classic.accounts.account_tournament_list[1] + classic.accounts.tournament_info[t_id] + for t_id in classic.accounts.account_tournament_list[1] ] self._update_for_data(tourney_data) @@ -329,37 +313,24 @@ class CoopBrowserWindow(bui.Window): ) self._update() - def _update_corner_button_positions(self) -> None: - assert bui.app.classic is not None - uiscale = bui.app.ui_v1.uiscale - offs = ( - -55 - if uiscale is bui.UIScale.SMALL and bui.is_party_icon_visible() - else 0 + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) ) - if self._league_rank_button is not None: - self._league_rank_button.set_position( - ( - self._width - 282 + offs - self._x_inset, - self._height - - 85 - - (4 if uiscale is bui.UIScale.SMALL else 0), - ) - ) - if self._store_button is not None: - self._store_button.set_position( - ( - self._width - 170 + offs - self._x_inset, - self._height - - 85 - - (4 if uiscale is bui.UIScale.SMALL else 0), - ) - ) - # noinspection PyUnresolvedReferences + @override + def on_main_window_close(self) -> None: + self._save_state() + @staticmethod def _preload_modules() -> None: """Preload modules we use; avoids hitches (called in bg thread).""" + # pylint: disable=cyclic-import import bauiv1lib.purchase as _unused1 import bauiv1lib.coop.gamebutton as _unused2 import bauiv1lib.confirm as _unused3 @@ -382,9 +353,9 @@ class CoopBrowserWindow(bui.Window): cur_time = bui.apptime() - # If its been a while since we got a tournament update, consider the - # data invalid (prevents us from joining tournaments if our internet - # connection goes down for a while). + # If its been a while since we got a tournament update, consider + # the data invalid (prevents us from joining tournaments if our + # internet connection goes down for a while). if ( self._last_tournament_query_response_time is None or bui.apptime() - self._last_tournament_query_response_time @@ -466,8 +437,8 @@ class CoopBrowserWindow(bui.Window): logging.exception('Error updating campaign lock.') def _update_for_data(self, data: list[dict[str, Any]] | None) -> None: - # If the number of tournaments or challenges in the data differs from - # our current arrangement, refresh with the new number. + # If the number of tournaments or challenges in the data differs + # from our current arrangement, refresh with the new number. if (data is None and self._tournament_button_count != 0) or ( data is not None and (len(data) != self._tournament_button_count) ): @@ -541,6 +512,7 @@ class CoopBrowserWindow(bui.Window): def _refresh_campaign_row(self) -> None: # pylint: disable=too-many-locals + # pylint: disable=too-many-statements # pylint: disable=cyclic-import from bauiv1lib.coop.gamebutton import GameButton @@ -660,14 +632,21 @@ class CoopBrowserWindow(bui.Window): bui.widget(edit=campaign_buttons[0], left_widget=self._easy_button) - if self._back_button is not None: - bui.widget(edit=self._easy_button, up_widget=self._back_button) - for btn in campaign_buttons: - bui.widget( - edit=btn, - up_widget=self._back_button, - down_widget=next_widget_down, - ) + # bui.widget(edit=self._easy_button) + # if self._back_button is not None: + bui.widget( + edit=self._easy_button, + left_widget=self._back_button, + up_widget=self._back_button, + ) + bui.widget(edit=self._hard_button, left_widget=self._back_button) + for btn in campaign_buttons: + bui.widget( + edit=btn, + up_widget=self._back_button, + ) + for btn in campaign_buttons: + bui.widget(edit=btn, down_widget=next_widget_down) # Update our existing percent-complete text. assert bui.app.classic is not None @@ -721,7 +700,7 @@ class CoopBrowserWindow(bui.Window): tourney_row_height = 200 self._subcontainerheight = ( - 620 + self._tournament_button_count * tourney_row_height + 700 + self._tournament_button_count * tourney_row_height ) self._subcontainer = bui.containerwidget( @@ -736,15 +715,11 @@ class CoopBrowserWindow(bui.Window): bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) - if self._back_button is not None: - bui.containerwidget( - edit=self._root_widget, cancel_button=self._back_button - ) w_parent = self._subcontainer h_base = 6 - v = self._subcontainerheight - 73 + v = self._subcontainerheight - 90 self._campaign_percent_text = bui.textwidget( parent=w_parent, @@ -757,7 +732,7 @@ class CoopBrowserWindow(bui.Window): scale=1.1, ) - row_v_show_buffer = 100 + row_v_show_buffer = 80 v -= 198 h_scroll = bui.hscrollwidget( @@ -817,17 +792,17 @@ class CoopBrowserWindow(bui.Window): textcolor=(0.7, 0.6, 0.75), autoselect=True, up_widget=self._campaign_h_scroll, + left_widget=self._back_button, on_activate_call=self._on_tournament_info_press, ) bui.widget( edit=self._tournament_info_button, - left_widget=self._tournament_info_button, right_widget=self._tournament_info_button, ) - # Say 'unavailable' if there are zero tournaments, and if we're not - # signed in add that as well (that's probably why we see - # no tournaments). + # Say 'unavailable' if there are zero tournaments, and if we're + # not signed in add that as well (that's probably why we see no + # tournaments). if self._tournament_button_count == 0: unavailable_text = bui.Lstr(resource='unavailableText') if plus.get_v1_account_state() != 'signed_in': @@ -989,6 +964,7 @@ class CoopBrowserWindow(bui.Window): if i + 1 < len(self._tournament_buttons) else custom_h_scroll ), + left_widget=self._back_button, ) bui.widget( edit=tbutton.more_scores_button, @@ -1007,7 +983,7 @@ class CoopBrowserWindow(bui.Window): ), ) - for btn in self._custom_buttons: + for i, btn in enumerate(self._custom_buttons): try: bui.widget( edit=btn.get_button(), @@ -1017,18 +993,14 @@ class CoopBrowserWindow(bui.Window): else self._tournament_info_button ), ) + if i == 0: + bui.widget( + edit=btn.get_button(), left_widget=self._back_button + ) + except Exception: logging.exception('Error wiring up custom buttons.') - if self._back_button is not None: - bui.buttonwidget( - edit=self._back_button, on_activate_call=self._back - ) - else: - bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._back - ) - # There's probably several 'onSelected' callbacks pushed onto the # event queue.. we need to push ours too so we're enabled *after* them. bui.pushcall(self._enable_selectable_callback) @@ -1041,63 +1013,63 @@ class CoopBrowserWindow(bui.Window): def _enable_selectable_callback(self) -> None: self._do_selection_callbacks = True - def _switch_to_league_rankings(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.account import show_sign_in_prompt - from bauiv1lib.league.rankwindow import LeagueRankWindow + # def _switch_to_league_rankings(self) -> None: + # # pylint: disable=cyclic-import + # from bauiv1lib.account import show_sign_in_prompt + # from bauiv1lib.league.rankwindow import LeagueRankWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return + # # no-op if our underlying widget is dead or on its way out. + # if not self._root_widget or self._root_widget.transitioning_out: + # return - plus = bui.app.plus - assert plus is not None + # plus = bui.app.plus + # assert plus is not None - if plus.get_v1_account_state() != 'signed_in': - show_sign_in_prompt() - return - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - assert self._league_rank_button is not None - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - LeagueRankWindow( - origin_widget=self._league_rank_button.get_button() - ).get_root_widget(), - from_window=self._root_widget, - ) + # if plus.get_v1_account_state() != 'signed_in': + # show_sign_in_prompt() + # return + # self._save_state() + # bui.containerwidget(edit=self._root_widget, transition='out_left') + # assert self._league_rank_button is not None + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # LeagueRankWindow( + # origin_widget=self._league_rank_button.get_button() + # ), + # from_window=self, + # ) - def _switch_to_score( - self, - show_tab: ( - StoreBrowserWindow.TabID | None - ) = StoreBrowserWindow.TabID.EXTRAS, - ) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.account import show_sign_in_prompt + # def _switch_to_score( + # self, + # show_tab: ( + # StoreBrowserWindow.TabID | None + # ) = StoreBrowserWindow.TabID.EXTRAS, + # ) -> None: + # # pylint: disable=cyclic-import + # from bauiv1lib.account import show_sign_in_prompt - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return + # # no-op if our underlying widget is dead or on its way out. + # if not self._root_widget or self._root_widget.transitioning_out: + # return - plus = bui.app.plus - assert plus is not None + # plus = bui.app.plus + # assert plus is not None - if plus.get_v1_account_state() != 'signed_in': - show_sign_in_prompt() - return - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - assert self._store_button is not None - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - StoreBrowserWindow( - origin_widget=self._store_button.get_button(), - show_tab=show_tab, - back_location='CoopBrowserWindow', - ).get_root_widget(), - from_window=self._root_widget, - ) + # if plus.get_v1_account_state() != 'signed_in': + # show_sign_in_prompt() + # return + # self._save_state() + # bui.containerwidget(edit=self._root_widget, transition='out_left') + # assert self._store_button is not None + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # StoreBrowserWindow( + # origin_widget=self._store_button.get_button(), + # show_tab=show_tab, + # back_location='CoopBrowserWindow', + # ), + # from_window=self, + # ) def is_tourney_data_up_to_date(self) -> bool: """Return whether our tourney data is up to date.""" @@ -1252,24 +1224,23 @@ class CoopBrowserWindow(bui.Window): position=tournament_button.button.get_screen_space_center(), ) - def _back(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.play import PlayWindow + # def _back(self) -> None: + # # pylint: disable=cyclic-import + # from bauiv1lib.play import PlayWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return + # # no-op if our underlying widget is dead or on its way out. + # if not self._root_widget or self._root_widget.transitioning_out: + # return - # If something is selected, store it. - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - PlayWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + # # If something is selected, store it. + # self._save_state() + # bui.containerwidget( + # edit=self._root_widget, transition=self._transition_out + # ) + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # PlayWindow(transition='in_left'), from_window=self, is_back=True + # ) def _save_state(self) -> None: cfg = bui.app.config @@ -1277,10 +1248,10 @@ class CoopBrowserWindow(bui.Window): sel = self._root_widget.get_selected_child() if sel == self._back_button: sel_name = 'Back' - elif sel == self._store_button_widget: - sel_name = 'Store' - elif sel == self._league_rank_button_widget: - sel_name = 'PowerRanking' + # elif sel == self._store_button_widget: + # sel_name = 'Store' + # elif sel == self._league_rank_button_widget: + # sel_name = 'PowerRanking' elif sel == self._scrollwidget: sel_name = 'Scroll' else: @@ -1305,10 +1276,10 @@ class CoopBrowserWindow(bui.Window): sel = self._back_button elif sel_name == 'Scroll': sel = self._scrollwidget - elif sel_name == 'PowerRanking': - sel = self._league_rank_button_widget - elif sel_name == 'Store': - sel = self._store_button_widget + # elif sel_name == 'PowerRanking': + # sel = self._league_rank_button_widget + # elif sel_name == 'Store': + # sel = self._store_button_widget else: sel = self._scrollwidget bui.containerwidget(edit=self._root_widget, selected_child=sel) diff --git a/src/assets/ba_data/python/bauiv1lib/creditslist.py b/src/assets/ba_data/python/bauiv1lib/credits.py similarity index 82% rename from src/assets/ba_data/python/bauiv1lib/creditslist.py rename to src/assets/ba_data/python/bauiv1lib/credits.py index 711d3ffc..5c735e49 100644 --- a/src/assets/ba_data/python/bauiv1lib/creditslist.py +++ b/src/assets/ba_data/python/bauiv1lib/credits.py @@ -6,7 +6,7 @@ from __future__ import annotations import os import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import bauiv1 as bui @@ -14,30 +14,23 @@ if TYPE_CHECKING: from typing import Sequence -class CreditsListWindow(bui.Window): +class CreditsWindow(bui.MainWindow): """Window for displaying game credits.""" - def __init__(self, origin_widget: bui.Widget | None = None): + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): # pylint: disable=too-many-locals # pylint: disable=too-many-statements import json bui.set_analytics_screen('Credits Window') - # if they provided an origin-widget, scale up from that - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - transition = 'in_right' - assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - width = 870 if uiscale is bui.UIScale.SMALL else 670 + width = 990 if uiscale is bui.UIScale.SMALL else 670 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 height = 398 if uiscale is bui.UIScale.SMALL else 500 @@ -45,36 +38,37 @@ class CreditsListWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(width, height), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, - scale=( - 2.0 + toolbar_visibility=( + 'menu_minimal' if uiscale is bui.UIScale.SMALL - else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 + else 'menu_full' + ), + scale=( + 1.8 + if uiscale is bui.UIScale.SMALL + else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( - (0, -8) if uiscale is bui.UIScale.SMALL else (0, 0) + (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) - if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._back + edit=self._root_widget, on_cancel_call=self.main_window_back ) else: btn = bui.buttonwidget( parent=self._root_widget, - position=( - 40 + x_inset, - height - (68 if uiscale is bui.UIScale.SMALL else 62), - ), + position=(40 + x_inset, height - 62), size=(140, 60), scale=0.8, label=bui.Lstr(resource='backText'), button_type='back', - on_activate_call=self._back, + on_activate_call=self.main_window_back, autoselect=True, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) @@ -84,7 +78,7 @@ class CreditsListWindow(bui.Window): button_type='backSmall', position=( 40 + x_inset, - height - (68 if uiscale is bui.UIScale.SMALL else 62) + 5, + height - 62 + 5, ), size=(60, 48), label=bui.charstr(bui.SpecialChar.BACK), @@ -92,8 +86,9 @@ class CreditsListWindow(bui.Window): bui.textwidget( parent=self._root_widget, - position=(0, height - (59 if uiscale is bui.UIScale.SMALL else 54)), + position=(0, height - (65 if uiscale is bui.UIScale.SMALL else 54)), size=(width, 30), + scale=0.8 if uiscale is bui.UIScale.SMALL else 1.0, text=bui.Lstr( resource=f'{self._r}.titleText', subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], @@ -111,16 +106,15 @@ class CreditsListWindow(bui.Window): capture_arrows=True, ) - if bui.app.ui_v1.use_toolbars: + bui.widget( + edit=scroll, + right_widget=bui.get_special_widget('squad_button'), + ) + if uiscale is bui.UIScale.SMALL: bui.widget( edit=scroll, - right_widget=bui.get_special_widget('party_button'), + left_widget=bui.get_special_widget('back_button'), ) - if uiscale is bui.UIScale.SMALL: - bui.widget( - edit=scroll, - left_widget=bui.get_special_widget('back_button'), - ) def _format_names(names2: Sequence[str], inset: float) -> str: sval = '' @@ -354,18 +348,12 @@ class CreditsListWindow(bui.Window): ) voffs -= line_height - def _back(self) -> None: - from bauiv1lib.mainmenu import MainMenuWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) ) diff --git a/src/assets/ba_data/python/bauiv1lib/discord.py b/src/assets/ba_data/python/bauiv1lib/discord.py index de891b29..40aa34f1 100644 --- a/src/assets/ba_data/python/bauiv1lib/discord.py +++ b/src/assets/ba_data/python/bauiv1lib/discord.py @@ -51,7 +51,7 @@ class DiscordWindow(bui.Window): ) ) - if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: bui.containerwidget( edit=self._root_widget, on_cancel_call=self._do_back ) diff --git a/src/assets/ba_data/python/bauiv1lib/fileselector.py b/src/assets/ba_data/python/bauiv1lib/fileselector.py index a2547b26..df17ad11 100644 --- a/src/assets/ba_data/python/bauiv1lib/fileselector.py +++ b/src/assets/ba_data/python/bauiv1lib/fileselector.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from typing import Any, Callable, Sequence -class FileSelectorWindow(bui.Window): +class FileSelectorWindow(bui.MainWindow): """Window for selecting files.""" def __init__( @@ -26,6 +26,8 @@ class FileSelectorWindow(bui.Window): show_base_path: bool = True, valid_file_extensions: Sequence[str] | None = None, allow_folders: bool = False, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, ): if valid_file_extensions is None: valid_file_extensions = [] @@ -51,7 +53,6 @@ class FileSelectorWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), - transition='in_right', scale=( 2.23 if uiscale is bui.UIScale.SMALL @@ -60,7 +61,9 @@ class FileSelectorWindow(bui.Window): stack_offset=( (0, -35) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) bui.textwidget( parent=self._root_widget, @@ -135,6 +138,31 @@ class FileSelectorWindow(bui.Window): ) self._set_path(path) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # Pull everything out of self here. If we do it below in the lambda, + # we'll keep self alive which is bad. + path = self._base_path + callback = self._callback + show_base_path = self._show_base_path + valid_file_extensions = self._valid_file_extensions + allow_folders = self._allow_folders + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + path=path, + callback=callback, + show_base_path=show_base_path, + valid_file_extensions=valid_file_extensions, + allow_folders=allow_folders, + ) + ) + def _on_up_press(self) -> None: self._on_entry_activated('..') @@ -458,6 +486,7 @@ class FileSelectorWindow(bui.Window): ) def _cancel(self) -> None: - bui.containerwidget(edit=self._root_widget, transition='out_right') + # bui.containerwidget(edit=self._root_widget, transition='out_right') + self.main_window_back() if self._callback is not None: self._callback(None) diff --git a/src/assets/ba_data/python/bauiv1lib/gather/__init__.py b/src/assets/ba_data/python/bauiv1lib/gather/__init__.py index 6e35d6a8..e71d2cc3 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/__init__.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/__init__.py @@ -7,6 +7,7 @@ from __future__ import annotations import weakref import logging from enum import Enum +from typing import override from bauiv1lib.tabs import TabRow import bauiv1 as bui @@ -52,7 +53,7 @@ class GatherTab: """Called when the parent window is restoring state.""" -class GatherWindow(bui.Window): +class GatherWindow(bui.MainWindow): """Window for joining/inviting friends.""" class TabID(Enum): @@ -82,22 +83,11 @@ class GatherWindow(bui.Window): assert plus is not None bui.set_analytics_screen('Gather Window') - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_location('Gather') - bui.set_party_icon_always_visible(True) uiscale = bui.app.ui_v1.uiscale - self._width = 1440 if uiscale is bui.UIScale.SMALL else 1040 + self._width = 1640 if uiscale is bui.UIScale.SMALL else 1040 x_offs = 200 if uiscale is bui.UIScale.SMALL else 0 self._height = ( - 582 + 550 if uiscale is bui.UIScale.SMALL else 680 if uiscale is bui.UIScale.MEDIUM else 800 ) @@ -108,25 +98,29 @@ class GatherWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + extra_top), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, - scale=( - 1.3 + toolbar_visibility=( + 'menu_tokens' if uiscale is bui.UIScale.SMALL - else 0.97 if uiscale is bui.UIScale.MEDIUM else 0.8 + else 'menu_full' + ), + scale=( + 1.15 + if uiscale is bui.UIScale.SMALL + else 0.95 if uiscale is bui.UIScale.MEDIUM else 0.7 ), stack_offset=( - (0, -11) + (0, -20) if uiscale is bui.UIScale.SMALL else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) - if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars: + if uiscale is bui.UIScale.SMALL: bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._back + edit=self._root_widget, on_cancel_call=self.main_window_back ) self._back_button = None else: @@ -138,7 +132,7 @@ class GatherWindow(bui.Window): autoselect=True, label=bui.Lstr(resource='backText'), button_type='back', - on_activate_call=self._back, + on_activate_call=self.main_window_back, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) bui.buttonwidget( @@ -151,7 +145,7 @@ class GatherWindow(bui.Window): condensed = uiscale is not bui.UIScale.LARGE t_offs_y = ( - 0 if not condensed else 25 if uiscale is bui.UIScale.MEDIUM else 17 + 0 if not condensed else 25 if uiscale is bui.UIScale.MEDIUM else 33 ) bui.textwidget( parent=self._root_widget, @@ -161,12 +155,12 @@ class GatherWindow(bui.Window): scale=( 1.5 if not condensed - else 1.0 if uiscale is bui.UIScale.MEDIUM else 0.6 + else 1.0 if uiscale is bui.UIScale.MEDIUM else 1.0 ), h_align='center', v_align='center', text=bui.Lstr(resource=f'{self._r}.titleText'), - maxwidth=550, + maxwidth=320, ) scroll_buffer_h = 130 + 2 * x_offs @@ -218,16 +212,15 @@ class GatherWindow(bui.Window): if tabtype is not None: self._tabs[tab_id] = tabtype(self) - if bui.app.ui_v1.use_toolbars: + bui.widget( + edit=self._tab_row.tabs[tabdefs[-1][0]].button, + right_widget=bui.get_special_widget('squad_button'), + ) + if uiscale is bui.UIScale.SMALL: bui.widget( - edit=self._tab_row.tabs[tabdefs[-1][0]].button, - right_widget=bui.get_special_widget('party_button'), + edit=self._tab_row.tabs[tabdefs[0][0]].button, + left_widget=bui.get_special_widget('back_button'), ) - if uiscale is bui.UIScale.SMALL: - bui.widget( - edit=self._tab_row.tabs[tabdefs[0][0]].button, - left_widget=bui.get_special_widget('back_button'), - ) self._scroll_width = self._width - scroll_buffer_h self._scroll_height = self._height - 180.0 + tabs_top_extra @@ -257,24 +250,36 @@ class GatherWindow(bui.Window): self._restore_state() - def __del__(self) -> None: - bui.set_party_icon_always_visible(False) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() def playlist_select(self, origin_widget: bui.Widget) -> None: """Called by the private-hosting tab to select a playlist.""" from bauiv1lib.play import PlayWindow + classic = bui.app.classic + assert classic is not None + # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') - assert bui.app.classic is not None - bui.app.ui_v1.selecting_private_party_playlist = True - bui.app.ui_v1.set_main_menu_window( - PlayWindow(origin_widget=origin_widget).get_root_widget(), - from_window=self._root_widget, + classic.selecting_private_party_playlist = True + bui.app.ui_v1.set_main_window( + PlayWindow(origin_widget=origin_widget), from_window=self ) def _set_tab(self, tab_id: TabID) -> None: @@ -340,8 +345,6 @@ class GatherWindow(bui.Window): logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: - from efro.util import enum_by_value - try: for tab in self._tabs.values(): tab.restore_state() @@ -354,7 +357,7 @@ class GatherWindow(bui.Window): current_tab = self.TabID.ABOUT gather_tab_val = bui.app.config.get('Gather Tab') try: - stored_tab = enum_by_value(self.TabID, gather_tab_val) + stored_tab = self.TabID(gather_tab_val) if stored_tab in self._tab_row.tabs: current_tab = stored_tab except ValueError: @@ -366,9 +369,7 @@ class GatherWindow(bui.Window): sel = self._tab_container elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): try: - sel_tab_id = enum_by_value( - self.TabID, sel_name.split(':')[-1] - ) + sel_tab_id = self.TabID(sel_name.split(':')[-1]) except ValueError: sel_tab_id = self.TabID.ABOUT sel = self._tab_row.tabs[sel_tab_id].button @@ -378,20 +379,3 @@ class GatherWindow(bui.Window): except Exception: logging.exception('Error restoring state for %s.', self) - - def _back(self) -> None: - from bauiv1lib.mainmenu import MainMenuWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) diff --git a/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py b/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py index 086471e0..dacf2c6c 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py @@ -157,6 +157,7 @@ class AboutGatherTab(GatherTab): autoselect=True, on_activate_call=bui.WeakCall(self._invite_to_try_press), up_widget=tab_button, + show_buffer_top=500, ) y -= invite_height else: diff --git a/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py b/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py index 8c889efd..4dcc9c79 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py @@ -89,10 +89,10 @@ class ManualGatherTab(GatherTab): self._container: bui.Widget | None = None self._join_by_address_text: bui.Widget | None = None self._favorites_text: bui.Widget | None = None - self._width: int | None = None - self._height: int | None = None - self._scroll_width: int | None = None - self._scroll_height: int | None = None + self._width: float | None = None + self._height: float | None = None + self._scroll_width: float | None = None + self._scroll_height: float | None = None self._favorites_scroll_width: int | None = None self._favorites_connect_button: bui.Widget | None = None self._scrollwidget: bui.Widget | None = None @@ -241,7 +241,7 @@ class ManualGatherTab(GatherTab): self._build_join_by_address_tab(region_width, region_height) if value is SubTabType.FAVORITES: - self._build_favorites_tab(region_height) + self._build_favorites_tab(region_width, region_height) # The old manual tab def _build_join_by_address_tab( @@ -276,7 +276,9 @@ class ManualGatherTab(GatherTab): maxwidth=380, size=(420, 60), ) + assert self._join_by_address_text is not None bui.widget(edit=self._join_by_address_text, down_widget=txt) + assert self._favorites_text is not None bui.widget(edit=self._favorites_text, down_widget=txt) bui.textwidget( parent=self._container, @@ -349,13 +351,16 @@ class ManualGatherTab(GatherTab): bui.widget(edit=self._check_button, up_widget=btn) # Tab containing saved favorite addresses - def _build_favorites_tab(self, region_height: float) -> None: + def _build_favorites_tab( + self, region_width: float, region_height: float + ) -> None: c_height = region_height - 20 v = c_height - 35 - 25 - 30 assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040 + # self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040 + self._width = region_width x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 578 @@ -400,7 +405,7 @@ class ManualGatherTab(GatherTab): self._favorites_connect_button = btn1 = bui.buttonwidget( parent=self._container, size=(b_width, b_height), - position=(40 if uiscale is bui.UIScale.SMALL else 40, btnv), + position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv), button_type='square', color=(0.6, 0.53, 0.63), textcolor=(0.75, 0.7, 0.8), @@ -409,7 +414,7 @@ class ManualGatherTab(GatherTab): label=bui.Lstr(resource='gatherWindow.manualConnectText'), autoselect=True, ) - if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars: + if uiscale is bui.UIScale.SMALL: bui.widget( edit=btn1, left_widget=bui.get_special_widget('back_button'), @@ -418,7 +423,7 @@ class ManualGatherTab(GatherTab): bui.buttonwidget( parent=self._container, size=(b_width, b_height), - position=(40 if uiscale is bui.UIScale.SMALL else 40, btnv), + position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv), button_type='square', color=(0.6, 0.53, 0.63), textcolor=(0.75, 0.7, 0.8), @@ -431,7 +436,7 @@ class ManualGatherTab(GatherTab): bui.buttonwidget( parent=self._container, size=(b_width, b_height), - position=(40 if uiscale is bui.UIScale.SMALL else 40, btnv), + position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv), button_type='square', color=(0.6, 0.53, 0.63), textcolor=(0.75, 0.7, 0.8), @@ -444,7 +449,7 @@ class ManualGatherTab(GatherTab): v -= sub_scroll_height + 23 self._scrollwidget = scrlw = bui.scrollwidget( parent=self._container, - position=(190 if uiscale is bui.UIScale.SMALL else 225, v), + position=(290 if uiscale is bui.UIScale.SMALL else 225, v), size=(sub_scroll_width, sub_scroll_height), claims_left_right=True, ) @@ -469,7 +474,7 @@ class ManualGatherTab(GatherTab): scale=1.2, position=( ( - (190 if uiscale is bui.UIScale.SMALL else 225) + (240 if uiscale is bui.UIScale.SMALL else 225) + sub_scroll_width * 0.5 ), v + sub_scroll_height * 0.5, @@ -760,6 +765,7 @@ class ManualGatherTab(GatherTab): claims_left_right=bool(servers), claims_up_down=bool(servers), ) + assert self._scrollwidget is not None bui.widget( edit=self._scrollwidget, up_widget=self._favorites_text, diff --git a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py index 223d6d2c..6bdf4c6b 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py @@ -559,7 +559,8 @@ class PrivateGatherTab(GatherTab): def _build_host_tab(self) -> None: # pylint: disable=too-many-branches # pylint: disable=too-many-statements - assert bui.app.classic is not None + classic = bui.app.classic + assert classic is not None plus = bui.app.plus assert plus is not None @@ -636,41 +637,7 @@ class PrivateGatherTab(GatherTab): and hostingstate.tickets_to_host_now != 0 and not havegoldpass ): - if not bui.app.ui_v1.use_toolbars: - - # Currently have no allow_token_purchases value like - # we had with tickets; just assuming we always allow. - if bool(True): - # if bui.app.classic.allow_ticket_purchases: - self._get_tokens_button = bui.buttonwidget( - parent=self._container, - position=( - self._c_width - 210 + 125, - self._c_height - 44, - ), - autoselect=True, - scale=0.6, - size=(120, 60), - textcolor=(1.0, 0.6, 0.0), - label=bui.charstr(bui.SpecialChar.TOKEN), - color=(0.65, 0.5, 0.8), - on_activate_call=self._on_get_tokens_press, - ) - else: - self._token_count_text = bui.textwidget( - parent=self._container, - scale=0.6, - position=( - self._c_width - 210 + 125, - self._c_height - 44, - ), - color=(1.0, 0.6, 0.0), - h_align='center', - v_align='center', - ) - - # Set initial token count. - self._update_currency_ui() + pass v = self._c_height - 90 if hostingstate.party_code is None: @@ -689,7 +656,7 @@ class PrivateGatherTab(GatherTab): ), ) - v -= 100 + v -= 90 if hostingstate.party_code is None: # We've got no current party running; show options to set # one up. @@ -718,12 +685,12 @@ class PrivateGatherTab(GatherTab): # If it appears we're coming back from playlist selection, # re-select our playlist button. - if bui.app.ui_v1.selecting_private_party_playlist: + if classic.selecting_private_party_playlist: bui.containerwidget( edit=self._container, selected_child=self._host_playlist_button, ) - bui.app.ui_v1.selecting_private_party_playlist = False + classic.selecting_private_party_playlist = False else: # We've got a current party; show its info. bui.textwidget( @@ -785,7 +752,7 @@ class PrivateGatherTab(GatherTab): autoselect=True, ) - v -= 120 + v -= 110 # Line above the main action button: @@ -951,6 +918,9 @@ class PrivateGatherTab(GatherTab): ) def _playlist_press(self) -> None: + if bool(True): + bui.screenmessage('UNDER CONSTRUCTION') + return assert self._host_playlist_button is not None self.window.playlist_select(origin_widget=self._host_playlist_button) diff --git a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py index d8b2f145..ca799188 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py @@ -584,7 +584,7 @@ class PublicGatherTab(GatherTab): parent=self._container, text=self._filter_value, size=(350, 45), - position=(290, v - 10), + position=(c_width * 0.5 - 150, v - 10), h_align='left', v_align='center', editable=True, @@ -596,7 +596,7 @@ class PublicGatherTab(GatherTab): text=filter_txt, parent=self._container, size=(0, 0), - position=(270, v + 13), + position=(c_width * 0.5 - 170, v + 13), maxwidth=150, scale=0.8, color=(0.5, 0.46, 0.5), @@ -609,7 +609,7 @@ class PublicGatherTab(GatherTab): text=bui.Lstr(resource='nameText'), parent=self._container, size=(0, 0), - position=(90, v - 8), + position=((c_width - sub_scroll_width) * 0.5 + 50, v - 8), maxwidth=60, scale=0.6, color=(0.5, 0.46, 0.5), @@ -621,7 +621,10 @@ class PublicGatherTab(GatherTab): text=bui.Lstr(resource='gatherWindow.partySizeText'), parent=self._container, size=(0, 0), - position=(755, v - 8), + position=( + c_width * 0.5 + sub_scroll_width * 0.5 - 110, + v - 8, + ), maxwidth=60, scale=0.6, color=(0.5, 0.46, 0.5), @@ -633,7 +636,10 @@ class PublicGatherTab(GatherTab): text=bui.Lstr(resource='gatherWindow.pingText'), parent=self._container, size=(0, 0), - position=(825, v - 8), + position=( + c_width * 0.5 + sub_scroll_width * 0.5 - 30, + v - 8, + ), maxwidth=60, scale=0.6, color=(0.5, 0.46, 0.5), @@ -811,6 +817,7 @@ class PublicGatherTab(GatherTab): bui.widget(edit=self._host_name_text, down_widget=btn2) bui.widget(edit=btn2, up_widget=self._host_name_text) bui.widget(edit=btn1, up_widget=self._host_name_text) + assert self._join_text is not None bui.widget(edit=self._join_text, down_widget=self._host_name_text) v -= 10 self._host_status_text = bui.textwidget( @@ -897,11 +904,6 @@ class PublicGatherTab(GatherTab): plus = bui.app.plus assert plus is not None - # Special case: if a party-queue window is up, don't do any of this - # (keeps things smoother). - # if bui.app.ui.have_party_queue_window: - # return - if self._sub_tab is SubTabType.JOIN: # Keep our filter-text up to date from the UI. text = self._filter_text diff --git a/src/assets/ba_data/python/bauiv1lib/gettickets.py b/src/assets/ba_data/python/bauiv1lib/gettickets.py deleted file mode 100644 index 3d40a4ac..00000000 --- a/src/assets/ba_data/python/bauiv1lib/gettickets.py +++ /dev/null @@ -1,881 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""UI functionality for purchasing/acquiring currency.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from efro.util import utc_now - -import bauiv1 as bui - -if TYPE_CHECKING: - from typing import Any - - -class GetTicketsWindow(bui.Window): - """Window for purchasing/acquiring classic tickets.""" - - def __init__( - self, - transition: str = 'in_right', - from_modal_store: bool = False, - modal: bool = False, - origin_widget: bui.Widget | None = None, - store_back_location: str | None = None, - ): - # pylint: disable=too-many-statements - # pylint: disable=too-many-locals - - plus = bui.app.plus - assert plus is not None - - bui.set_analytics_screen('Get Tickets Window') - - self._transitioning_out = False - self._store_back_location = store_back_location # ew. - - self._ad_button_greyed = False - self._smooth_update_timer: bui.AppTimer | None = None - self._ad_button = None - self._ad_label = None - self._ad_image = None - self._ad_time_text = None - - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - - assert bui.app.classic is not None - uiscale = bui.app.ui_v1.uiscale - self._width = 1000.0 if uiscale is bui.UIScale.SMALL else 800.0 - x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 - self._height = 480.0 - - self._modal = modal - self._from_modal_store = from_modal_store - self._r = 'getTicketsWindow' - - top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 - - super().__init__( - root_widget=bui.containerwidget( - size=(self._width, self._height + top_extra), - transition=transition, - scale_origin_stack_offset=scale_origin, - color=(0.4, 0.37, 0.55), - scale=( - 1.63 - if uiscale is bui.UIScale.SMALL - else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0 - ), - stack_offset=( - (0, -3) if uiscale is bui.UIScale.SMALL else (0, 0) - ), - ) - ) - - btn = bui.buttonwidget( - parent=self._root_widget, - position=(55 + x_inset, self._height - 79), - size=(140, 60), - scale=1.0, - autoselect=True, - label=bui.Lstr(resource='doneText' if modal else 'backText'), - button_type='regular' if modal else 'back', - on_activate_call=self._back, - ) - - bui.containerwidget(edit=self._root_widget, cancel_button=btn) - - bui.textwidget( - parent=self._root_widget, - position=(self._width * 0.5 - 15, self._height - 47), - size=(0, 0), - color=bui.app.ui_v1.title_color, - scale=1.2, - h_align='right', - v_align='center', - text=bui.Lstr(resource=f'{self._r}.titleText'), - # text='Testing really long text here blah blah', - maxwidth=260, - ) - - # Get Tokens button - bui.buttonwidget( - parent=self._root_widget, - position=(self._width * 0.5, self._height - 72), - color=(0.65, 0.5, 0.7), - textcolor=bui.app.ui_v1.title_color, - size=(190, 50), - autoselect=True, - label=bui.Lstr(resource='tokens.getTokensText'), - on_activate_call=self._get_tokens_press, - ) - - # 'New!' by tokens button - bui.textwidget( - parent=self._root_widget, - text=bui.Lstr(resource='newExclaimText'), - position=(self._width * 0.5 + 25, self._height - 32), - size=(0, 0), - color=(1, 1, 0, 1.0), - rotate=22, - shadow=1.0, - maxwidth=150, - h_align='center', - v_align='center', - scale=0.7, - ) - - if not modal: - bui.buttonwidget( - edit=btn, - button_type='backSmall', - size=(60, 60), - label=bui.charstr(bui.SpecialChar.BACK), - ) - - b_size = (220.0, 180.0) - v = self._height - b_size[1] - 80 - spacing = 1 - - self._ad_button = None - - def _add_button( - item: str, - position: tuple[float, float], - size: tuple[float, float], - label: bui.Lstr, - price: str | None = None, - tex_name: str | None = None, - tex_opacity: float = 1.0, - tex_scale: float = 1.0, - enabled: bool = True, - text_scale: float = 1.0, - ) -> bui.Widget: - btn2 = bui.buttonwidget( - parent=self._root_widget, - position=position, - button_type='square', - size=size, - label='', - autoselect=True, - color=None if enabled else (0.5, 0.5, 0.5), - on_activate_call=( - bui.Call(self._purchase, item) - if enabled - else self._disabled_press - ), - ) - txt = bui.textwidget( - parent=self._root_widget, - text=label, - position=( - position[0] + size[0] * 0.5, - position[1] + size[1] * 0.3, - ), - scale=text_scale, - maxwidth=size[0] * 0.75, - size=(0, 0), - h_align='center', - v_align='center', - draw_controller=btn2, - color=(0.7, 0.9, 0.7, 1.0 if enabled else 0.2), - ) - if price is not None and enabled: - bui.textwidget( - parent=self._root_widget, - text=price, - position=( - position[0] + size[0] * 0.5, - position[1] + size[1] * 0.17, - ), - scale=0.7, - maxwidth=size[0] * 0.75, - size=(0, 0), - h_align='center', - v_align='center', - draw_controller=btn2, - color=(0.4, 0.9, 0.4, 1.0), - ) - i = None - if tex_name is not None: - tex_size = 90.0 * tex_scale - i = bui.imagewidget( - parent=self._root_widget, - texture=bui.gettexture(tex_name), - position=( - position[0] + size[0] * 0.5 - tex_size * 0.5, - position[1] + size[1] * 0.66 - tex_size * 0.5, - ), - size=(tex_size, tex_size), - draw_controller=btn2, - opacity=tex_opacity * (1.0 if enabled else 0.25), - ) - if item == 'ad': - self._ad_button = btn2 - self._ad_label = txt - assert i is not None - self._ad_image = i - self._ad_time_text = bui.textwidget( - parent=self._root_widget, - text='1m 10s', - position=( - position[0] + size[0] * 0.5, - position[1] + size[1] * 0.5, - ), - scale=text_scale * 1.2, - maxwidth=size[0] * 0.85, - size=(0, 0), - h_align='center', - v_align='center', - draw_controller=btn2, - color=(0.4, 0.9, 0.4, 1.0), - ) - return btn2 - - rsrc = f'{self._r}.ticketsText' - - c2txt = bui.Lstr( - resource=rsrc, - subs=[ - ( - '${COUNT}', - str( - plus.get_v1_account_misc_read_val('tickets2Amount', 500) - ), - ) - ], - ) - c3txt = bui.Lstr( - resource=rsrc, - subs=[ - ( - '${COUNT}', - str( - plus.get_v1_account_misc_read_val( - 'tickets3Amount', 1500 - ) - ), - ) - ], - ) - c4txt = bui.Lstr( - resource=rsrc, - subs=[ - ( - '${COUNT}', - str( - plus.get_v1_account_misc_read_val( - 'tickets4Amount', 5000 - ) - ), - ) - ], - ) - c5txt = bui.Lstr( - resource=rsrc, - subs=[ - ( - '${COUNT}', - str( - plus.get_v1_account_misc_read_val( - 'tickets5Amount', 15000 - ) - ), - ) - ], - ) - - h = 110.0 - - # Enable buttons if we have prices. - tickets2_price = plus.get_price('tickets2') - tickets3_price = plus.get_price('tickets3') - tickets4_price = plus.get_price('tickets4') - tickets5_price = plus.get_price('tickets5') - - # TEMP - # tickets1_price = '$0.99' - # tickets2_price = '$4.99' - # tickets3_price = '$9.99' - # tickets4_price = '$19.99' - # tickets5_price = '$49.99' - - _add_button( - 'tickets2', - enabled=(tickets2_price is not None), - position=( - self._width * 0.5 - spacing * 1.5 - b_size[0] * 2.0 + h, - v, - ), - size=b_size, - label=c2txt, - price=tickets2_price, - tex_name='ticketsMore', - ) # 0.99-ish - _add_button( - 'tickets3', - enabled=(tickets3_price is not None), - position=( - self._width * 0.5 - spacing * 0.5 - b_size[0] * 1.0 + h, - v, - ), - size=b_size, - label=c3txt, - price=tickets3_price, - tex_name='ticketRoll', - ) # 4.99-ish - v -= b_size[1] - 5 - _add_button( - 'tickets4', - enabled=(tickets4_price is not None), - position=( - self._width * 0.5 - spacing * 1.5 - b_size[0] * 2.0 + h, - v, - ), - size=b_size, - label=c4txt, - price=tickets4_price, - tex_name='ticketRollBig', - tex_scale=1.2, - ) # 9.99-ish - _add_button( - 'tickets5', - enabled=(tickets5_price is not None), - position=( - self._width * 0.5 - spacing * 0.5 - b_size[0] * 1.0 + h, - v, - ), - size=b_size, - label=c5txt, - price=tickets5_price, - tex_name='ticketRolls', - tex_scale=1.2, - ) # 19.99-ish - - self._enable_ad_button = plus.has_video_ads() - h = self._width * 0.5 + 110.0 - v = self._height - b_size[1] - 115.0 - - if self._enable_ad_button: - h_offs = 35 - b_size_3 = (150, 120) - cdb = _add_button( - 'ad', - position=(h + h_offs, v), - size=b_size_3, - label=bui.Lstr( - resource=f'{self._r}.ticketsFromASponsorText', - subs=[ - ( - '${COUNT}', - str( - plus.get_v1_account_misc_read_val( - 'sponsorTickets', 5 - ) - ), - ) - ], - ), - tex_name='ticketsMore', - enabled=self._enable_ad_button, - tex_opacity=0.6, - tex_scale=0.7, - text_scale=0.7, - ) - bui.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7)) - - self._ad_free_text = bui.textwidget( - parent=self._root_widget, - text=bui.Lstr(resource=f'{self._r}.freeText'), - position=( - h + h_offs + b_size_3[0] * 0.5, - v + b_size_3[1] * 0.5 + 25, - ), - size=(0, 0), - color=(1, 1, 0, 1.0), - draw_controller=cdb, - rotate=15, - shadow=1.0, - maxwidth=150, - h_align='center', - v_align='center', - scale=1.0, - ) - v -= 125 - else: - v -= 20 - - if bool(True): - h_offs = 35 - b_size_3 = (150, 120) - cdb = _add_button( - 'app_invite', - position=(h + h_offs, v), - size=b_size_3, - label=bui.Lstr( - resource='gatherWindow.earnTicketsForRecommendingText', - subs=[ - ( - '${COUNT}', - str( - plus.get_v1_account_misc_read_val( - 'sponsorTickets', 5 - ) - ), - ) - ], - ), - tex_name='ticketsMore', - enabled=True, - tex_opacity=0.6, - tex_scale=0.7, - text_scale=0.7, - ) - bui.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7)) - - bui.textwidget( - parent=self._root_widget, - text=bui.Lstr(resource=f'{self._r}.freeText'), - position=( - h + h_offs + b_size_3[0] * 0.5, - v + b_size_3[1] * 0.5 + 25, - ), - size=(0, 0), - color=(1, 1, 0, 1.0), - draw_controller=cdb, - rotate=15, - shadow=1.0, - maxwidth=150, - h_align='center', - v_align='center', - scale=1.0, - ) - tc_y_offs = 0 - else: - tc_y_offs = 0 - - h = self._width - (185 + x_inset) - v = self._height - 105 + tc_y_offs - - txt1 = ( - bui.Lstr(resource=f'{self._r}.youHaveText') - .evaluate() - .partition('${COUNT}')[0] - .strip() - ) - txt2 = ( - bui.Lstr(resource=f'{self._r}.youHaveText') - .evaluate() - .rpartition('${COUNT}')[-1] - .strip() - ) - - bui.textwidget( - parent=self._root_widget, - text=txt1, - position=(h, v), - size=(0, 0), - color=(0.5, 0.5, 0.6), - maxwidth=200, - h_align='center', - v_align='center', - scale=0.8, - ) - v -= 30 - self._ticket_count_text = bui.textwidget( - parent=self._root_widget, - position=(h, v), - size=(0, 0), - color=(0.2, 1.0, 0.2), - maxwidth=200, - h_align='center', - v_align='center', - scale=1.6, - ) - v -= 30 - bui.textwidget( - parent=self._root_widget, - text=txt2, - position=(h, v), - size=(0, 0), - color=(0.5, 0.5, 0.6), - maxwidth=200, - h_align='center', - v_align='center', - scale=0.8, - ) - - self._ticking_sound: bui.Sound | None = None - self._smooth_ticket_count: float | None = None - self._ticket_count = 0 - self._update() - self._update_timer = bui.AppTimer( - 1.0, bui.WeakCall(self._update), repeat=True - ) - self._smooth_increase_speed = 1.0 - - def __del__(self) -> None: - if self._ticking_sound is not None: - self._ticking_sound.stop() - self._ticking_sound = None - - def _smooth_update(self) -> None: - if not self._ticket_count_text: - self._smooth_update_timer = None - return - - finished = False - - # If we're going down, do it immediately. - assert self._smooth_ticket_count is not None - if int(self._smooth_ticket_count) >= self._ticket_count: - self._smooth_ticket_count = float(self._ticket_count) - finished = True - else: - # We're going up; start a sound if need be. - self._smooth_ticket_count = min( - self._smooth_ticket_count + 1.0 * self._smooth_increase_speed, - self._ticket_count, - ) - if int(self._smooth_ticket_count) >= self._ticket_count: - finished = True - self._smooth_ticket_count = float(self._ticket_count) - elif self._ticking_sound is None: - self._ticking_sound = bui.getsound('scoreIncrease') - self._ticking_sound.play() - - bui.textwidget( - edit=self._ticket_count_text, - text=str(int(self._smooth_ticket_count)), - ) - - # If we've reached the target, kill the timer/sound/etc. - if finished: - self._smooth_update_timer = None - if self._ticking_sound is not None: - self._ticking_sound.stop() - self._ticking_sound = None - bui.getsound('cashRegister2').play() - - def _update(self) -> None: - import datetime - - plus = bui.app.plus - assert plus is not None - - # If we somehow get signed out, just die. - if plus.get_v1_account_state() != 'signed_in': - self._back() - return - - self._ticket_count = plus.get_v1_account_ticket_count() - - # Update our incentivized ad button depending on whether ads are - # available. - if self._ad_button is not None: - next_reward_ad_time = plus.get_v1_account_misc_read_val_2( - 'nextRewardAdTime', None - ) - if next_reward_ad_time is not None: - next_reward_ad_time = datetime.datetime.fromtimestamp( - next_reward_ad_time, datetime.UTC - ) - now = utc_now() - if plus.have_incentivized_ad() and ( - next_reward_ad_time is None or next_reward_ad_time <= now - ): - self._ad_button_greyed = False - bui.buttonwidget(edit=self._ad_button, color=(0.65, 0.5, 0.7)) - bui.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 1.0)) - bui.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 1)) - bui.imagewidget(edit=self._ad_image, opacity=0.6) - bui.textwidget(edit=self._ad_time_text, text='') - else: - self._ad_button_greyed = True - bui.buttonwidget(edit=self._ad_button, color=(0.5, 0.5, 0.5)) - bui.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 0.2)) - bui.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 0.2)) - bui.imagewidget(edit=self._ad_image, opacity=0.6 * 0.25) - sval: str | bui.Lstr - if ( - next_reward_ad_time is not None - and next_reward_ad_time > now - ): - sval = bui.timestring( - (next_reward_ad_time - now).total_seconds(), centi=False - ) - else: - sval = '' - bui.textwidget(edit=self._ad_time_text, text=sval) - - # If this is our first update, assign immediately; otherwise kick - # off a smooth transition if the value has changed. - if self._smooth_ticket_count is None: - self._smooth_ticket_count = float(self._ticket_count) - self._smooth_update() # will set the text widget - - elif ( - self._ticket_count != int(self._smooth_ticket_count) - and self._smooth_update_timer is None - ): - self._smooth_update_timer = bui.AppTimer( - 0.05, bui.WeakCall(self._smooth_update), repeat=True - ) - diff = abs(float(self._ticket_count) - self._smooth_ticket_count) - self._smooth_increase_speed = ( - diff / 100.0 - if diff >= 5000 - else ( - diff / 50.0 - if diff >= 1500 - else diff / 30.0 if diff >= 500 else diff / 15.0 - ) - ) - - def _disabled_press(self) -> None: - plus = bui.app.plus - assert plus is not None - - # If we're on a platform without purchases, inform the user they - # can link their accounts and buy stuff elsewhere. - app = bui.app - assert app.classic is not None - if ( - app.env.test - or ( - app.classic.platform == 'android' - and app.classic.subplatform in ['oculus', 'cardboard'] - ) - ) and plus.get_v1_account_misc_read_val('allowAccountLinking2', False): - bui.screenmessage( - bui.Lstr(resource=f'{self._r}.unavailableLinkAccountText'), - color=(1, 0.5, 0), - ) - else: - bui.screenmessage( - bui.Lstr(resource=f'{self._r}.unavailableText'), - color=(1, 0.5, 0), - ) - bui.getsound('error').play() - - def _purchase(self, item: str) -> None: - from bauiv1lib import account - from bauiv1lib import appinvite - - plus = bui.app.plus - assert plus is not None - - if bui.app.classic is None: - raise RuntimeError('This requires classic support.') - - if item == 'app_invite': - if plus.get_v1_account_state() != 'signed_in': - account.show_sign_in_prompt() - return - appinvite.handle_app_invites_press() - return - - # Here we ping the server to ask if it's valid for us to - # purchase this.. (better to fail now than after we've paid - # locally). - app = bui.app - assert app.classic is not None - bui.app.classic.master_server_v1_get( - 'bsAccountPurchaseCheck', - { - 'item': item, - 'platform': app.classic.platform, - 'subplatform': app.classic.subplatform, - 'version': app.env.engine_version, - 'buildNumber': app.env.engine_build_number, - }, - callback=bui.WeakCall(self._purchase_check_result, item), - ) - - def _purchase_check_result( - self, item: str, result: dict[str, Any] | None - ) -> None: - if result is None: - bui.getsound('error').play() - bui.screenmessage( - bui.Lstr(resource='internal.unavailableNoConnectionText'), - color=(1, 0, 0), - ) - else: - if result['allow']: - self._do_purchase(item) - else: - if result['reason'] == 'versionTooOld': - bui.getsound('error').play() - bui.screenmessage( - bui.Lstr(resource='getTicketsWindow.versionTooOldText'), - color=(1, 0, 0), - ) - else: - bui.getsound('error').play() - bui.screenmessage( - bui.Lstr(resource='getTicketsWindow.unavailableText'), - color=(1, 0, 0), - ) - - # Actually start the purchase locally. - def _do_purchase(self, item: str) -> None: - plus = bui.app.plus - assert plus is not None - - if item == 'ad': - import datetime - - # If ads are disabled until some time, error. - next_reward_ad_time = plus.get_v1_account_misc_read_val_2( - 'nextRewardAdTime', None - ) - if next_reward_ad_time is not None: - next_reward_ad_time = datetime.datetime.fromtimestamp( - next_reward_ad_time, datetime.UTC - ) - now = utc_now() - if ( - next_reward_ad_time is not None and next_reward_ad_time > now - ) or self._ad_button_greyed: - bui.getsound('error').play() - bui.screenmessage( - bui.Lstr( - resource='getTicketsWindow.unavailableTemporarilyText' - ), - color=(1, 0, 0), - ) - elif self._enable_ad_button: - assert bui.app.classic is not None - bui.app.classic.ads.show_ad('tickets') - else: - plus.purchase(item) - - def _get_tokens_press(self) -> None: - from functools import partial - - from bauiv1lib.gettokens import GetTokensWindow - - # No-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - if self._transitioning_out: - return - - bui.containerwidget(edit=self._root_widget, transition='out_left') - - # Note: Make sure we don't pass anything here that would - # capture 'self'. (a lambda would implicitly do this by capturing - # the stack frame). - restorecall = partial( - _restore_get_tickets_window, - self._modal, - self._from_modal_store, - self._store_back_location, - ) - - window = GetTokensWindow( - transition='in_right', - restore_previous_call=restorecall, - ).get_root_widget() - if not self._modal and not self._from_modal_store: - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - window, from_window=self._root_widget - ) - self._transitioning_out = True - - def _back(self) -> None: - from bauiv1lib.store import browser - - # No-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - if self._transitioning_out: - return - - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - if not self._modal: - window = browser.StoreBrowserWindow( - transition='in_left', - modal=self._from_modal_store, - back_location=self._store_back_location, - ).get_root_widget() - if not self._from_modal_store: - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - window, from_window=self._root_widget - ) - self._transitioning_out = True - - -# A call we can bundle up and pass to windows we open; allows them to -# get back to us without having to explicitly know about us. -def _restore_get_tickets_window( - modal: bool, - from_modal_store: bool, - store_back_location: str | None, - from_window: bui.Widget, -) -> None: - restored = GetTicketsWindow( - transition='in_left', - modal=modal, - from_modal_store=from_modal_store, - store_back_location=store_back_location, - ) - if not modal and not from_modal_store: - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - restored.get_root_widget(), from_window=from_window - ) - - -def show_get_tickets_prompt() -> None: - """Show a 'not enough tickets' prompt with an option to purchase more. - - Note that the purchase option may not always be available - depending on the build of the game. - """ - from bauiv1lib.confirm import ConfirmWindow - - assert bui.app.classic is not None - - if bui.app.classic.allow_ticket_purchases: - ConfirmWindow( - bui.Lstr( - translate=( - 'serverResponses', - 'You don\'t have enough tickets for this!', - ) - ), - lambda: GetTicketsWindow(modal=True), - ok_text=bui.Lstr(resource='getTicketsWindow.titleText'), - width=460, - height=130, - ) - else: - ConfirmWindow( - bui.Lstr( - translate=( - 'serverResponses', - 'You don\'t have enough tickets for this!', - ) - ), - cancel_button=False, - width=460, - height=130, - ) diff --git a/src/assets/ba_data/python/bauiv1lib/gettokens.py b/src/assets/ba_data/python/bauiv1lib/gettokens.py index 22bff444..0c3baeb8 100644 --- a/src/assets/ba_data/python/bauiv1lib/gettokens.py +++ b/src/assets/ba_data/python/bauiv1lib/gettokens.py @@ -325,56 +325,74 @@ class GetTokensWindow(bui.Window): uiscale = bui.app.ui_v1.uiscale self._width = 1000.0 if uiscale is bui.UIScale.SMALL else 800.0 - self._x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 - self._height = 480.0 + self._x_inset = 25.0 if uiscale is bui.UIScale.SMALL else 0.0 + self._height = 550 if uiscale is bui.UIScale.SMALL else 480.0 + self._y_offset = -60 if uiscale is bui.UIScale.SMALL else 0 self._r = 'getTokensWindow' - top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 - super().__init__( root_widget=bui.containerwidget( - size=(self._width, self._height + top_extra), + size=(self._width, self._height), transition=transition, scale_origin_stack_offset=scale_origin, color=(0.3, 0.23, 0.36), scale=( - 1.63 + 1.5 if uiscale is bui.UIScale.SMALL else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, -3) if uiscale is bui.UIScale.SMALL else (0, 0) ), + # toolbar_visibility='menu_minimal', + toolbar_visibility='get_tokens', ) ) - self._back_button = btn = bui.buttonwidget( - parent=self._root_widget, - position=(45 + self._x_inset, self._height - 80), - size=( - (140, 60) if self._restore_previous_call is None else (60, 60) - ), - scale=1.0, - autoselect=True, - label=( - bui.Lstr(resource='doneText') - if self._restore_previous_call is None - else bui.charstr(bui.SpecialChar.BACK) - ), - button_type=( - 'regular' - if self._restore_previous_call is None - else 'backSmall' - ), - on_activate_call=self._back, - ) - - bui.containerwidget(edit=self._root_widget, cancel_button=btn) + if uiscale is bui.UIScale.SMALL: + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self._back + ) + self._back_button = bui.get_special_widget('back_button') + else: + self._back_button = bui.buttonwidget( + parent=self._root_widget, + position=( + 55 + self._x_inset, + self._height - 80 + self._y_offset, + ), + size=( + (140, 60) + if self._restore_previous_call is None + else (60, 60) + ), + scale=1.0, + autoselect=True, + label=( + bui.Lstr(resource='doneText') + if self._restore_previous_call is None + else bui.charstr(bui.SpecialChar.BACK) + ), + button_type=( + 'regular' + if self._restore_previous_call is None + else 'backSmall' + ), + on_activate_call=self._back, + ) + # if uiscale is bui.UIScale.SMALL: + # bui.widget( + # edit=self._back_button, + # up_widget=bui.get_special_widget('tokens_meter'), + # ) + bui.containerwidget( + edit=self._root_widget, cancel_button=self._back_button + ) self._title_text = bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height - 47), + position=(self._width * 0.5, self._height - 42 + self._y_offset), size=(0, 0), color=self._textcolor, flatness=0.0, @@ -403,12 +421,12 @@ class GetTokensWindow(bui.Window): self._status_text, ] - self._token_count_widget: bui.Widget | None = None - self._smooth_update_timer: bui.AppTimer | None = None - self._smooth_token_count: float | None = None - self._token_count: int = 0 - self._smooth_increase_speed = 1.0 - self._ticking_sound: bui.Sound | None = None + # self._token_count_widget: bui.Widget | None = None + # self._smooth_update_timer: bui.AppTimer | None = None + # self._smooth_token_count: float | None = None + # self._token_count: int = 0 + # self._smooth_increase_speed = 1.0 + # self._ticking_sound: bui.Sound | None = None # Get all textures used by our buttons preloading so hopefully # they'll be in place by the time we show them. @@ -423,10 +441,10 @@ class GetTokensWindow(bui.Window): ) self._update() - def __del__(self) -> None: - if self._ticking_sound is not None: - self._ticking_sound.stop() - self._ticking_sound = None + # def __del__(self) -> None: + # if self._ticking_sound is not None: + # self._ticking_sound.stop() + # self._ticking_sound = None def _update(self) -> None: # No-op if our underlying widget is dead or on its way out. @@ -475,7 +493,7 @@ class GetTokensWindow(bui.Window): return # Ok, state is changing. Start by resetting to a blank slate. - self._token_count_widget = None + # self._token_count_widget = None for widget in self._root_widget.get_children(): if widget not in self._core_widgets: widget.delete() @@ -527,6 +545,8 @@ class GetTokensWindow(bui.Window): # pylint: disable=too-many-locals plus = bui.app.plus + uiscale = bui.app.ui_v1.uiscale + bui.textwidget(edit=self._status_text, text='') xinset = 40 @@ -540,17 +560,23 @@ class GetTokensWindow(bui.Window): # We currently don't handle the zero-button case. assert self._buttondefs - total_button_width = sum( - b.width + b.prepad for b in self._buttondefs - ) + buttonpadding * (len(self._buttondefs) - 1) + sidepad = 10.0 + total_button_width = ( + sum(b.width + b.prepad for b in self._buttondefs) + + buttonpadding * (len(self._buttondefs) - 1) + + 2 * sidepad + ) h_scroll = bui.hscrollwidget( parent=self._root_widget, size=(scrollwidth, scrollheight), - position=(self._x_inset + xinset, 45), + position=( + self._x_inset + xinset, + self._height - 415 + self._y_offset, + ), claims_left_right=True, highlight=False, - border_opacity=0.25, + border_opacity=0.3 if uiscale is bui.UIScale.SMALL else 1.0, ) subcontainer = bui.containerwidget( parent=h_scroll, @@ -561,7 +587,10 @@ class GetTokensWindow(bui.Window): parent=self._root_widget, autoselect=True, label=bui.Lstr(resource='learnMoreText'), - position=(self._width * 0.5 - 75, self._height * 0.703), + position=( + self._width * 0.5 - 75, + self._height - 125 + self._y_offset, + ), size=(180, 43), scale=0.8, color=(0.4, 0.25, 0.5), @@ -570,8 +599,19 @@ class GetTokensWindow(bui.Window): self._on_learn_more_press, response.token_info_url ), ) + if uiscale is bui.UIScale.SMALL: + bui.widget( + edit=tinfobtn, + left_widget=bui.get_special_widget('back_button'), + up_widget=bui.get_special_widget('back_button'), + ) - x = 0.0 + bui.widget( + edit=tinfobtn, + right_widget=bui.get_special_widget('tokens_meter'), + ) + + x = sidepad bwidgets: list[bui.Widget] = [] for i, buttondef in enumerate(self._buttondefs): @@ -594,6 +634,10 @@ class GetTokensWindow(bui.Window): ), ) bwidgets.append(btn) + + if i == 0: + bui.widget(edit=btn, left_widget=self._back_button) + for imgdef in buttondef.imgdefs: _img = bui.imagewidget( parent=subcontainer, @@ -645,7 +689,7 @@ class GetTokensWindow(bui.Window): _tinfotxt = bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height * 0.812), + position=(self._width * 0.5, self._height - 70 + self._y_offset), color=self._textcolor, shadow=1.0, scale=0.7, @@ -654,29 +698,35 @@ class GetTokensWindow(bui.Window): v_align='center', text=bui.Lstr(resource='tokens.shinyNewCurrencyText'), ) - self._token_count_widget = bui.textwidget( - parent=self._root_widget, - position=(self._width - self._x_inset - 120.0, self._height - 48), - color=(2.0, 0.7, 0.0), - shadow=1.0, - flatness=0.0, - size=(0, 0), - h_align='left', - v_align='center', - text='', - ) - self._token_count = response.tokens - self._smooth_token_count = float(self._token_count) - self._smooth_update() # will set the text widget. + # self._token_count_widget = bui.textwidget( + # parent=self._root_widget, + # position=( + # self._width - self._x_inset - 120.0, + # self._height - 48 + self._y_offset, + # ), + # color=(2.0, 0.7, 0.0), + # shadow=1.0, + # flatness=0.0, + # size=(0, 0), + # h_align='left', + # v_align='center', + # text='', + # ) + # self._token_count = response.tokens + # self._smooth_token_count = float(self._token_count) + # self._smooth_update() # will set the text widget. - _tlabeltxt = bui.textwidget( - parent=self._root_widget, - position=(self._width - self._x_inset - 123.0, self._height - 48), - size=(0, 0), - h_align='right', - v_align='center', - text=bui.charstr(bui.SpecialChar.TOKEN), - ) + # _tlabeltxt = bui.textwidget( + # parent=self._root_widget, + # position=( + # self._width - self._x_inset - 123.0, + # self._height - 48 + self._y_offset, + # ), + # size=(0, 0), + # h_align='right', + # v_align='center', + # text=bui.charstr(bui.SpecialChar.TOKEN), + # ) def _purchase_press(self, itemid: str) -> None: plus = bui.app.plus @@ -700,70 +750,70 @@ class GetTokensWindow(bui.Window): def _update_store_state(self) -> None: """Called to make minor updates to an already shown store.""" - assert self._token_count_widget is not None + # assert self._token_count_widget is not None assert self._last_query_response is not None - self._token_count = self._last_query_response.tokens + # self._token_count = self._last_query_response.tokens # Kick off new smooth update if need be. - assert self._smooth_token_count is not None - if ( - self._token_count != int(self._smooth_token_count) - and self._smooth_update_timer is None - ): - self._smooth_update_timer = bui.AppTimer( - 0.05, bui.WeakCall(self._smooth_update), repeat=True - ) - diff = abs(float(self._token_count) - self._smooth_token_count) - self._smooth_increase_speed = ( - diff / 100.0 - if diff >= 5000 - else ( - diff / 50.0 - if diff >= 1500 - else diff / 30.0 if diff >= 500 else diff / 15.0 - ) - ) + # assert self._smooth_token_count is not None + # if ( + # self._token_count != int(self._smooth_token_count) + # and self._smooth_update_timer is None + # ): + # self._smooth_update_timer = bui.AppTimer( + # 0.05, bui.WeakCall(self._smooth_update), repeat=True + # ) + # diff = abs(float(self._token_count) - self._smooth_token_count) + # self._smooth_increase_speed = ( + # diff / 100.0 + # if diff >= 5000 + # else ( + # diff / 50.0 + # if diff >= 1500 + # else diff / 30.0 if diff >= 500 else diff / 15.0 + # ) + # ) - def _smooth_update(self) -> None: + # def _smooth_update(self) -> None: - # Stop if the count widget disappears. - if not self._token_count_widget: - self._smooth_update_timer = None - return + # # Stop if the count widget disappears. + # if not self._token_count_widget: + # self._smooth_update_timer = None + # return - finished = False + # finished = False - # If we're going down, do it immediately. - assert self._smooth_token_count is not None - if int(self._smooth_token_count) >= self._token_count: - self._smooth_token_count = float(self._token_count) - finished = True - else: - # We're going up; start a sound if need be. - self._smooth_token_count = min( - self._smooth_token_count + 1.0 * self._smooth_increase_speed, - self._token_count, - ) - if int(self._smooth_token_count) >= self._token_count: - finished = True - self._smooth_token_count = float(self._token_count) - elif self._ticking_sound is None: - self._ticking_sound = bui.getsound('scoreIncrease') - self._ticking_sound.play() + # # If we're going down, do it immediately. + # assert self._smooth_token_count is not None + # if int(self._smooth_token_count) >= self._token_count: + # self._smooth_token_count = float(self._token_count) + # finished = True + # else: + # # We're going up; start a sound if need be. + # self._smooth_token_count = min( + # self._smooth_token_count + 1.0 * self._smooth_increase_speed, + # self._token_count, + # ) + # if int(self._smooth_token_count) >= self._token_count: + # finished = True + # self._smooth_token_count = float(self._token_count) + # elif self._ticking_sound is None: + # self._ticking_sound = bui.getsound('scoreIncrease') + # self._ticking_sound.play() - bui.textwidget( - edit=self._token_count_widget, - text=str(int(self._smooth_token_count)), - ) + # bui.textwidget( + # edit=self._token_count_widget, + # text=str(int(self._smooth_token_count)), + # ) - # If we've reached the target, kill the timer/sound/etc. - if finished: - self._smooth_update_timer = None - if self._ticking_sound is not None: - self._ticking_sound.stop() - self._ticking_sound = None - bui.getsound('cashRegister2').play() + # # If we've reached the target, kill the timer/sound/etc. + # if finished: + # self._smooth_update_timer = None + # if self._ticking_sound is not None: + # self._ticking_sound.stop() + # self._ticking_sound = None + # bui.getsound('cashRegister2').play() def _back(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/helpui.py b/src/assets/ba_data/python/bauiv1lib/helpui.py index 887f0050..ea4e8722 100644 --- a/src/assets/ba_data/python/bauiv1lib/helpui.py +++ b/src/assets/ba_data/python/bauiv1lib/helpui.py @@ -4,40 +4,33 @@ from __future__ import annotations +from typing import override + import bauiv1 as bui -class HelpWindow(bui.Window): +class HelpWindow(bui.MainWindow): """A window providing help on how to play.""" def __init__( - self, main_menu: bool = False, origin_widget: bui.Widget | None = None + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals bui.set_analytics_screen('Help Window') - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - transition = 'in_right' - self._r = 'helpWindow' getres = bui.app.lang.get_resource - self._main_menu = main_menu + # self._main_menu = main_menu assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale width = 1050 if uiscale is bui.UIScale.SMALL else 750 - x_offs = 150 if uiscale is bui.UIScale.SMALL else 0 + x_offs = 70 if uiscale is bui.UIScale.SMALL else 0 height = ( 460 if uiscale is bui.UIScale.SMALL @@ -47,20 +40,24 @@ class HelpWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(width, height), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, - scale=( - 1.77 + toolbar_visibility=( + 'menu_minimal' if uiscale is bui.UIScale.SMALL - else 1.25 if uiscale is bui.UIScale.MEDIUM else 1.0 + else 'menu_full' + ), + scale=( + 1.55 + if uiscale is bui.UIScale.SMALL + else 1.15 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( - (0, -30) + (0, -24) if uiscale is bui.UIScale.SMALL else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) bui.textwidget( @@ -87,20 +84,19 @@ class HelpWindow(bui.Window): capture_arrows=True, ) - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=self._scrollwidget, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=self._scrollwidget, + right_widget=bui.get_special_widget('squad_button'), + ) bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) # ugly: create this last so it gets first dibs at touch events (since # we have it close to the scroll widget) - if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars: + if uiscale is bui.UIScale.SMALL: bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._close + edit=self._root_widget, on_cancel_call=self.main_window_back ) bui.widget( edit=self._scrollwidget, @@ -109,33 +105,18 @@ class HelpWindow(bui.Window): else: btn = bui.buttonwidget( parent=self._root_widget, - position=( - x_offs + (40 + 0 if uiscale is bui.UIScale.SMALL else 70), - height - (59 if uiscale is bui.UIScale.SMALL else 50), - ), - size=(140, 60), - scale=0.7 if uiscale is bui.UIScale.SMALL else 0.8, - label=( - bui.Lstr(resource='backText') - if self._main_menu - else 'Close' - ), - button_type='back' if self._main_menu else None, + position=(x_offs + 50, height - 55), + size=(60, 55), + scale=0.8, + label=bui.charstr(bui.SpecialChar.BACK), + button_type='backSmall', extra_touch_border_scale=2.0, autoselect=True, - on_activate_call=self._close, + on_activate_call=self.main_window_back, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) - if self._main_menu: - bui.buttonwidget( - edit=btn, - button_type='backSmall', - size=(60, 55), - label=bui.charstr(bui.SpecialChar.BACK), - ) - - self._sub_width = 660 + self._sub_width = 810 if uiscale is bui.UIScale.SMALL else 660 self._sub_height = ( 1590 + bui.app.lang.get_resource(f'{self._r}.someDaysExtraSpace') @@ -639,20 +620,12 @@ class HelpWindow(bui.Window): res_scale=0.5, ) - def _close(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.mainmenu import MainMenuWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - if self._main_menu: - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget ) + ) diff --git a/src/assets/ba_data/python/bauiv1lib/inbox.py b/src/assets/ba_data/python/bauiv1lib/inbox.py new file mode 100644 index 00000000..ee4b4108 --- /dev/null +++ b/src/assets/ba_data/python/bauiv1lib/inbox.py @@ -0,0 +1,119 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a popup window to view achievements.""" + +from __future__ import annotations + +from typing import override + +from bauiv1lib.popup import PopupWindow +import bauiv1 as bui + + +class InboxWindow(PopupWindow): + """Popup window to show account messages.""" + + def __init__( + self, position: tuple[float, float], scale: float | None = None + ): + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + if scale is None: + scale = ( + 2.3 + if uiscale is bui.UIScale.SMALL + else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 + ) + self._transitioning_out = False + self._width = 450 + self._height = ( + 300 + if uiscale is bui.UIScale.SMALL + else 370 if uiscale is bui.UIScale.MEDIUM else 450 + ) + bg_color = (0.5, 0.4, 0.6) + + # creates our _root_widget + super().__init__( + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=bg_color, + edge_buffer_scale=4.0, # Try to keep button unobscured. + ) + + self._cancel_button = bui.buttonwidget( + parent=self.root_widget, + position=(50, self._height - 30), + size=(50, 50), + scale=0.5, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=bui.gettexture('crossOut'), + iconscale=1.2, + ) + + self._title_text = bui.textwidget( + parent=self.root_widget, + position=(self._width * 0.5, self._height - 20), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.6, + text='INBOX (UNDER CONSTRUCTION)', + maxwidth=200, + color=bui.app.ui_v1.title_color, + ) + + self._scrollwidget = bui.scrollwidget( + parent=self.root_widget, + size=(self._width - 60, self._height - 70), + position=(30, 30), + capture_arrows=True, + simple_culling_v=10, + ) + bui.widget(edit=self._scrollwidget, autoselect=True) + + bui.containerwidget( + edit=self.root_widget, cancel_button=self._cancel_button + ) + + entries: list[str] = [] + incr = 20 + sub_width = self._width - 90 + sub_height = 40 + len(entries) * incr + + self._subcontainer = bui.containerwidget( + parent=self._scrollwidget, + size=(sub_width, sub_height), + background=False, + ) + + for i, entry in enumerate(entries): + bui.textwidget( + parent=self._subcontainer, + position=(sub_width * 0.08 - 5, sub_height - 20 - incr * i), + maxwidth=20, + scale=0.5, + flatness=1.0, + shadow=0.0, + text=entry, + size=(0, 0), + h_align='right', + v_align='center', + ) + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + bui.containerwidget(edit=self.root_widget, transition='out_scale') + + @override + def on_popup_cancel(self) -> None: + bui.getsound('swish').play() + self._transition_out() diff --git a/src/assets/ba_data/python/bauiv1lib/ingamemenu.py b/src/assets/ba_data/python/bauiv1lib/ingamemenu.py new file mode 100644 index 00000000..abb04027 --- /dev/null +++ b/src/assets/ba_data/python/bauiv1lib/ingamemenu.py @@ -0,0 +1,590 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Implements the in-gmae menu window.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, override +import logging + +import bauiv1 as bui +import bascenev1 as bs + +if TYPE_CHECKING: + from typing import Any, Callable + + +class InGameMenuWindow(bui.MainWindow): + """The menu that can be invoked while in a game.""" + + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): + + # Make a vanilla container; we'll modify it to our needs in + # refresh. + super().__init__( + root_widget=bui.containerwidget( + toolbar_visibility=('menu_in_game') + ), + transition=transition, + origin_widget=origin_widget, + ) + + # Grab this stuff in case it changes. + self._is_demo = bui.app.env.demo + self._is_arcade = bui.app.env.arcade + + self._p_index = 0 + self._use_autoselect = True + self._button_width = 200.0 + self._button_height = 45.0 + self._width = 100.0 + self._height = 100.0 + + self._refresh() + + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + def _refresh(self) -> None: + + # Clear everything that was there. + children = self._root_widget.get_children() + for child in children: + child.delete() + + self._r = 'mainMenu' + + self._input_device = input_device = bs.get_ui_input_device() + + # Are we connected to a local player? + self._input_player = input_device.player if input_device else None + + # Are we connected to a remote player?. + self._connected_to_remote_player = ( + input_device.is_attached_to_player() + if (input_device and self._input_player is None) + else False + ) + + positions: list[tuple[float, float, float]] = [] + self._p_index = 0 + + self._refresh_in_game(positions) + + h, v, scale = positions[self._p_index] + self._p_index += 1 + + # If we're in a replay, we have a 'Leave Replay' button. + if bs.is_in_replay(): + bui.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width * 0.5 * scale, v), + scale=scale, + size=(self._button_width, self._button_height), + autoselect=self._use_autoselect, + label=bui.Lstr(resource='replayEndText'), + on_activate_call=self._confirm_end_replay, + ) + elif bs.get_foreground_host_session() is not None: + bui.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width * 0.5 * scale, v), + scale=scale, + size=(self._button_width, self._button_height), + autoselect=self._use_autoselect, + label=bui.Lstr( + resource=self._r + + ( + '.endTestText' + if self._is_benchmark() + else '.endGameText' + ) + ), + on_activate_call=( + self._confirm_end_test + if self._is_benchmark() + else self._confirm_end_game + ), + ) + else: + # Assume we're in a client-session. + bui.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width * 0.5 * scale, v), + scale=scale, + size=(self._button_width, self._button_height), + autoselect=self._use_autoselect, + label=bui.Lstr(resource=f'{self._r}.leavePartyText'), + on_activate_call=self._confirm_leave_party, + ) + + # Add speed-up/slow-down buttons for replays. Ideally this + # should be part of a fading-out playback bar like most media + # players but this works for now. + if bs.is_in_replay(): + b_size = 50.0 + b_buffer_1 = 50.0 + b_buffer_2 = 10.0 + t_scale = 0.75 + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + if uiscale is bui.UIScale.SMALL: + b_size *= 0.6 + b_buffer_1 *= 0.8 + b_buffer_2 *= 1.0 + v_offs = -40 + t_scale = 0.5 + elif uiscale is bui.UIScale.MEDIUM: + v_offs = -70 + else: + v_offs = -100 + self._replay_speed_text = bui.textwidget( + parent=self._root_widget, + text=bui.Lstr( + resource='watchWindow.playbackSpeedText', + subs=[('${SPEED}', str(1.23))], + ), + position=(h, v + v_offs + 15 * t_scale), + h_align='center', + v_align='center', + size=(0, 0), + scale=t_scale, + ) + + # Update to current value. + self._change_replay_speed(0) + + # Keep updating in a timer in case it gets changed elsewhere. + self._change_replay_speed_timer = bui.AppTimer( + 0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True + ) + btn = bui.buttonwidget( + parent=self._root_widget, + position=( + h - b_size - b_buffer_1, + v - b_size - b_buffer_2 + v_offs, + ), + button_type='square', + size=(b_size, b_size), + label='', + autoselect=True, + on_activate_call=bui.Call(self._change_replay_speed, -1), + ) + bui.textwidget( + parent=self._root_widget, + draw_controller=btn, + text='-', + position=( + h - b_size * 0.5 - b_buffer_1, + v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, + ), + h_align='center', + v_align='center', + size=(0, 0), + scale=3.0 * t_scale, + ) + btn = bui.buttonwidget( + parent=self._root_widget, + position=(h + b_buffer_1, v - b_size - b_buffer_2 + v_offs), + button_type='square', + size=(b_size, b_size), + label='', + autoselect=True, + on_activate_call=bui.Call(self._change_replay_speed, 1), + ) + bui.textwidget( + parent=self._root_widget, + draw_controller=btn, + text='+', + position=( + h + b_size * 0.5 + b_buffer_1, + v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, + ), + h_align='center', + v_align='center', + size=(0, 0), + scale=3.0 * t_scale, + ) + self._pause_resume_button = btn = bui.buttonwidget( + parent=self._root_widget, + position=(h - b_size * 0.5, v - b_size - b_buffer_2 + v_offs), + button_type='square', + size=(b_size, b_size), + label=bui.charstr( + bui.SpecialChar.PLAY_BUTTON + if bs.is_replay_paused() + else bui.SpecialChar.PAUSE_BUTTON + ), + autoselect=True, + on_activate_call=bui.Call(self._pause_or_resume_replay), + ) + btn = bui.buttonwidget( + parent=self._root_widget, + position=( + h - b_size * 1.5 - b_buffer_1 * 2, + v - b_size - b_buffer_2 + v_offs, + ), + button_type='square', + size=(b_size, b_size), + label='', + autoselect=True, + on_activate_call=bui.WeakCall(self._rewind_replay), + ) + bui.textwidget( + parent=self._root_widget, + draw_controller=btn, + # text='<<', + text=bui.charstr(bui.SpecialChar.REWIND_BUTTON), + position=( + h - b_size - b_buffer_1 * 2, + v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, + ), + h_align='center', + v_align='center', + size=(0, 0), + scale=2.0 * t_scale, + ) + btn = bui.buttonwidget( + parent=self._root_widget, + position=( + h + b_size * 0.5 + b_buffer_1 * 2, + v - b_size - b_buffer_2 + v_offs, + ), + button_type='square', + size=(b_size, b_size), + label='', + autoselect=True, + on_activate_call=bui.WeakCall(self._forward_replay), + ) + bui.textwidget( + parent=self._root_widget, + draw_controller=btn, + # text='>>', + text=bui.charstr(bui.SpecialChar.FAST_FORWARD_BUTTON), + position=( + h + b_size + b_buffer_1 * 2, + v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, + ), + h_align='center', + v_align='center', + size=(0, 0), + scale=2.0 * t_scale, + ) + + def _rewind_replay(self) -> None: + bs.seek_replay(-2 * pow(2, bs.get_replay_speed_exponent())) + + def _forward_replay(self) -> None: + bs.seek_replay(2 * pow(2, bs.get_replay_speed_exponent())) + + def _refresh_in_game( + self, positions: list[tuple[float, float, float]] + ) -> tuple[float, float, float]: + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + assert bui.app.classic is not None + custom_menu_entries: list[dict[str, Any]] = [] + session = bs.get_foreground_host_session() + if session is not None: + try: + custom_menu_entries = session.get_custom_menu_entries() + for cme in custom_menu_entries: + cme_any: Any = cme # Type check may not hold true. + if ( + not isinstance(cme_any, dict) + or 'label' not in cme + or not isinstance(cme['label'], (str, bui.Lstr)) + or 'call' not in cme + or not callable(cme['call']) + ): + raise ValueError( + 'invalid custom menu entry: ' + str(cme) + ) + except Exception: + custom_menu_entries = [] + logging.exception( + 'Error getting custom menu entries for %s.', session + ) + self._width = 250.0 + self._height = 250.0 if self._input_player else 180.0 + if (self._is_demo or self._is_arcade) and self._input_player: + self._height -= 40 + # if not self._have_settings_button: + self._height -= 50 + if self._connected_to_remote_player: + # In this case we have a leave *and* a disconnect button. + self._height += 50 + self._height += 50 * (len(custom_menu_entries)) + uiscale = bui.app.ui_v1.uiscale + bui.containerwidget( + edit=self._root_widget, + size=(self._width, self._height), + scale=( + 2.15 + if uiscale is bui.UIScale.SMALL + else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0 + ), + ) + h = 125.0 + v = self._height - 80.0 if self._input_player else self._height - 60 + h_offset = 0 + d_h_offset = 0 + v_offset = -50 + for _i in range(6 + len(custom_menu_entries)): + positions.append((h, v, 1.0)) + v += v_offset + h += h_offset + h_offset += d_h_offset + # self._play_button = None + bui.app.classic.pause() + + # Player name if applicable. + if self._input_player: + player_name = self._input_player.getname() + h, v, scale = positions[self._p_index] + v += 35 + bui.textwidget( + parent=self._root_widget, + position=(h - self._button_width / 2, v), + size=(self._button_width, self._button_height), + color=(1, 1, 1, 0.5), + scale=0.7, + h_align='center', + text=bui.Lstr(value=player_name), + ) + else: + player_name = '' + h, v, scale = positions[self._p_index] + self._p_index += 1 + btn = bui.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width / 2, v), + size=(self._button_width, self._button_height), + scale=scale, + label=bui.Lstr(resource=f'{self._r}.resumeText'), + autoselect=self._use_autoselect, + on_activate_call=self._resume, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + + # Add any custom options defined by the current game. + for entry in custom_menu_entries: + h, v, scale = positions[self._p_index] + self._p_index += 1 + + # Ask the entry whether we should resume when we call + # it (defaults to true). + resume = bool(entry.get('resume_on_call', True)) + + if resume: + call = bui.Call(self._resume_and_call, entry['call']) + else: + call = bui.Call(entry['call'], bui.WeakCall(self._resume)) + + bui.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width / 2, v), + size=(self._button_width, self._button_height), + scale=scale, + on_activate_call=call, + label=entry['label'], + autoselect=self._use_autoselect, + ) + + # Add a 'leave' button if the menu-owner has a player. + if (self._input_player or self._connected_to_remote_player) and not ( + self._is_demo or self._is_arcade + ): + h, v, scale = positions[self._p_index] + self._p_index += 1 + btn = bui.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width / 2, v), + size=(self._button_width, self._button_height), + scale=scale, + on_activate_call=self._leave, + label='', + autoselect=self._use_autoselect, + ) + + if ( + player_name != '' + and player_name[0] != '<' + and player_name[-1] != '>' + ): + txt = bui.Lstr( + resource=f'{self._r}.justPlayerText', + subs=[('${NAME}', player_name)], + ) + else: + txt = bui.Lstr(value=player_name) + bui.textwidget( + parent=self._root_widget, + position=( + h, + v + + self._button_height + * (0.64 if player_name != '' else 0.5), + ), + size=(0, 0), + text=bui.Lstr(resource=f'{self._r}.leaveGameText'), + scale=(0.83 if player_name != '' else 1.0), + color=(0.75, 1.0, 0.7), + h_align='center', + v_align='center', + draw_controller=btn, + maxwidth=self._button_width * 0.9, + ) + bui.textwidget( + parent=self._root_widget, + position=(h, v + self._button_height * 0.27), + size=(0, 0), + text=txt, + color=(0.75, 1.0, 0.7), + h_align='center', + v_align='center', + draw_controller=btn, + scale=0.45, + maxwidth=self._button_width * 0.9, + ) + return h, v, scale + + def _change_replay_speed(self, offs: int) -> None: + if not self._replay_speed_text: + if bui.do_once(): + print('_change_replay_speed called without widget') + return + bs.set_replay_speed_exponent(bs.get_replay_speed_exponent() + offs) + actual_speed = pow(2.0, bs.get_replay_speed_exponent()) + bui.textwidget( + edit=self._replay_speed_text, + text=bui.Lstr( + resource='watchWindow.playbackSpeedText', + subs=[('${SPEED}', str(actual_speed))], + ), + ) + + def _pause_or_resume_replay(self) -> None: + if bs.is_replay_paused(): + bs.resume_replay() + bui.buttonwidget( + edit=self._pause_resume_button, + label=bui.charstr(bui.SpecialChar.PAUSE_BUTTON), + ) + else: + bs.pause_replay() + bui.buttonwidget( + edit=self._pause_resume_button, + label=bui.charstr(bui.SpecialChar.PLAY_BUTTON), + ) + + def _is_benchmark(self) -> bool: + session = bs.get_foreground_host_session() + return getattr(session, 'benchmark_type', None) == 'cpu' or ( + bui.app.classic is not None + and bui.app.classic.stress_test_update_timer is not None + ) + + def _confirm_end_game(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.confirm import ConfirmWindow + + # FIXME: Currently we crash calling this on client-sessions. + + # Select cancel by default; this occasionally gets called by accident + # in a fit of button mashing and this will help reduce damage. + ConfirmWindow( + bui.Lstr(resource=f'{self._r}.exitToMenuText'), + self._end_game, + cancel_is_selected=True, + ) + + def _confirm_end_test(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.confirm import ConfirmWindow + + # Select cancel by default; this occasionally gets called by accident + # in a fit of button mashing and this will help reduce damage. + ConfirmWindow( + bui.Lstr(resource=f'{self._r}.exitToMenuText'), + self._end_game, + cancel_is_selected=True, + ) + + def _confirm_end_replay(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.confirm import ConfirmWindow + + # Select cancel by default; this occasionally gets called by accident + # in a fit of button mashing and this will help reduce damage. + ConfirmWindow( + bui.Lstr(resource=f'{self._r}.exitToMenuText'), + self._end_game, + cancel_is_selected=True, + ) + + def _confirm_leave_party(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.confirm import ConfirmWindow + + # Select cancel by default; this occasionally gets called by accident + # in a fit of button mashing and this will help reduce damage. + ConfirmWindow( + bui.Lstr(resource=f'{self._r}.leavePartyConfirmText'), + self._leave_party, + cancel_is_selected=True, + ) + + def _leave_party(self) -> None: + bs.disconnect_from_host() + + def _end_game(self) -> None: + assert bui.app.classic is not None + + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + + bui.containerwidget(edit=self._root_widget, transition='out_left') + bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) + + def _leave(self) -> None: + if self._input_player: + self._input_player.remove_from_game() + elif self._connected_to_remote_player: + if self._input_device: + self._input_device.detach_from_player() + self._resume() + + def _resume_and_call(self, call: Callable[[], Any]) -> None: + self._resume() + call() + + def _resume(self) -> None: + classic = bui.app.classic + + assert classic is not None + classic.resume() + + bui.app.ui_v1.clear_main_window() + + # If there's callbacks waiting for us to resume, call them. + for call in classic.main_menu_resume_callbacks: + try: + call() + except Exception: + logging.exception('Error in classic resume callback.') + + classic.main_menu_resume_callbacks.clear() diff --git a/src/assets/ba_data/python/bauiv1lib/inventory.py b/src/assets/ba_data/python/bauiv1lib/inventory.py new file mode 100644 index 00000000..6297f3aa --- /dev/null +++ b/src/assets/ba_data/python/bauiv1lib/inventory.py @@ -0,0 +1,127 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides help related ui.""" + +from __future__ import annotations + +from typing import override + +import bauiv1 as bui + + +class InventoryWindow(bui.MainWindow): + """Shows what you got.""" + + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): + + bui.set_analytics_screen('Help Window') + + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + width = 1050 if uiscale is bui.UIScale.SMALL else 750 + height = ( + 460 + if uiscale is bui.UIScale.SMALL + else 530 if uiscale is bui.UIScale.MEDIUM else 600 + ) + x_offs = 70 if uiscale is bui.UIScale.SMALL else 0 + + super().__init__( + root_widget=bui.containerwidget( + size=(width, height), + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), + scale=( + 1.55 + if uiscale is bui.UIScale.SMALL + else 1.15 if uiscale is bui.UIScale.MEDIUM else 1.0 + ), + stack_offset=( + (0, -24) + if uiscale is bui.UIScale.SMALL + else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0) + ), + ), + transition=transition, + origin_widget=origin_widget, + ) + + bui.textwidget( + parent=self._root_widget, + position=(0, height - (50 if uiscale is bui.UIScale.SMALL else 45)), + size=(width, 25), + text='INVENTORY', + color=bui.app.ui_v1.title_color, + h_align='center', + v_align='center', + ) + + if uiscale is bui.UIScale.SMALL: + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + else: + btn = bui.buttonwidget( + parent=self._root_widget, + position=(x_offs + 50, height - 55), + size=(60, 55), + scale=0.8, + label=bui.charstr(bui.SpecialChar.BACK), + button_type='backSmall', + extra_touch_border_scale=2.0, + autoselect=True, + on_activate_call=self.main_window_back, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + + bui.textwidget( + parent=self._root_widget, + position=(0, height - 120), + size=(width, 25), + text='(under construction)', + scale=0.7, + h_align='center', + v_align='center', + ) + + button_width = 300 + self._player_profiles_button = btn = bui.buttonwidget( + parent=self._root_widget, + position=((width - button_width) * 0.5, height - 200), + autoselect=True, + size=(button_width, 60), + label=bui.Lstr(resource='playerProfilesWindow.titleText'), + color=(0.55, 0.5, 0.6), + icon=bui.gettexture('cuteSpaz'), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._player_profiles_press, + ) + + def _player_profiles_press(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.profile.browser import ProfileBrowserWindow + + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + + self.main_window_replace( + ProfileBrowserWindow(origin_widget=self._player_profiles_button) + ) + + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) diff --git a/src/assets/ba_data/python/bauiv1lib/kiosk.py b/src/assets/ba_data/python/bauiv1lib/kiosk.py index 46475635..206569dc 100644 --- a/src/assets/ba_data/python/bauiv1lib/kiosk.py +++ b/src/assets/ba_data/python/bauiv1lib/kiosk.py @@ -8,10 +8,14 @@ import bascenev1 as bs import bauiv1 as bui -class KioskWindow(bui.Window): +class KioskWindow(bui.MainWindow): """Kiosk mode window.""" - def __init__(self, transition: str = 'in_right'): + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): # pylint: disable=too-many-locals, too-many-statements from bauiv1lib.confirm import QuitWindow @@ -26,11 +30,13 @@ class KioskWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), - transition=transition, + # transition=transition, on_cancel_call=_do_cancel, background=False, stack_offset=(0, -130), - ) + ), + transition=transition, + origin_widget=origin_widget, ) self._r = 'kioskWindow' @@ -501,6 +507,7 @@ class KioskWindow(bui.Window): bui.containerwidget(edit=self._root_widget, transition='out_left') def _do_full_menu(self) -> None: + # pylint: disable=cyclic-import from bauiv1lib.mainmenu import MainMenuWindow # no-op if our underlying widget is dead or on its way out. @@ -512,6 +519,4 @@ class KioskWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') bui.app.classic.did_menu_intro = True # prevent delayed transition-in - bui.app.ui_v1.set_main_menu_window( - MainMenuWindow().get_root_widget(), from_window=self._root_widget - ) + bui.app.ui_v1.set_main_window(MainMenuWindow(), from_window=self) diff --git a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py index 7e6d8d1e..b4e235e6 100644 --- a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py +++ b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py @@ -16,14 +16,14 @@ if TYPE_CHECKING: from typing import Any -class LeagueRankWindow(bui.Window): +class LeagueRankWindow(bui.MainWindow): """Window for showing league rank.""" def __init__( self, - transition: str = 'in_right', - modal: bool = False, + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, + modal: bool = False, ): # pylint: disable=too-many-statements plus = bui.app.plus @@ -46,22 +46,18 @@ class LeagueRankWindow(bui.Window): self._to_ranked_text: bui.Widget | None = None self._trophy_counts_reset_text: bui.Widget | None = None - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None + # Need to handle transitioning out ourself for modal case if origin_widget is not None: self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' else: self._transition_out = 'out_right' - scale_origin = None assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - self._width = 1320 if uiscale is bui.UIScale.SMALL else 1120 + self._width = 1490 if uiscale is bui.UIScale.SMALL else 1120 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( - 657 + 660 if uiscale is bui.UIScale.SMALL else 710 if uiscale is bui.UIScale.MEDIUM else 800 ) @@ -69,6 +65,8 @@ class LeagueRankWindow(bui.Window): self._rdict = bui.app.lang.get_resource(self._r) top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 + self._xoffs = 80.0 if uiscale is bui.UIScale.SMALL else 0 + self._league_url_arg = '' self._is_current_season = False @@ -78,37 +76,62 @@ class LeagueRankWindow(bui.Window): root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), stack_offset=( - (0, -15) + (0, 0) if uiscale is bui.UIScale.SMALL else (0, 10) if uiscale is bui.UIScale.MEDIUM else (0, 0) ), - transition=transition, - scale_origin_stack_offset=scale_origin, scale=( - 1.2 + 1.08 if uiscale is bui.UIScale.SMALL else 0.93 if uiscale is bui.UIScale.MEDIUM else 0.8 ), - ) + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, ) - self._back_button = btn = bui.buttonwidget( - parent=self._root_widget, - position=( - 75 + x_inset, - self._height - 87 - (4 if uiscale is bui.UIScale.SMALL else 0), - ), - size=(120, 60), - scale=1.2, - autoselect=True, - label=bui.Lstr(resource='doneText' if self._modal else 'backText'), - button_type=None if self._modal else 'back', - on_activate_call=self._back, - ) + if uiscale is bui.UIScale.SMALL: + self._back_button = bui.get_special_widget('back_button') + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self._back + ) + else: + self._back_button = btn = bui.buttonwidget( + parent=self._root_widget, + position=(75 + x_inset, self._height - 87), + size=(120, 60), + scale=1.2, + autoselect=True, + label=bui.Lstr( + resource='doneText' if self._modal else 'backText' + ), + button_type=None if self._modal else 'back', + on_activate_call=self._back, + ) + bui.buttonwidget( + edit=btn, + button_type='backSmall', + position=(75 + x_inset, self._height - 87), + size=(60, 55), + label=bui.charstr(bui.SpecialChar.BACK), + ) + bui.containerwidget( + edit=self._root_widget, + cancel_button=self._back_button, + selected_child=self._back_button, + ) self._title_text = bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height - 56), + position=( + self._width * 0.5, + self._height - (66 if uiscale is bui.UIScale.SMALL else 56), + ), size=(0, 0), text=bui.Lstr( resource='league.leagueRankText', @@ -121,17 +144,6 @@ class LeagueRankWindow(bui.Window): v_align='center', ) - bui.buttonwidget( - edit=btn, - button_type='backSmall', - position=( - 75 + x_inset, - self._height - 87 - (2 if uiscale is bui.UIScale.SMALL else 0), - ), - size=(60, 55), - label=bui.charstr(bui.SpecialChar.BACK), - ) - self._scroll_width = self._width - (130 + 2 * x_inset) self._scroll_height = self._height - 160 self._scrollwidget = bui.scrollwidget( @@ -143,11 +155,6 @@ class LeagueRankWindow(bui.Window): ) bui.widget(edit=self._scrollwidget, autoselect=True) bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) - bui.containerwidget( - edit=self._root_widget, - cancel_button=self._back_button, - selected_child=self._back_button, - ) self._last_power_ranking_query_time: float | None = None self._doing_power_ranking_query = False @@ -374,7 +381,7 @@ class LeagueRankWindow(bui.Window): bui.textwidget( parent=w_parent, - position=(h2 - 60, v2 + 106), + position=(self._xoffs + h2 - 60, v2 + 106), size=(0, 0), flatness=1.0, shadow=0.0, @@ -388,7 +395,7 @@ class LeagueRankWindow(bui.Window): self._power_ranking_achievements_button = bui.buttonwidget( parent=w_parent, - position=(h2 - 60, v2 + 10), + position=(self._xoffs + h2 - 60, v2 + 10), size=(200, 80), icon=bui.gettexture('achievementsIcon'), autoselect=True, @@ -402,7 +409,7 @@ class LeagueRankWindow(bui.Window): self._power_ranking_achievement_total_text = bui.textwidget( parent=w_parent, - position=(h2 + h_offs_tally, v2 + 45), + position=(self._xoffs + h2 + h_offs_tally, v2 + 45), size=(0, 0), flatness=1.0, shadow=0.0, @@ -418,7 +425,7 @@ class LeagueRankWindow(bui.Window): self._power_ranking_trophies_button = bui.buttonwidget( parent=w_parent, - position=(h2 - 60, v2 + 10), + position=(self._xoffs + h2 - 60, v2 + 10), size=(200, 80), icon=bui.gettexture('medalSilver'), autoselect=True, @@ -430,7 +437,7 @@ class LeagueRankWindow(bui.Window): ) self._power_ranking_trophies_total_text = bui.textwidget( parent=w_parent, - position=(h2 + h_offs_tally, v2 + 45), + position=(self._xoffs + h2 + h_offs_tally, v2 + 45), size=(0, 0), flatness=1.0, shadow=0.0, @@ -446,7 +453,7 @@ class LeagueRankWindow(bui.Window): bui.textwidget( parent=w_parent, - position=(h2 - 60, v2 + 86), + position=(self._xoffs + h2 - 60, v2 + 86), size=(0, 0), flatness=1.0, shadow=0.0, @@ -462,7 +469,7 @@ class LeagueRankWindow(bui.Window): if plus.get_v1_account_misc_read_val('act', False): self._activity_mult_button = bui.buttonwidget( parent=w_parent, - position=(h2 - 60, v2 + 10), + position=(self._xoffs + h2 - 60, v2 + 10), size=(200, 60), icon=bui.gettexture('heart'), icon_color=(0.5, 0, 0.5), @@ -476,7 +483,7 @@ class LeagueRankWindow(bui.Window): self._activity_mult_text = bui.textwidget( parent=w_parent, - position=(h2 + h_offs_tally, v2 + 40), + position=(self._xoffs + h2 + h_offs_tally, v2 + 40), size=(0, 0), flatness=1.0, shadow=0.0, @@ -493,7 +500,7 @@ class LeagueRankWindow(bui.Window): self._pro_mult_button = bui.buttonwidget( parent=w_parent, - position=(h2 - 60, v2 + 10), + position=(self._xoffs + h2 - 60, v2 + 10), size=(200, 60), icon=bui.gettexture('logo'), icon_color=(0.3, 0, 0.3), @@ -510,7 +517,7 @@ class LeagueRankWindow(bui.Window): self._pro_mult_text = bui.textwidget( parent=w_parent, - position=(h2 + h_offs_tally, v2 + 40), + position=(self._xoffs + h2 + h_offs_tally, v2 + 40), size=(0, 0), flatness=1.0, shadow=0.0, @@ -526,7 +533,7 @@ class LeagueRankWindow(bui.Window): v2 -= spc bui.textwidget( parent=w_parent, - position=(h2 + h_offs_tally - 10 - 40, v2 + 35), + position=(self._xoffs + h2 + h_offs_tally - 10 - 40, v2 + 35), size=(0, 0), flatness=1.0, shadow=0.0, @@ -539,7 +546,7 @@ class LeagueRankWindow(bui.Window): ) self._power_ranking_total_text = bui.textwidget( parent=w_parent, - position=(h2 + h_offs_tally - 40, v2 + 35), + position=(self._xoffs + h2 + h_offs_tally - 40, v2 + 35), size=(0, 0), flatness=1.0, shadow=0.0, @@ -553,7 +560,7 @@ class LeagueRankWindow(bui.Window): self._season_show_text = bui.textwidget( parent=w_parent, - position=(390 - 15, v - 20), + position=(self._xoffs + 390 - 15, v - 20), size=(0, 0), color=(0.6, 0.6, 0.7), maxwidth=200, @@ -567,7 +574,7 @@ class LeagueRankWindow(bui.Window): self._league_title_text = bui.textwidget( parent=w_parent, - position=(470, v - 97), + position=(self._xoffs + 470, v - 97), size=(0, 0), color=(0.6, 0.6, 0.7), maxwidth=230, @@ -583,7 +590,7 @@ class LeagueRankWindow(bui.Window): self._league_text_maxwidth = 210 self._league_text = bui.textwidget( parent=w_parent, - position=(470, v - 140), + position=(self._xoffs + 470, v - 140), size=(0, 0), color=(1, 1, 1), maxwidth=self._league_text_maxwidth, @@ -597,7 +604,7 @@ class LeagueRankWindow(bui.Window): self._league_number_base_pos = (470, v - 140) self._league_number_text = bui.textwidget( parent=w_parent, - position=(470, v - 140), + position=(self._xoffs + 470, v - 140), size=(0, 0), color=(1, 1, 1), maxwidth=100, @@ -611,7 +618,7 @@ class LeagueRankWindow(bui.Window): self._your_power_ranking_text = bui.textwidget( parent=w_parent, - position=(470, v - 142 - 70), + position=(self._xoffs + 470, v - 142 - 70), size=(0, 0), color=(0.6, 0.6, 0.7), maxwidth=230, @@ -625,7 +632,7 @@ class LeagueRankWindow(bui.Window): self._to_ranked_text = bui.textwidget( parent=w_parent, - position=(470, v - 250 - 70), + position=(self._xoffs + 470, v - 250 - 70), size=(0, 0), color=(0.6, 0.6, 0.7), maxwidth=230, @@ -639,7 +646,7 @@ class LeagueRankWindow(bui.Window): self._power_ranking_rank_text = bui.textwidget( parent=w_parent, - position=(473, v - 210 - 70), + position=(self._xoffs + 473, v - 210 - 70), size=(0, 0), big=False, text='-', @@ -650,7 +657,7 @@ class LeagueRankWindow(bui.Window): self._season_ends_text = bui.textwidget( parent=w_parent, - position=(470, v - 380), + position=(self._xoffs + 470, v - 380), size=(0, 0), color=(0.6, 0.6, 0.6), maxwidth=230, @@ -663,7 +670,7 @@ class LeagueRankWindow(bui.Window): ) self._trophy_counts_reset_text = bui.textwidget( parent=w_parent, - position=(470, v - 410), + position=(self._xoffs + 470, v - 410), size=(0, 0), color=(0.5, 0.5, 0.5), maxwidth=230, @@ -685,7 +692,7 @@ class LeagueRankWindow(bui.Window): self._see_more_button = bui.buttonwidget( parent=w_parent, label=self._rdict.seeMoreText, - position=(h, v), + position=(self._xoffs + h, v), color=(0.5, 0.5, 0.6), textcolor=(0.7, 0.7, 0.8), size=(230, 60), @@ -732,6 +739,7 @@ class LeagueRankWindow(bui.Window): if not self._root_widget: return plus = bui.app.plus + uiscale = bui.app.ui_v1.uiscale assert plus is not None assert bui.app.classic is not None accounts = bui.app.classic.accounts @@ -825,7 +833,7 @@ class LeagueRankWindow(bui.Window): assert self._subcontainer self._season_popup_menu = PopupMenu( parent=self._subcontainer, - position=(390, v - 45), + position=(self._xoffs + 390, v - 45), width=150, button_size=(200, 50), choices=season_choices, @@ -843,11 +851,12 @@ class LeagueRankWindow(bui.Window): edit=self._season_popup_menu.get_button(), up_widget=self._back_button, ) - bui.widget( - edit=self._back_button, - down_widget=self._power_ranking_achievements_button, - right_widget=self._season_popup_menu.get_button(), - ) + if uiscale is not bui.UIScale.SMALL: + bui.widget( + edit=self._back_button, + down_widget=self._power_ranking_achievements_button, + right_widget=self._season_popup_menu.get_button(), + ) bui.textwidget( edit=self._league_title_text, @@ -928,7 +937,10 @@ class LeagueRankWindow(bui.Window): text=lnum, color=lcolor, position=( - self._league_number_base_pos[0] + l_text_width * 0.5 + 8, + self._xoffs + + self._league_number_base_pos[0] + + l_text_width * 0.5 + + 8, self._league_number_base_pos[1] + 10, ), ) @@ -957,7 +969,7 @@ class LeagueRankWindow(bui.Window): bui.textwidget( edit=self._power_ranking_rank_text, - position=(473, v - 70 - (170 if do_percent else 220)), + position=(self._xoffs + 473, v - 70 - (170 if do_percent else 220)), text=status_text, big=(in_top or do_percent), scale=( @@ -1080,7 +1092,7 @@ class LeagueRankWindow(bui.Window): self._power_ranking_score_widgets.append( bui.textwidget( parent=w_parent, - position=(h2 - 20, v2), + position=(self._xoffs + h2 - 20, v2), size=(0, 0), color=(1, 1, 1) if is_us else (0.6, 0.6, 0.7), maxwidth=40, @@ -1095,7 +1107,7 @@ class LeagueRankWindow(bui.Window): self._power_ranking_score_widgets.append( bui.textwidget( parent=w_parent, - position=(h2 + 20, v2), + position=(self._xoffs + h2 + 20, v2), size=(0, 0), color=(1, 1, 1) if is_us else tally_color, maxwidth=60, @@ -1109,7 +1121,7 @@ class LeagueRankWindow(bui.Window): ) txt = bui.textwidget( parent=w_parent, - position=(h2 + 60, v2 - (28 * 0.5) / 0.9), + position=(self._xoffs + h2 + 60, v2 - (28 * 0.5) / 0.9), size=(210 / 0.9, 28), color=(1, 1, 1) if is_us else (0.6, 0.6, 0.6), maxwidth=210, @@ -1155,19 +1167,15 @@ class LeagueRankWindow(bui.Window): pass def _back(self) -> None: - from bauiv1lib.coop.browser import CoopBrowserWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - if not self._modal: - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - CoopBrowserWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, + if self._modal: + bui.containerwidget( + edit=self._root_widget, transition=self._transition_out ) + else: + self.main_window_back() diff --git a/src/assets/ba_data/python/bauiv1lib/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py index cc793476..ea7fd0e0 100644 --- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py +++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py @@ -1,11 +1,10 @@ # Released under the MIT License. See LICENSE for details. # """Implements the main menu window.""" -# pylint: disable=too-many-lines from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import logging import bauiv1 as bui @@ -15,41 +14,30 @@ if TYPE_CHECKING: from typing import Any, Callable -class MainMenuWindow(bui.Window): - """The main menu window, both in-game and in the main menu session.""" +class MainMenuWindow(bui.MainWindow): + """The main menu window.""" - def __init__(self, transition: str | None = 'in_right'): - # pylint: disable=cyclic-import - import threading - from bascenev1lib.mainmenu import MainMenuSession - - plus = bui.app.plus - assert plus is not None - - self._in_game = not isinstance( - bs.get_foreground_host_session(), - MainMenuSession, - ) + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. - threading.Thread(target=self._preload_modules).start() + bui.app.threadpool_submit_no_wait(self._preload_modules) - if not self._in_game: - bui.set_analytics_screen('Main Menu') - self._show_remote_app_info_on_first_launch() + bui.set_analytics_screen('Main Menu') + self._show_remote_app_info_on_first_launch() # Make a vanilla container; we'll modify it to our needs in # refresh. super().__init__( root_widget=bui.containerwidget( - transition=transition, - toolbar_visibility=( - 'menu_minimal_no_back' - if self._in_game - else 'menu_minimal_no_back' - ), - ) + toolbar_visibility=('menu_full_no_back') + ), + transition=transition, + origin_widget=origin_widget, ) # Grab this stuff in case it changes. @@ -59,7 +47,6 @@ class MainMenuWindow(bui.Window): self._tdelay = 0.0 self._t_delay_inc = 0.02 self._t_delay_play = 1.7 - self._p_index = 0 self._use_autoselect = True self._button_width = 200.0 self._button_height = 45.0 @@ -67,41 +54,43 @@ class MainMenuWindow(bui.Window): self._height = 100.0 self._demo_menu_button: bui.Widget | None = None self._gather_button: bui.Widget | None = None - self._start_button: bui.Widget | None = None + self._play_button: bui.Widget | None = None self._watch_button: bui.Widget | None = None - self._account_button: bui.Widget | None = None self._how_to_play_button: bui.Widget | None = None self._credits_button: bui.Widget | None = None - self._settings_button: bui.Widget | None = None - self._next_refresh_allow_time = 0.0 - - self._store_char_tex = self._get_store_char_tex() self._refresh() + self._restore_state() - # Keep an eye on a few things and refresh if they change. - self._account_state = plus.get_v1_account_state() - self._account_state_num = plus.get_v1_account_state_num() - self._account_type = ( - plus.get_v1_account_type() - if self._account_state == 'signed_in' - else None - ) - self._refresh_timer = bui.AppTimer( - 0.27, bui.WeakCall(self._check_refresh), repeat=True + @override + def on_main_window_close(self) -> None: + self._save_state() + + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + return self.do_get_main_window_state() + + @classmethod + def do_get_main_window_state(cls) -> bui.MainWindowState: + """Classmethod to gen a windowstate for the main menu.""" + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) ) @staticmethod def _preload_modules() -> None: """Preload modules we use; avoids hitches (called in bg thread).""" + # pylint: disable=cyclic-import import bauiv1lib.getremote as _unused import bauiv1lib.confirm as _unused2 import bauiv1lib.store.button as _unused3 - import bauiv1lib.kiosk as _unused4 import bauiv1lib.account.settings as _unused5 import bauiv1lib.store.browser as _unused6 - import bauiv1lib.creditslist as _unused7 + import bauiv1lib.credits as _unused7 import bauiv1lib.helpui as _unused8 import bauiv1lib.settings.allsettings as _unused9 import bauiv1lib.gather as _unused10 @@ -111,8 +100,8 @@ class MainMenuWindow(bui.Window): def _show_remote_app_info_on_first_launch(self) -> None: app = bui.app assert app.classic is not None - # The first time the non-in-game menu pops up, we might wanna show - # a 'get-remote-app' dialog in front of it. + # The first time the non-in-game menu pops up, we might wanna + # show a 'get-remote-app' dialog in front of it. if app.classic.first_main_menu: app.classic.first_main_menu = False try: @@ -138,67 +127,16 @@ class MainMenuWindow(bui.Window): except Exception: logging.exception('Error showing get-remote-app info.') - def _get_store_char_tex(self) -> str: - plus = bui.app.plus - assert plus is not None - return ( - 'storeCharacterXmas' - if plus.get_v1_account_misc_read_val('xmas', False) - else ( - 'storeCharacterEaster' - if plus.get_v1_account_misc_read_val('easter', False) - else 'storeCharacter' - ) - ) - - def _check_refresh(self) -> None: - plus = bui.app.plus - assert plus is not None - - if not self._root_widget: - return - - now = bui.apptime() - if now < self._next_refresh_allow_time: - return - - # Don't refresh for the first few seconds the game is up so we - # don't interrupt the transition in. - - # bui.app.main_menu_window_refresh_check_count += 1 - # if bui.app.main_menu_window_refresh_check_count < 4: - # return - - store_char_tex = self._get_store_char_tex() - account_state_num = plus.get_v1_account_state_num() - if ( - account_state_num != self._account_state_num - or store_char_tex != self._store_char_tex - ): - self._store_char_tex = store_char_tex - self._account_state_num = account_state_num - account_state = self._account_state = plus.get_v1_account_state() - self._account_type = ( - plus.get_v1_account_type() - if account_state == 'signed_in' - else None - ) - self._save_state() - self._refresh() - self._restore_state() - def get_play_button(self) -> bui.Widget | None: """Return the play button.""" - return self._start_button + return self._play_button def _refresh(self) -> None: - # pylint: disable=too-many-branches - # pylint: disable=too-many-locals # pylint: disable=too-many-statements - from bauiv1lib.store.button import StoreButton + # pylint: disable=too-many-locals - plus = bui.app.plus - assert plus is not None + classic = bui.app.classic + assert classic is not None # Clear everything that was there. children = self._root_widget.get_children() @@ -215,484 +153,63 @@ class MainMenuWindow(bui.Window): app = bui.app assert app.classic is not None - self._have_quit_button = app.ui_v1.uiscale is bui.UIScale.LARGE or ( - app.classic.platform == 'windows' - and app.classic.subplatform == 'oculus' + + self._have_quit_button = app.classic.platform in ( + 'windows', + 'mac', + 'linux', ) - self._have_store_button = not self._in_game - - self._have_settings_button = ( - not self._in_game or not app.ui_v1.use_toolbars - ) and not (self._is_demo or self._is_arcade) - - self._input_device = input_device = bs.get_ui_input_device() - - # Are we connected to a local player? - self._input_player = input_device.player if input_device else None - - # Are we connected to a remote player?. - self._connected_to_remote_player = ( - input_device.is_attached_to_player() - if (input_device and self._input_player is None) - else False - ) - - positions: list[tuple[float, float, float]] = [] - self._p_index = 0 - - if self._in_game: - h, v, scale = self._refresh_in_game(positions) - else: - h, v, scale = self._refresh_not_in_game(positions) - - if self._have_settings_button: - h, v, scale = positions[self._p_index] - self._p_index += 1 - self._settings_button = bui.buttonwidget( - parent=self._root_widget, - position=(h - self._button_width * 0.5 * scale, v), - size=(self._button_width, self._button_height), - scale=scale, - autoselect=self._use_autoselect, - label=bui.Lstr(resource=f'{self._r}.settingsText'), - transition_delay=self._tdelay, - on_activate_call=self._settings, - ) - - # Scattered eggs on easter. - if ( - plus.get_v1_account_misc_read_val('easter', False) - and not self._in_game - ): - icon_size = 34 - bui.imagewidget( - parent=self._root_widget, - position=( - h - icon_size * 0.5 - 15, - v + self._button_height * scale - icon_size * 0.24 + 1.5, - ), - transition_delay=self._tdelay, - size=(icon_size, icon_size), - texture=bui.gettexture('egg3'), - tilt_scale=0.0, - ) - - self._tdelay += self._t_delay_inc - - if self._in_game: - h, v, scale = positions[self._p_index] - self._p_index += 1 - - # If we're in a replay, we have a 'Leave Replay' button. - if bs.is_in_replay(): - bui.buttonwidget( - parent=self._root_widget, - position=(h - self._button_width * 0.5 * scale, v), - scale=scale, - size=(self._button_width, self._button_height), - autoselect=self._use_autoselect, - label=bui.Lstr(resource='replayEndText'), - on_activate_call=self._confirm_end_replay, - ) - elif bs.get_foreground_host_session() is not None: - bui.buttonwidget( - parent=self._root_widget, - position=(h - self._button_width * 0.5 * scale, v), - scale=scale, - size=(self._button_width, self._button_height), - autoselect=self._use_autoselect, - label=bui.Lstr( - resource=self._r - + ( - '.endTestText' - if self._is_benchmark() - else '.endGameText' - ) - ), - on_activate_call=( - self._confirm_end_test - if self._is_benchmark() - else self._confirm_end_game - ), - ) - else: - # Assume we're in a client-session. - bui.buttonwidget( - parent=self._root_widget, - position=(h - self._button_width * 0.5 * scale, v), - scale=scale, - size=(self._button_width, self._button_height), - autoselect=self._use_autoselect, - label=bui.Lstr(resource=f'{self._r}.leavePartyText'), - on_activate_call=self._confirm_leave_party, - ) - - self._store_button: bui.Widget | None - if self._have_store_button: - this_b_width = self._button_width - h, v, scale = positions[self._p_index] - self._p_index += 1 - - sbtn = self._store_button_instance = StoreButton( - parent=self._root_widget, - position=(h - this_b_width * 0.5 * scale, v), - size=(this_b_width, self._button_height), - scale=scale, - on_activate_call=bui.WeakCall(self._on_store_pressed), - sale_scale=1.3, - transition_delay=self._tdelay, - ) - self._store_button = store_button = sbtn.get_button() - assert bui.app.classic is not None - uiscale = bui.app.ui_v1.uiscale - icon_size = ( - 55 - if uiscale is bui.UIScale.SMALL - else 55 if uiscale is bui.UIScale.MEDIUM else 70 - ) - bui.imagewidget( - parent=self._root_widget, - position=( - h - icon_size * 0.5, - v + self._button_height * scale - icon_size * 0.23, - ), - transition_delay=self._tdelay, - size=(icon_size, icon_size), - texture=bui.gettexture(self._store_char_tex), - tilt_scale=0.0, - draw_controller=store_button, - ) - self._tdelay += self._t_delay_inc - else: - self._store_button = None - - self._quit_button: bui.Widget | None - if not self._in_game and self._have_quit_button: - h, v, scale = positions[self._p_index] - self._p_index += 1 - self._quit_button = quit_button = bui.buttonwidget( - parent=self._root_widget, - autoselect=self._use_autoselect, - position=(h - self._button_width * 0.5 * scale, v), - size=(self._button_width, self._button_height), - scale=scale, - label=bui.Lstr( - resource=self._r - + ( - '.quitText' - if 'Mac' in app.classic.legacy_user_agent_string - else '.exitGameText' - ) - ), - on_activate_call=self._quit, - transition_delay=self._tdelay, - ) - - # Scattered eggs on easter. - if plus.get_v1_account_misc_read_val('easter', False): - icon_size = 30 - bui.imagewidget( - parent=self._root_widget, - position=( - h - icon_size * 0.5 + 25, - v - + self._button_height * scale - - icon_size * 0.24 - + 1.5, - ), - transition_delay=self._tdelay, - size=(icon_size, icon_size), - texture=bui.gettexture('egg1'), - tilt_scale=0.0, - ) - - bui.containerwidget( - edit=self._root_widget, cancel_button=quit_button - ) - self._tdelay += self._t_delay_inc - else: - self._quit_button = None - - # If we're not in-game, have no quit button, and this is - # android, we want back presses to quit our activity. - if ( - not self._in_game - and not self._have_quit_button - and app.classic.platform == 'android' - ): - - def _do_quit() -> None: - bui.quit(confirm=True, quit_type=bui.QuitType.BACK) - - bui.containerwidget( - edit=self._root_widget, on_cancel_call=_do_quit - ) - - # Add speed-up/slow-down buttons for replays. Ideally this - # should be part of a fading-out playback bar like most media - # players but this works for now. - if bs.is_in_replay(): - b_size = 50.0 - b_buffer_1 = 50.0 - b_buffer_2 = 10.0 - t_scale = 0.75 - assert bui.app.classic is not None - uiscale = bui.app.ui_v1.uiscale - if uiscale is bui.UIScale.SMALL: - b_size *= 0.6 - b_buffer_1 *= 0.8 - b_buffer_2 *= 1.0 - v_offs = -40 - t_scale = 0.5 - elif uiscale is bui.UIScale.MEDIUM: - v_offs = -70 - else: - v_offs = -100 - self._replay_speed_text = bui.textwidget( - parent=self._root_widget, - text=bui.Lstr( - resource='watchWindow.playbackSpeedText', - subs=[('${SPEED}', str(1.23))], - ), - position=(h, v + v_offs + 15 * t_scale), - h_align='center', - v_align='center', - size=(0, 0), - scale=t_scale, - ) - - # Update to current value. - self._change_replay_speed(0) - - # Keep updating in a timer in case it gets changed elsewhere. - self._change_replay_speed_timer = bui.AppTimer( - 0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True - ) - btn = bui.buttonwidget( - parent=self._root_widget, - position=( - h - b_size - b_buffer_1, - v - b_size - b_buffer_2 + v_offs, - ), - button_type='square', - size=(b_size, b_size), - label='', - autoselect=True, - on_activate_call=bui.Call(self._change_replay_speed, -1), - ) - bui.textwidget( - parent=self._root_widget, - draw_controller=btn, - text='-', - position=( - h - b_size * 0.5 - b_buffer_1, - v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, - ), - h_align='center', - v_align='center', - size=(0, 0), - scale=3.0 * t_scale, - ) - btn = bui.buttonwidget( - parent=self._root_widget, - position=(h + b_buffer_1, v - b_size - b_buffer_2 + v_offs), - button_type='square', - size=(b_size, b_size), - label='', - autoselect=True, - on_activate_call=bui.Call(self._change_replay_speed, 1), - ) - bui.textwidget( - parent=self._root_widget, - draw_controller=btn, - text='+', - position=( - h + b_size * 0.5 + b_buffer_1, - v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, - ), - h_align='center', - v_align='center', - size=(0, 0), - scale=3.0 * t_scale, - ) - self._pause_resume_button = btn = bui.buttonwidget( - parent=self._root_widget, - position=(h - b_size * 0.5, v - b_size - b_buffer_2 + v_offs), - button_type='square', - size=(b_size, b_size), - label=bui.charstr( - bui.SpecialChar.PLAY_BUTTON - if bs.is_replay_paused() - else bui.SpecialChar.PAUSE_BUTTON - ), - autoselect=True, - on_activate_call=bui.Call(self._pause_or_resume_replay), - ) - btn = bui.buttonwidget( - parent=self._root_widget, - position=( - h - b_size * 1.5 - b_buffer_1 * 2, - v - b_size - b_buffer_2 + v_offs, - ), - button_type='square', - size=(b_size, b_size), - label='', - autoselect=True, - on_activate_call=bui.WeakCall(self._rewind_replay), - ) - bui.textwidget( - parent=self._root_widget, - draw_controller=btn, - # text='<<', - text=bui.charstr(bui.SpecialChar.REWIND_BUTTON), - position=( - h - b_size - b_buffer_1 * 2, - v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, - ), - h_align='center', - v_align='center', - size=(0, 0), - scale=2.0 * t_scale, - ) - btn = bui.buttonwidget( - parent=self._root_widget, - position=( - h + b_size * 0.5 + b_buffer_1 * 2, - v - b_size - b_buffer_2 + v_offs, - ), - button_type='square', - size=(b_size, b_size), - label='', - autoselect=True, - on_activate_call=bui.WeakCall(self._forward_replay), - ) - bui.textwidget( - parent=self._root_widget, - draw_controller=btn, - # text='>>', - text=bui.charstr(bui.SpecialChar.FAST_FORWARD_BUTTON), - position=( - h + b_size + b_buffer_1 * 2, - v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, - ), - h_align='center', - v_align='center', - size=(0, 0), - scale=2.0 * t_scale, - ) - - def _rewind_replay(self) -> None: - bs.seek_replay(-2 * pow(2, bs.get_replay_speed_exponent())) - - def _forward_replay(self) -> None: - bs.seek_replay(2 * pow(2, bs.get_replay_speed_exponent())) - - def _refresh_not_in_game( - self, positions: list[tuple[float, float, float]] - ) -> tuple[float, float, float]: - # pylint: disable=too-many-branches - # pylint: disable=too-many-locals - # pylint: disable=too-many-statements - plus = bui.app.plus - assert plus is not None - - assert bui.app.classic is not None - if not bui.app.classic.did_menu_intro: - self._tdelay = 2.0 - self._t_delay_inc = 0.02 + if not classic.did_menu_intro: + self._tdelay = 1.7 + self._t_delay_inc = 0.05 self._t_delay_play = 1.7 + classic.did_menu_intro = True - def _set_allow_time() -> None: - self._next_refresh_allow_time = bui.apptime() + 2.5 - - # Slight hack: widget transitions currently only progress - # when frames are being drawn, but this tends to get called - # before frame drawing even starts, meaning we don't know - # exactly how long we should wait before refreshing to avoid - # interrupting the transition. To make things a bit better, - # let's do a redundant set of the time in a deferred call - # which hopefully happens closer to actual frame draw times. - _set_allow_time() - bui.pushcall(_set_allow_time) - - bui.app.classic.did_menu_intro = True self._width = 400.0 self._height = 200.0 - enable_account_button = True - account_type_name: str | bui.Lstr - if plus.get_v1_account_state() == 'signed_in': - account_type_name = plus.get_v1_account_display_string() - account_type_icon = None - account_textcolor = (1.0, 1.0, 1.0) - else: - account_type_name = bui.Lstr( - resource='notSignedInText', - fallback_resource='accountSettingsWindow.titleText', - ) - account_type_icon = None - account_textcolor = (1.0, 0.2, 0.2) - account_type_icon_color = (1.0, 1.0, 1.0) - account_type_call = self._show_account_window - account_type_enable_button_sound = True - b_count = 3 # play, help, credits - if self._have_settings_button: - b_count += 1 - if enable_account_button: - b_count += 1 - if self._have_quit_button: - b_count += 1 - if self._have_store_button: - b_count += 1 + + play_button_width = self._button_width * 0.65 + play_button_height = self._button_height * 1.1 + play_button_scale = 1.7 + hspace = 20.0 + side_button_width = self._button_width * 0.4 + side_button_height = side_button_width + side_button_scale = 0.95 + side_button_y_offs = 5.0 + hspace2 = 15.0 + side_button_2_width = self._button_width * 1.0 + side_button_2_height = side_button_2_width * 0.3 + side_button_2_y_offs = 10.0 + side_button_2_scale = 0.5 + uiscale = bui.app.ui_v1.uiscale if uiscale is bui.UIScale.SMALL: - root_widget_scale = 1.6 - play_button_width = self._button_width * 0.65 - play_button_height = self._button_height * 1.1 - small_button_scale = 0.51 if b_count > 6 else 0.63 + root_widget_scale = 1.3 button_y_offs = -20.0 - button_y_offs2 = -60.0 self._button_height *= 1.3 - button_spacing = 1.04 elif uiscale is bui.UIScale.MEDIUM: root_widget_scale = 1.3 - play_button_width = self._button_width * 0.65 - play_button_height = self._button_height * 1.1 - small_button_scale = 0.6 button_y_offs = -55.0 - button_y_offs2 = -75.0 self._button_height *= 1.25 - button_spacing = 1.1 else: root_widget_scale = 1.0 - play_button_width = self._button_width * 0.65 - play_button_height = self._button_height * 1.1 - small_button_scale = 0.75 - button_y_offs = -80.0 - button_y_offs2 = -100.0 + button_y_offs = -90.0 self._button_height *= 1.2 - button_spacing = 1.1 - spc = self._button_width * small_button_scale * button_spacing + bui.containerwidget( edit=self._root_widget, size=(self._width, self._height), background=False, scale=root_widget_scale, ) - assert not positions - positions.append((self._width * 0.5, button_y_offs, 1.7)) - x_offs = self._width * 0.5 - (spc * (b_count - 1) * 0.5) + (spc * 0.5) - for i in range(b_count - 1): - positions.append( - ( - x_offs + spc * i - 1.0, - button_y_offs + button_y_offs2, - small_button_scale, - ) - ) + # In kiosk mode, provide a button to get back to the kiosk menu. if bui.app.env.demo or bui.app.env.arcade: - h, v, scale = positions[self._p_index] + # h, v, scale = positions[self._p_index] + h = self._width * 0.5 + v = button_y_offs + scale = 1.0 this_b_width = self._button_width * 0.4 * scale demo_menu_delay = ( 0.0 @@ -714,70 +231,90 @@ class MainMenuWindow(bui.Window): ) ), transition_delay=demo_menu_delay, - on_activate_call=self._demo_menu_press, + on_activate_call=self.main_window_back, ) else: self._demo_menu_button = None - uiscale = bui.app.ui_v1.uiscale - foof = ( - -1 - if uiscale is bui.UIScale.SMALL - else 1 if uiscale is bui.UIScale.MEDIUM else 3 + + # Gather button + h = self._width * 0.5 + h = ( + self._width * 0.5 + - play_button_width * play_button_scale * 0.5 + - hspace + - side_button_width * side_button_scale * 0.5 ) - h, v, scale = positions[self._p_index] - v = v + foof - gather_delay = ( - 0.0 - if self._t_delay_play == 0.0 - else max(0.0, self._t_delay_play + 0.1) - ) - assert play_button_width is not None - assert play_button_height is not None - this_h = h - play_button_width * 0.5 * scale - 40 * scale - this_b_width = self._button_width * 0.25 * scale - this_b_height = self._button_height * 0.82 * scale + v = button_y_offs + side_button_y_offs self._gather_button = btn = bui.buttonwidget( parent=self._root_widget, - position=(this_h - this_b_width * 0.5, v), - size=(this_b_width, this_b_height), + position=(h - side_button_width * side_button_scale * 0.5, v), + size=(side_button_width, side_button_height), + scale=side_button_scale, autoselect=self._use_autoselect, button_type='square', label='', - transition_delay=gather_delay, + transition_delay=self._tdelay, on_activate_call=self._gather_press, ) bui.textwidget( parent=self._root_widget, - position=(this_h, v + self._button_height * 0.33), + position=(h, v + side_button_height * side_button_scale * 0.25), size=(0, 0), scale=0.75, - transition_delay=gather_delay, + transition_delay=self._tdelay, draw_controller=btn, color=(0.75, 1.0, 0.7), - maxwidth=self._button_width * 0.33, + maxwidth=side_button_width * side_button_scale * 0.8, text=bui.Lstr(resource='gatherWindow.titleText'), h_align='center', v_align='center', ) - icon_size = this_b_width * 0.6 + icon_size = side_button_width * side_button_scale * 0.63 bui.imagewidget( parent=self._root_widget, size=(icon_size, icon_size), draw_controller=btn, - transition_delay=gather_delay, - position=(this_h - 0.5 * icon_size, v + 0.31 * this_b_height), + transition_delay=self._tdelay, + position=( + h - 0.5 * icon_size, + v + + 0.65 * side_button_height * side_button_scale + - 0.5 * icon_size, + ), texture=bui.gettexture('usersButton'), ) - # Play button. - h, v, scale = positions[self._p_index] - self._p_index += 1 - self._start_button = start_button = bui.buttonwidget( + h -= ( + side_button_width * side_button_scale * 0.5 + + hspace2 + + side_button_2_width * side_button_2_scale + ) + v = button_y_offs + side_button_2_y_offs + + btn = bui.buttonwidget( parent=self._root_widget, - position=(h - play_button_width * 0.5 * scale, v), + position=(h, v), + autoselect=self._use_autoselect, + size=(side_button_2_width, side_button_2_height * 2.0), + button_type='square', + scale=side_button_2_scale, + label=bui.Lstr(resource=f'{self._r}.howToPlayText'), + transition_delay=self._tdelay, + on_activate_call=self._howtoplay, + ) + self._how_to_play_button = btn + + # Play button. + h = self._width * 0.5 + v = button_y_offs + assert play_button_width is not None + assert play_button_height is not None + self._play_button = start_button = bui.buttonwidget( + parent=self._root_widget, + position=(h - play_button_width * 0.5 * play_button_scale, v), size=(play_button_width, play_button_height), autoselect=self._use_autoselect, - scale=scale, + scale=play_button_scale, text_res_scale=2.0, label=bui.Lstr(resource='playText'), transition_delay=self._t_delay_play, @@ -788,346 +325,125 @@ class MainMenuWindow(bui.Window): start_button=start_button, selected_child=start_button, ) - v = v + foof - watch_delay = ( - 0.0 - if self._t_delay_play == 0.0 - else max(0.0, self._t_delay_play - 0.1) + + self._tdelay += self._t_delay_inc + + h = ( + self._width * 0.5 + + play_button_width * play_button_scale * 0.5 + + hspace + + side_button_width * side_button_scale * 0.5 ) - this_h = h + play_button_width * 0.5 * scale + 40 * scale - this_b_width = self._button_width * 0.25 * scale - this_b_height = self._button_height * 0.82 * scale + v = button_y_offs + side_button_y_offs self._watch_button = btn = bui.buttonwidget( parent=self._root_widget, - position=(this_h - this_b_width * 0.5, v), - size=(this_b_width, this_b_height), + position=(h - side_button_width * side_button_scale * 0.5, v), + size=(side_button_width, side_button_height), + scale=side_button_scale, autoselect=self._use_autoselect, button_type='square', label='', - transition_delay=watch_delay, + transition_delay=self._tdelay, on_activate_call=self._watch_press, ) bui.textwidget( parent=self._root_widget, - position=(this_h, v + self._button_height * 0.33), + position=(h, v + side_button_height * side_button_scale * 0.25), size=(0, 0), scale=0.75, - transition_delay=watch_delay, + transition_delay=self._tdelay, color=(0.75, 1.0, 0.7), draw_controller=btn, - maxwidth=self._button_width * 0.33, + maxwidth=side_button_width * side_button_scale * 0.8, text=bui.Lstr(resource='watchWindow.titleText'), h_align='center', v_align='center', ) - icon_size = this_b_width * 0.55 + icon_size = side_button_width * side_button_scale * 0.63 bui.imagewidget( parent=self._root_widget, size=(icon_size, icon_size), draw_controller=btn, - transition_delay=watch_delay, - position=(this_h - 0.5 * icon_size, v + 0.33 * this_b_height), + transition_delay=self._tdelay, + position=( + h - 0.5 * icon_size, + v + + 0.65 * side_button_height * side_button_scale + - 0.5 * icon_size, + ), texture=bui.gettexture('tv'), ) - if not self._in_game and enable_account_button: - this_b_width = self._button_width - h, v, scale = positions[self._p_index] - self._p_index += 1 - self._account_button = bui.buttonwidget( - parent=self._root_widget, - position=(h - this_b_width * 0.5 * scale, v), - size=(this_b_width, self._button_height), - scale=scale, - label=account_type_name, - autoselect=self._use_autoselect, - on_activate_call=account_type_call, - textcolor=account_textcolor, - icon=account_type_icon, - icon_color=account_type_icon_color, - transition_delay=self._tdelay, - enable_sound=account_type_enable_button_sound, - ) - # Scattered eggs on easter. - if ( - plus.get_v1_account_misc_read_val('easter', False) - and not self._in_game - ): - icon_size = 32 - bui.imagewidget( - parent=self._root_widget, - position=( - h - icon_size * 0.5 + 35, - v - + self._button_height * scale - - icon_size * 0.24 - + 1.5, - ), - transition_delay=self._tdelay, - size=(icon_size, icon_size), - texture=bui.gettexture('egg2'), - tilt_scale=0.0, - ) - self._tdelay += self._t_delay_inc - else: - self._account_button = None - - # How-to-play button. - h, v, scale = positions[self._p_index] - self._p_index += 1 - btn = bui.buttonwidget( - parent=self._root_widget, - position=(h - self._button_width * 0.5 * scale, v), - scale=scale, - autoselect=self._use_autoselect, - size=(self._button_width, self._button_height), - label=bui.Lstr(resource=f'{self._r}.howToPlayText'), - transition_delay=self._tdelay, - on_activate_call=self._howtoplay, - ) - self._how_to_play_button = btn - - # Scattered eggs on easter. - if ( - plus.get_v1_account_misc_read_val('easter', False) - and not self._in_game - ): - icon_size = 28 - bui.imagewidget( - parent=self._root_widget, - position=( - h - icon_size * 0.5 + 30, - v + self._button_height * scale - icon_size * 0.24 + 1.5, - ), - transition_delay=self._tdelay, - size=(icon_size, icon_size), - texture=bui.gettexture('egg4'), - tilt_scale=0.0, - ) # Credits button. self._tdelay += self._t_delay_inc - h, v, scale = positions[self._p_index] - self._p_index += 1 + + h += side_button_width * side_button_scale * 0.5 + hspace2 + v = button_y_offs + side_button_2_y_offs + + if self._have_quit_button: + v += 1.17 * side_button_2_height * side_button_2_scale + self._credits_button = bui.buttonwidget( parent=self._root_widget, - position=(h - self._button_width * 0.5 * scale, v), - size=(self._button_width, self._button_height), + position=(h, v), + button_type=None if self._have_quit_button else 'square', + size=( + side_button_2_width, + side_button_2_height * (1.0 if self._have_quit_button else 2.0), + ), + scale=side_button_2_scale, autoselect=self._use_autoselect, label=bui.Lstr(resource=f'{self._r}.creditsText'), - scale=scale, transition_delay=self._tdelay, on_activate_call=self._credits, ) self._tdelay += self._t_delay_inc - return h, v, scale - def _refresh_in_game( - self, positions: list[tuple[float, float, float]] - ) -> tuple[float, float, float]: - # pylint: disable=too-many-branches - # pylint: disable=too-many-locals - # pylint: disable=too-many-statements - assert bui.app.classic is not None - custom_menu_entries: list[dict[str, Any]] = [] - session = bs.get_foreground_host_session() - if session is not None: - try: - custom_menu_entries = session.get_custom_menu_entries() - for cme in custom_menu_entries: - cme_any: Any = cme # Type check may not hold true. - if ( - not isinstance(cme_any, dict) - or 'label' not in cme - or not isinstance(cme['label'], (str, bui.Lstr)) - or 'call' not in cme - or not callable(cme['call']) - ): - raise ValueError( - 'invalid custom menu entry: ' + str(cme) - ) - except Exception: - custom_menu_entries = [] - logging.exception( - 'Error getting custom menu entries for %s.', session - ) - self._width = 250.0 - self._height = 250.0 if self._input_player else 180.0 - if (self._is_demo or self._is_arcade) and self._input_player: - self._height -= 40 - if not self._have_settings_button: - self._height -= 50 - if self._connected_to_remote_player: - # In this case we have a leave *and* a disconnect button. - self._height += 50 - self._height += 50 * (len(custom_menu_entries)) - uiscale = bui.app.ui_v1.uiscale - bui.containerwidget( - edit=self._root_widget, - size=(self._width, self._height), - scale=( - 2.15 - if uiscale is bui.UIScale.SMALL - else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0 - ), - ) - h = 125.0 - v = self._height - 80.0 if self._input_player else self._height - 60 - h_offset = 0 - d_h_offset = 0 - v_offset = -50 - for _i in range(6 + len(custom_menu_entries)): - positions.append((h, v, 1.0)) - v += v_offset - h += h_offset - h_offset += d_h_offset - self._start_button = None - bui.app.classic.pause() - - # Player name if applicable. - if self._input_player: - player_name = self._input_player.getname() - h, v, scale = positions[self._p_index] - v += 35 - bui.textwidget( + self._quit_button: bui.Widget | None + if self._have_quit_button: + v -= 1.1 * side_button_2_height * side_button_2_scale + self._quit_button = quit_button = bui.buttonwidget( parent=self._root_widget, - position=(h - self._button_width / 2, v), - size=(self._button_width, self._button_height), - color=(1, 1, 1, 0.5), - scale=0.7, - h_align='center', - text=bui.Lstr(value=player_name), - ) - else: - player_name = '' - h, v, scale = positions[self._p_index] - self._p_index += 1 - btn = bui.buttonwidget( - parent=self._root_widget, - position=(h - self._button_width / 2, v), - size=(self._button_width, self._button_height), - scale=scale, - label=bui.Lstr(resource=f'{self._r}.resumeText'), - autoselect=self._use_autoselect, - on_activate_call=self._resume, - ) - bui.containerwidget(edit=self._root_widget, cancel_button=btn) - - # Add any custom options defined by the current game. - for entry in custom_menu_entries: - h, v, scale = positions[self._p_index] - self._p_index += 1 - - # Ask the entry whether we should resume when we call - # it (defaults to true). - resume = bool(entry.get('resume_on_call', True)) - - if resume: - call = bui.Call(self._resume_and_call, entry['call']) - else: - call = bui.Call(entry['call'], bui.WeakCall(self._resume)) - - bui.buttonwidget( - parent=self._root_widget, - position=(h - self._button_width / 2, v), - size=(self._button_width, self._button_height), - scale=scale, - on_activate_call=call, - label=entry['label'], autoselect=self._use_autoselect, - ) - # Add a 'leave' button if the menu-owner has a player. - if (self._input_player or self._connected_to_remote_player) and not ( - self._is_demo or self._is_arcade - ): - h, v, scale = positions[self._p_index] - self._p_index += 1 - btn = bui.buttonwidget( - parent=self._root_widget, - position=(h - self._button_width / 2, v), - size=(self._button_width, self._button_height), - scale=scale, - on_activate_call=self._leave, - label='', - autoselect=self._use_autoselect, - ) - - if ( - player_name != '' - and player_name[0] != '<' - and player_name[-1] != '>' - ): - txt = bui.Lstr( - resource=f'{self._r}.justPlayerText', - subs=[('${NAME}', player_name)], - ) - else: - txt = bui.Lstr(value=player_name) - bui.textwidget( - parent=self._root_widget, - position=( - h, - v - + self._button_height - * (0.64 if player_name != '' else 0.5), + position=(h, v), + size=(side_button_2_width, side_button_2_height), + scale=side_button_2_scale, + label=bui.Lstr( + resource=self._r + + ( + '.quitText' + if 'Mac' in app.classic.legacy_user_agent_string + else '.exitGameText' + ) ), - size=(0, 0), - text=bui.Lstr(resource=f'{self._r}.leaveGameText'), - scale=(0.83 if player_name != '' else 1.0), - color=(0.75, 1.0, 0.7), - h_align='center', - v_align='center', - draw_controller=btn, - maxwidth=self._button_width * 0.9, + on_activate_call=self._quit, + transition_delay=self._tdelay, ) - bui.textwidget( - parent=self._root_widget, - position=(h, v + self._button_height * 0.27), - size=(0, 0), - text=txt, - color=(0.75, 1.0, 0.7), - h_align='center', - v_align='center', - draw_controller=btn, - scale=0.45, - maxwidth=self._button_width * 0.9, - ) - return h, v, scale - def _change_replay_speed(self, offs: int) -> None: - if not self._replay_speed_text: - if bui.do_once(): - print('_change_replay_speed called without widget') - return - bs.set_replay_speed_exponent(bs.get_replay_speed_exponent() + offs) - actual_speed = pow(2.0, bs.get_replay_speed_exponent()) - bui.textwidget( - edit=self._replay_speed_text, - text=bui.Lstr( - resource='watchWindow.playbackSpeedText', - subs=[('${SPEED}', str(actual_speed))], - ), - ) - - def _pause_or_resume_replay(self) -> None: - if bs.is_replay_paused(): - bs.resume_replay() - bui.buttonwidget( - edit=self._pause_resume_button, - label=bui.charstr(bui.SpecialChar.PAUSE_BUTTON), + bui.containerwidget( + edit=self._root_widget, cancel_button=quit_button ) + self._tdelay += self._t_delay_inc else: - bs.pause_replay() - bui.buttonwidget( - edit=self._pause_resume_button, - label=bui.charstr(bui.SpecialChar.PLAY_BUTTON), - ) + self._quit_button = None + + # If we're not in-game, have no quit button, and this is + # android, we want back presses to quit our activity. + if app.classic.platform == 'android': + + def _do_quit() -> None: + bui.quit(confirm=True, quit_type=bui.QuitType.BACK) + + bui.containerwidget( + edit=self._root_widget, on_cancel_call=_do_quit + ) def _quit(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.confirm import QuitWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: + # no-op if we're not currently in control. + if not self.can_change_main_window(): return # Note: Normally we should go through bui.quit(confirm=True) but @@ -1135,334 +451,120 @@ class MainMenuWindow(bui.Window): # button. QuitWindow(origin_widget=self._quit_button) - def _demo_menu_press(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.kiosk import KioskWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_right') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - KioskWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) - - def _show_account_window(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.account.settings import AccountSettingsWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AccountSettingsWindow( - origin_widget=self._account_button - ).get_root_widget(), - from_window=self._root_widget, - ) - - def _on_store_pressed(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.store.browser import StoreBrowserWindow - from bauiv1lib.account import show_sign_in_prompt - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - plus = bui.app.plus - assert plus is not None - - if plus.get_v1_account_state() != 'signed_in': - show_sign_in_prompt() - return - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - StoreBrowserWindow( - origin_widget=self._store_button - ).get_root_widget(), - from_window=self._root_widget, - ) - - def _is_benchmark(self) -> bool: - session = bs.get_foreground_host_session() - return getattr(session, 'benchmark_type', None) == 'cpu' or ( - bui.app.classic is not None - and bui.app.classic.stress_test_update_timer is not None - ) - - def _confirm_end_game(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.confirm import ConfirmWindow - - # FIXME: Currently we crash calling this on client-sessions. - - # Select cancel by default; this occasionally gets called by accident - # in a fit of button mashing and this will help reduce damage. - ConfirmWindow( - bui.Lstr(resource=f'{self._r}.exitToMenuText'), - self._end_game, - cancel_is_selected=True, - ) - - def _confirm_end_test(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.confirm import ConfirmWindow - - # Select cancel by default; this occasionally gets called by accident - # in a fit of button mashing and this will help reduce damage. - ConfirmWindow( - bui.Lstr(resource=f'{self._r}.exitToMenuText'), - self._end_game, - cancel_is_selected=True, - ) - - def _confirm_end_replay(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.confirm import ConfirmWindow - - # Select cancel by default; this occasionally gets called by accident - # in a fit of button mashing and this will help reduce damage. - ConfirmWindow( - bui.Lstr(resource=f'{self._r}.exitToMenuText'), - self._end_game, - cancel_is_selected=True, - ) - - def _confirm_leave_party(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.confirm import ConfirmWindow - - # Select cancel by default; this occasionally gets called by accident - # in a fit of button mashing and this will help reduce damage. - ConfirmWindow( - bui.Lstr(resource=f'{self._r}.leavePartyConfirmText'), - self._leave_party, - cancel_is_selected=True, - ) - - def _leave_party(self) -> None: - bs.disconnect_from_host() - - def _end_game(self) -> None: - assert bui.app.classic is not None - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - bui.containerwidget(edit=self._root_widget, transition='out_left') - bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) - - def _leave(self) -> None: - if self._input_player: - self._input_player.remove_from_game() - elif self._connected_to_remote_player: - if self._input_device: - self._input_device.detach_from_player() - self._resume() - def _credits(self) -> None: # pylint: disable=cyclic-import - from bauiv1lib.creditslist import CreditsListWindow + from bauiv1lib.credits import CreditsWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: + # no-op if we're not currently in control. + if not self.can_change_main_window(): return - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - CreditsListWindow( - origin_widget=self._credits_button - ).get_root_widget(), - from_window=self._root_widget, + self.main_window_replace( + CreditsWindow(origin_widget=self._credits_button), + # group_id='mainmenutop', ) def _howtoplay(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.helpui import HelpWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: + # no-op if we're not currently in control. + if not self.can_change_main_window(): return - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - HelpWindow( - main_menu=True, origin_widget=self._how_to_play_button - ).get_root_widget(), - from_window=self._root_widget, + self.main_window_replace( + HelpWindow(origin_widget=self._how_to_play_button), ) - def _settings(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.settings.allsettings import AllSettingsWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AllSettingsWindow( - origin_widget=self._settings_button - ).get_root_widget(), - from_window=self._root_widget, - ) - - def _resume_and_call(self, call: Callable[[], Any]) -> None: - self._resume() - call() - - def _do_game_service_press(self) -> None: - self._save_state() - if bui.app.plus is not None: - bui.app.plus.show_game_service_ui() - else: - logging.warning( - 'plus feature-set is required to show game service ui' - ) - def _save_state(self) -> None: - # Don't do this for the in-game menu. - if self._in_game: - return - assert bui.app.classic is not None - ui = bui.app.ui_v1 - sel = self._root_widget.get_selected_child() - if sel == self._start_button: - ui.main_menu_selection = 'Start' - elif sel == self._gather_button: - ui.main_menu_selection = 'Gather' - elif sel == self._watch_button: - ui.main_menu_selection = 'Watch' - elif sel == self._how_to_play_button: - ui.main_menu_selection = 'HowToPlay' - elif sel == self._credits_button: - ui.main_menu_selection = 'Credits' - elif sel == self._settings_button: - ui.main_menu_selection = 'Settings' - elif sel == self._account_button: - ui.main_menu_selection = 'Account' - elif sel == self._store_button: - ui.main_menu_selection = 'Store' - elif sel == self._quit_button: - ui.main_menu_selection = 'Quit' - elif sel == self._demo_menu_button: - ui.main_menu_selection = 'DemoMenu' - else: - print('unknown widget in main menu store selection:', sel) - ui.main_menu_selection = 'Start' + try: + sel = self._root_widget.get_selected_child() + if sel == self._play_button: + sel_name = 'Start' + elif sel == self._gather_button: + sel_name = 'Gather' + elif sel == self._watch_button: + sel_name = 'Watch' + elif sel == self._how_to_play_button: + sel_name = 'HowToPlay' + elif sel == self._credits_button: + sel_name = 'Credits' + elif sel == self._quit_button: + sel_name = 'Quit' + elif sel == self._demo_menu_button: + sel_name = 'DemoMenu' + else: + print(f'Unknown widget in main menu selection: {sel}.') + sel_name = 'Start' + bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} + except Exception: + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: - # pylint: disable=too-many-branches + try: - # Don't do this for the in-game menu. - if self._in_game: - return - assert bui.app.classic is not None - sel_name = bui.app.ui_v1.main_menu_selection - sel: bui.Widget | None - if sel_name is None: - sel_name = 'Start' - if sel_name == 'HowToPlay': - sel = self._how_to_play_button - elif sel_name == 'Gather': - sel = self._gather_button - elif sel_name == 'Watch': - sel = self._watch_button - elif sel_name == 'Credits': - sel = self._credits_button - elif sel_name == 'Settings': - sel = self._settings_button - elif sel_name == 'Account': - sel = self._account_button - elif sel_name == 'Store': - sel = self._store_button - elif sel_name == 'Quit': - sel = self._quit_button - elif sel_name == 'DemoMenu': - sel = self._demo_menu_button - else: - sel = self._start_button - if sel is not None: - bui.containerwidget(edit=self._root_widget, selected_child=sel) + sel: bui.Widget | None + + sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( + 'sel_name' + ) + assert isinstance(sel_name, (str, type(None))) + if sel_name is None: + sel_name = 'Start' + if sel_name == 'HowToPlay': + sel = self._how_to_play_button + elif sel_name == 'Gather': + sel = self._gather_button + elif sel_name == 'Watch': + sel = self._watch_button + elif sel_name == 'Credits': + sel = self._credits_button + elif sel_name == 'Quit': + sel = self._quit_button + elif sel_name == 'DemoMenu': + sel = self._demo_menu_button + else: + sel = self._play_button + if sel is not None: + bui.containerwidget(edit=self._root_widget, selected_child=sel) + + except Exception: + logging.exception('Error restoring state for %s.', self) def _gather_press(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.gather import GatherWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: + # no-op if we're not currently in control. + if not self.can_change_main_window(): return - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - GatherWindow(origin_widget=self._gather_button).get_root_widget(), - from_window=self._root_widget, + self.main_window_replace( + GatherWindow(origin_widget=self._gather_button) ) def _watch_press(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.watch import WatchWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: + # no-op if we're not currently in control. + if not self.can_change_main_window(): return - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - WatchWindow(origin_widget=self._watch_button).get_root_widget(), - from_window=self._root_widget, + self.main_window_replace( + WatchWindow(origin_widget=self._watch_button), ) def _play_press(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.play import PlayWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: + # no-op if we're not currently in control. + if not self.can_change_main_window(): return - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') + classic = bui.app.classic + if classic is not None: + classic.selecting_private_party_playlist = False - assert bui.app.classic is not None - bui.app.ui_v1.selecting_private_party_playlist = False - bui.app.ui_v1.set_main_menu_window( - PlayWindow(origin_widget=self._start_button).get_root_widget(), - from_window=self._root_widget, - ) - - def _resume(self) -> None: - assert bui.app.classic is not None - bui.app.classic.resume() - # if self._root_widget: - # bui.containerwidget(edit=self._root_widget, - # transition='out_right') - bui.app.ui_v1.clear_main_menu_window(transition='out_right') - - # If there's callbacks waiting for this window to go away, call them. - for call in bui.app.ui_v1.main_menu_resume_callbacks: - call() - del bui.app.ui_v1.main_menu_resume_callbacks[:] + self.main_window_replace(PlayWindow(origin_widget=self._play_button)) diff --git a/src/assets/ba_data/python/bauiv1lib/party.py b/src/assets/ba_data/python/bauiv1lib/party.py index 199db42f..c40a6432 100644 --- a/src/assets/ba_data/python/bauiv1lib/party.py +++ b/src/assets/ba_data/python/bauiv1lib/party.py @@ -48,15 +48,15 @@ class PartyWindow(bui.Window): on_outside_click_call=self.close_with_sound, scale_origin_stack_offset=origin, scale=( - 2.0 + 1.6 if uiscale is bui.UIScale.SMALL - else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 + else 1.3 if uiscale is bui.UIScale.MEDIUM else 0.9 ), stack_offset=( - (0, -10) + (200, -10) if uiscale is bui.UIScale.SMALL else ( - (240, 0) if uiscale is bui.UIScale.MEDIUM else (330, 20) + (260, 0) if uiscale is bui.UIScale.MEDIUM else (370, 60) ) ), ) diff --git a/src/assets/ba_data/python/bauiv1lib/partyqueue.py b/src/assets/ba_data/python/bauiv1lib/partyqueue.py index ef514291..8435562d 100644 --- a/src/assets/ba_data/python/bauiv1lib/partyqueue.py +++ b/src/assets/ba_data/python/bauiv1lib/partyqueue.py @@ -223,7 +223,6 @@ class PartyQueueWindow(bui.Window): def __init__(self, queue_id: str, address: str, port: int): assert bui.app.classic is not None - bui.app.ui_v1.have_party_queue_window = True self._address = address self._port = port self._queue_id = queue_id @@ -329,8 +328,6 @@ class PartyQueueWindow(bui.Window): plus = bui.app.plus assert plus is not None - assert bui.app.classic is not None - bui.app.ui_v1.have_party_queue_window = False plus.add_v1_account_transaction( {'type': 'PARTY_QUEUE_REMOVE', 'q': self._queue_id} ) @@ -564,7 +561,8 @@ class PartyQueueWindow(bui.Window): def on_boost_press(self) -> None: """Boost was pressed.""" from bauiv1lib import account - from bauiv1lib import gettickets + + # from bauiv1lib import gettickets plus = bui.app.plus assert plus is not None @@ -575,7 +573,7 @@ class PartyQueueWindow(bui.Window): if plus.get_v1_account_ticket_count() < self._boost_tickets: bui.getsound('error').play() - gettickets.show_get_tickets_prompt() + # gettickets.show_get_tickets_prompt() return bui.getsound('laserReverse').play() diff --git a/src/assets/ba_data/python/bauiv1lib/play.py b/src/assets/ba_data/python/bauiv1lib/play.py index f33ffe61..5ec63a2c 100644 --- a/src/assets/ba_data/python/bauiv1lib/play.py +++ b/src/assets/ba_data/python/bauiv1lib/play.py @@ -5,31 +5,35 @@ from __future__ import annotations import logging +from typing import override import bascenev1 as bs import bauiv1 as bui -class PlayWindow(bui.Window): +class PlayWindow(bui.MainWindow): """Window for selecting overall play type.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals - import threading # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. - threading.Thread(target=self._preload_modules).start() + bui.app.threadpool_submit_no_wait(self._preload_modules) - # We can currently be used either for main menu duty or for selecting - # playlists (should make this more elegant/general). - assert bui.app.classic is not None - self._is_main_menu = not bui.app.ui_v1.selecting_private_party_playlist + classic = bui.app.classic + assert classic is not None + + # We can currently be used either for main window duty or + # modally for selecting playlists. Ideally we should clean + # things up and make playlist selection go through the + # main-window mechanism. + self._is_main_menu = not classic.selecting_private_party_playlist uiscale = bui.app.ui_v1.uiscale width = 1100 if uiscale is bui.UIScale.SMALL else 800 @@ -37,30 +41,31 @@ class PlayWindow(bui.Window): height = 550 button_width = 400 - scale_origin: tuple[float, float] | None if origin_widget is not None: + + # Need to store this ourself since we can function as a + # non-main window. self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' else: self._transition_out = 'out_right' - scale_origin = None self._r = 'playWindow' super().__init__( root_widget=bui.containerwidget( size=(width, height), - transition=transition, toolbar_visibility='menu_full', - scale_origin_stack_offset=scale_origin, scale=( - 1.6 + 1.35 if uiscale is bui.UIScale.SMALL else 0.9 if uiscale is bui.UIScale.MEDIUM else 0.8 ), - stack_offset=(0, 0) if uiscale is bui.UIScale.SMALL else (0, 0), - ) + stack_offset=( + (0, 20) if uiscale is bui.UIScale.SMALL else (0, 0) + ), + ), + transition=transition, + origin_widget=origin_widget, ) self._back_button = back_button = btn = bui.buttonwidget( parent=self._root_widget, @@ -101,18 +106,14 @@ class PlayWindow(bui.Window): size=(60, 60), label=bui.charstr(bui.SpecialChar.BACK), ) - if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: bui.textwidget(edit=txt, text='') v = height - (110 if self._is_main_menu else 90) v -= 100 clr = (0.6, 0.7, 0.6, 1.0) v -= 280 if self._is_main_menu else 180 - v += ( - 30 - if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL - else 0 - ) + v += 30 if uiscale is bui.UIScale.SMALL else 0 hoffs = x_offs + 80 if self._is_main_menu else x_offs - 100 scl = 1.13 if self._is_main_menu else 0.68 @@ -153,7 +154,7 @@ class PlayWindow(bui.Window): on_activate_call=self._coop, ) - if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: bui.widget( edit=btn, left_widget=bui.get_special_widget('back_button'), @@ -256,12 +257,11 @@ class PlayWindow(bui.Window): on_activate_call=self._team_tourney, ) - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - up_widget=bui.get_special_widget('tickets_plus_button'), - right_widget=bui.get_special_widget('party_button'), - ) + 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( @@ -489,11 +489,12 @@ class PlayWindow(bui.Window): color=clr, ) - if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: back_button.delete() bui.containerwidget( edit=self._root_widget, on_cancel_call=self._back, + # cancel_button=bui.get_special_widget('back_button'), selected_child=( self._coop_button if self._is_main_menu @@ -514,7 +515,20 @@ class PlayWindow(bui.Window): self._restore_state() - # noinspection PyUnresolvedReferences + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + @staticmethod def _preload_modules() -> None: """Preload modules we use; avoids hitches (called in bg thread).""" @@ -526,30 +540,22 @@ class PlayWindow(bui.Window): def _back(self) -> None: # pylint: disable=cyclic-import - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: + # no-op if we're not currently in control. + if not self.can_change_main_window(): return if self._is_main_menu: - from bauiv1lib.mainmenu import MainMenuWindow - - self._save_state() - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) + self.main_window_back() else: from bauiv1lib.gather import GatherWindow - self._save_state() assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - GatherWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, + + self._save_state() + bui.app.ui_v1.set_main_window( + GatherWindow(transition='in_left'), + from_window=self, + is_back=True, ) bui.containerwidget( edit=self._root_widget, transition=self._transition_out @@ -560,8 +566,8 @@ class PlayWindow(bui.Window): from bauiv1lib.account import show_sign_in_prompt from bauiv1lib.coop.browser import CoopBrowserWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: + # no-op if we're not currently in control. + if not self.can_change_main_window(): return plus = bui.app.plus @@ -573,48 +579,43 @@ class PlayWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - CoopBrowserWindow( - origin_widget=self._coop_button - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + CoopBrowserWindow(origin_widget=self._coop_button), from_window=self ) def _team_tourney(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.playlist.browser import PlaylistBrowserWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: + # no-op if we're not currently in control. + if not self.can_change_main_window(): return self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( + + self.main_window_replace( PlaylistBrowserWindow( origin_widget=self._teams_button, sessiontype=bs.DualTeamSession - ).get_root_widget(), - from_window=self._root_widget, + ) ) def _free_for_all(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.playlist.browser import PlaylistBrowserWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: + # no-op if we're not currently in control. + if not self.can_change_main_window(): return self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( + bui.app.ui_v1.set_main_window( PlaylistBrowserWindow( origin_widget=self._free_for_all_button, sessiontype=bs.FreeForAllSession, - ).get_root_widget(), - from_window=self._root_widget, + ), + from_window=self, ) def _draw_dude( diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/addgame.py b/src/assets/ba_data/python/bauiv1lib/playlist/addgame.py index 317c9e9e..02ef065a 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/addgame.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/addgame.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import bascenev1 as bs import bauiv1 as bui @@ -13,13 +13,14 @@ if TYPE_CHECKING: from bauiv1lib.playlist.editcontroller import PlaylistEditController -class PlaylistAddGameWindow(bui.Window): +class PlaylistAddGameWindow(bui.MainWindow): """Window for selecting a game type to add to a playlist.""" def __init__( self, editcontroller: PlaylistEditController, - transition: str = 'in_right', + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, ): self._editcontroller = editcontroller self._r = 'addGameWindow' @@ -38,27 +39,32 @@ class PlaylistAddGameWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), - transition=transition, scale=( - 2.17 + 1.95 if uiscale is bui.UIScale.SMALL else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, 1) if uiscale is bui.UIScale.SMALL else (0, 0), - ) + toolbar_visibility='menu_minimal', + ), + transition=transition, + origin_widget=origin_widget, ) - self._back_button = bui.buttonwidget( - parent=self._root_widget, - position=(58 + x_inset, self._height - 53), - size=(165, 70), - scale=0.75, - text_scale=1.2, - label=bui.Lstr(resource='backText'), - autoselect=True, - button_type='back', - on_activate_call=self._back, - ) + if uiscale is bui.UIScale.SMALL: + self._back_button = bui.get_special_widget('back_button') + else: + self._back_button = bui.buttonwidget( + parent=self._root_widget, + position=(58 + x_inset, self._height - 53), + size=(165, 70), + scale=0.75, + text_scale=1.2, + label=bui.Lstr(resource='backText'), + autoselect=True, + button_type='back', + on_activate_call=self.main_window_back, + ) self._select_button = select_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - (172 + x_inset), self._height - 50), @@ -70,11 +76,10 @@ class PlaylistAddGameWindow(bui.Window): on_activate_call=self._add, ) - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=select_button, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=select_button, + right_widget=bui.get_special_widget('squad_button'), + ) bui.textwidget( parent=self._root_widget, @@ -130,11 +135,18 @@ class PlaylistAddGameWindow(bui.Window): self._column: bui.Widget | None = None v -= 35 - bui.containerwidget( - edit=self._root_widget, - cancel_button=self._back_button, - start_button=select_button, - ) + + if uiscale is bui.UIScale.SMALL: + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + else: + bui.containerwidget( + edit=self._root_widget, + cancel_button=self._back_button, + ) + bui.containerwidget(edit=self._root_widget, start_button=select_button) + self._selected_game_type: type[bs.GameActivity] | None = None bui.containerwidget( @@ -154,6 +166,23 @@ class PlaylistAddGameWindow(bui.Window): # game loading is complete. self._refresh() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # Avoid dereferencing self from the lambda or we'll keep + # ourself alive indefinitely. + editcontroller = self._editcontroller + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + editcontroller=editcontroller, + ) + ) + def _on_game_types_loaded( self, gametypes: list[type[bs.GameActivity]] ) -> None: @@ -262,5 +291,5 @@ class PlaylistAddGameWindow(bui.Window): ), ) - def _back(self) -> None: - self._editcontroller.add_game_cancelled() + # def _back(self) -> None: + # self._editcontroller.add_game_cancelled() diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/browser.py b/src/assets/ba_data/python/bauiv1lib/playlist/browser.py index f8b1a73f..ef0fcf57 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/browser.py @@ -4,15 +4,16 @@ from __future__ import annotations -import logging import copy import math +import logging +from typing import override import bascenev1 as bs import bauiv1 as bui -class PlaylistBrowserWindow(bui.Window): +class PlaylistBrowserWindow(bui.MainWindow): """Window for starting teams games.""" def __init__( @@ -21,28 +22,13 @@ class PlaylistBrowserWindow(bui.Window): transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): - # pylint: disable=too-many-statements # pylint: disable=cyclic-import from bauiv1lib.playlist import PlaylistTypeVars - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - - assert bui.app.classic is not None - # Store state for when we exit the next game. if issubclass(sessiontype, bs.DualTeamSession): - bui.app.ui_v1.set_main_menu_location('Team Game Select') bui.set_analytics_screen('Teams Window') elif issubclass(sessiontype, bs.FreeForAllSession): - bui.app.ui_v1.set_main_menu_location('Free-for-All Game Select') bui.set_analytics_screen('FreeForAll Window') else: raise TypeError(f'Invalid sessiontype: {sessiontype}.') @@ -65,7 +51,7 @@ class PlaylistBrowserWindow(bui.Window): self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0 x_inset = 150 if uiscale is bui.UIScale.SMALL else 0 self._height = ( - 480 + 440 if uiscale is bui.UIScale.SMALL else 510 if uiscale is bui.UIScale.MEDIUM else 580 ) @@ -75,18 +61,22 @@ class PlaylistBrowserWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), - transition=transition, - toolbar_visibility='menu_full', - scale_origin_stack_offset=scale_origin, + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), scale=( - 1.69 + 1.83 if uiscale is bui.UIScale.SMALL else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9 ), stack_offset=( - (0, -26) if uiscale is bui.UIScale.SMALL else (0, 0) + (0, -56) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) self._back_button: bui.Widget | None = bui.buttonwidget( @@ -102,19 +92,22 @@ class PlaylistBrowserWindow(bui.Window): bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) - txt = self._title_text = bui.textwidget( + self._title_text = bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height - 41), + position=( + self._width * 0.5, + self._height - (32 if uiscale is bui.UIScale.SMALL else 41), + ), size=(0, 0), text=self._pvars.window_title_name, - scale=1.3, + scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3), res_scale=1.5, color=bui.app.ui_v1.heading_color, h_align='center', v_align='center', ) - if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars: - bui.textwidget(edit=txt, text='') + # if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars: + # bui.textwidget(edit=txt, text='') bui.buttonwidget( edit=self._back_button, @@ -124,7 +117,7 @@ class PlaylistBrowserWindow(bui.Window): label=bui.charstr(bui.SpecialChar.BACK), ) - if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars: + if uiscale is bui.UIScale.SMALL: self._back_button.delete() self._back_button = None bui.containerwidget( @@ -135,9 +128,7 @@ class PlaylistBrowserWindow(bui.Window): scroll_offs = 0 self._scroll_width = self._width - (100 + 2 * x_inset) self._scroll_height = self._height - ( - 146 - if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars - else 136 + 146 if uiscale is bui.UIScale.SMALL else 136 ) self._scrollwidget = bui.scrollwidget( parent=self._root_widget, @@ -160,6 +151,27 @@ class PlaylistBrowserWindow(bui.Window): 1.0, bui.WeakCall(self._update), repeat=True ) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # Pull things out of self here; if we do it below in the lambda + # then we keep self alive. + sessiontype = self._sessiontype + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + sessiontype=sessiontype, + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + def _ensure_standard_playlists_exist(self) -> None: plus = bui.app.plus assert plus is not None @@ -418,23 +430,15 @@ class PlaylistBrowserWindow(bui.Window): position=pos, ) - if ( - x == 0 - and bui.app.ui_v1.use_toolbars - and uiscale is bui.UIScale.SMALL - ): + if x == 0 and uiscale is bui.UIScale.SMALL: bui.widget( edit=btn, left_widget=bui.get_special_widget('back_button'), ) - if ( - x == columns - 1 - and bui.app.ui_v1.use_toolbars - and uiscale is bui.UIScale.SMALL - ): + if x == columns - 1 and uiscale is bui.UIScale.SMALL: bui.widget( edit=btn, - right_widget=bui.get_special_widget('party_button'), + right_widget=bui.get_special_widget('squad_button'), ) bui.buttonwidget( edit=btn, @@ -687,17 +691,17 @@ class PlaylistBrowserWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( + bui.app.ui_v1.set_main_window( PlaylistCustomizeBrowserWindow( origin_widget=self._customize_button, sessiontype=self._sessiontype, - ).get_root_widget(), - from_window=self._root_widget, + ), + from_window=self, ) def _on_back_press(self) -> None: # pylint: disable=cyclic-import - from bauiv1lib.play import PlayWindow + # from bauiv1lib.play import PlayWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: @@ -715,15 +719,16 @@ class PlaylistBrowserWindow(bui.Window): ) cfg.commit() - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - PlayWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + self.main_window_back() + + # self._save_state() + # bui.containerwidget( + # edit=self._root_widget, transition=self._transition_out + # ) + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # PlayWindow(transition='in_left'), from_window=self, is_back=True + # ) def _save_state(self) -> None: try: diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py b/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py index 789e53fd..08658d5f 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py @@ -7,7 +7,7 @@ from __future__ import annotations import copy import time import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import bascenev1 as bs import bauiv1 as bui @@ -15,16 +15,21 @@ import bauiv1 as bui if TYPE_CHECKING: from typing import Any +REQUIRE_PRO = False -class PlaylistCustomizeBrowserWindow(bui.Window): +# TEMP +UNDER_CONSTRUCTION = True + + +class PlaylistCustomizeBrowserWindow(bui.MainWindow): """Window for viewing a playlist.""" def __init__( self, sessiontype: type[bs.Session], - transition: str = 'in_right', - select_playlist: str | None = None, + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, + select_playlist: str | None = None, ): # Yes this needs tidying. # pylint: disable=too-many-locals @@ -32,22 +37,13 @@ class PlaylistCustomizeBrowserWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib import playlist - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - self._sessiontype = sessiontype self._pvars = playlist.PlaylistTypeVars(sessiontype) self._max_playlists = 30 self._r = 'gameListWindow' assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - self._width = 850.0 if uiscale is bui.UIScale.SMALL else 650.0 + self._width = 970.0 if uiscale is bui.UIScale.SMALL else 650.0 x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 self._height = ( 380.0 @@ -59,29 +55,47 @@ class PlaylistCustomizeBrowserWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), - transition=transition, - scale_origin_stack_offset=scale_origin, scale=( - 2.05 + 1.83 if uiscale is bui.UIScale.SMALL else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ), + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), stack_offset=( (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) - self._back_button = back_button = btn = bui.buttonwidget( - parent=self._root_widget, - position=(43 + x_inset, self._height - 60), - size=(160, 68), - scale=0.77, - autoselect=True, - text_scale=1.3, - label=bui.Lstr(resource='backText'), - button_type='back', - ) + self._back_button: bui.Widget | None + if uiscale is bui.UIScale.SMALL: + self._back_button = None + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self._back + ) + else: + self._back_button = bui.buttonwidget( + parent=self._root_widget, + position=(43 + x_inset, self._height - 60), + size=(160, 68), + scale=0.77, + autoselect=True, + text_scale=1.3, + label=bui.Lstr(resource='backText'), + button_type='back', + ) + bui.buttonwidget( + edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=bui.charstr(bui.SpecialChar.BACK), + ) bui.textwidget( parent=self._root_widget, @@ -97,13 +111,6 @@ class PlaylistCustomizeBrowserWindow(bui.Window): v_align='center', ) - bui.buttonwidget( - edit=btn, - button_type='backSmall', - size=(60, 60), - label=bui.charstr(bui.SpecialChar.BACK), - ) - v = self._height - 59.0 h = 41 + x_inset b_color = (0.6, 0.53, 0.63) @@ -261,7 +268,9 @@ class PlaylistCustomizeBrowserWindow(bui.Window): size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10), highlight=False, ) - bui.widget(edit=back_button, right_widget=scrollwidget) + if self._back_button is not None: + bui.widget(edit=self._back_button, right_widget=scrollwidget) + self._columnwidget = bui.columnwidget( parent=scrollwidget, border=2, margin=0 ) @@ -279,11 +288,7 @@ class PlaylistCustomizeBrowserWindow(bui.Window): bui.widget( edit=scrollwidget, left_widget=new_button, - right_widget=( - bui.get_special_widget('party_button') - if bui.app.ui_v1.use_toolbars - else None - ), + right_widget=bui.get_special_widget('squad_button'), ) # make sure config exists @@ -298,8 +303,13 @@ class PlaylistCustomizeBrowserWindow(bui.Window): self._refresh(select_playlist=select_playlist) - bui.buttonwidget(edit=back_button, on_activate_call=self._back) - bui.containerwidget(edit=self._root_widget, cancel_button=back_button) + if self._back_button is not None: + bui.buttonwidget( + edit=self._back_button, on_activate_call=self._back + ) + bui.containerwidget( + edit=self._root_widget, cancel_button=self._back_button + ) bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget) @@ -309,15 +319,38 @@ class PlaylistCustomizeBrowserWindow(bui.Window): ) self._update() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # Avoid dereferencing self within the lambda or we'll keep + # ourself alive indefinitely. + stype = self._sessiontype + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + sessiontype=stype, + ) + ) + + # @override + # def on_main_window_close(self) -> None: + # self._save_state() + def _update(self) -> None: assert bui.app.classic is not None have = bui.app.classic.accounts.have_pro_options() for lock in self._lock_images: - bui.imagewidget(edit=lock, opacity=0.0 if have else 1.0) + bui.imagewidget( + edit=lock, opacity=0.0 if (have or not REQUIRE_PRO) else 1.0 + ) def _back(self) -> None: # pylint: disable=cyclic-import - from bauiv1lib.playlist import browser + # from bauiv1lib.playlist import browser # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: @@ -330,16 +363,18 @@ class PlaylistCustomizeBrowserWindow(bui.Window): ) cfg.commit() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - browser.PlaylistBrowserWindow( - transition='in_left', sessiontype=self._sessiontype - ).get_root_widget(), - from_window=self._root_widget, - ) + self.main_window_back() + # bui.containerwidget( + # edit=self._root_widget, transition=self._transition_out + # ) + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # browser.PlaylistBrowserWindow( + # transition='in_left', sessiontype=self._sessiontype + # ), + # from_window=self, + # is_back=True, + # ) def _select(self, name: str, index: int) -> None: self._selected_playlist_name = name @@ -418,7 +453,14 @@ class PlaylistCustomizeBrowserWindow(bui.Window): # Hitting up from top widget should jump to 'back' if index == 0: - bui.widget(edit=txtw, up_widget=self._back_button) + bui.widget( + edit=txtw, + up_widget=( + self._back_button + if self._back_button is not None + else bui.get_special_widget('back_button') + ), + ) self._playlist_widgets.append(txtw) @@ -469,8 +511,12 @@ class PlaylistCustomizeBrowserWindow(bui.Window): from bauiv1lib.playlist.editcontroller import PlaylistEditController from bauiv1lib.purchase import PurchaseWindow + if UNDER_CONSTRUCTION: + bui.screenmessage('UNDER CONSTRUCTION') + return + assert bui.app.classic is not None - if not bui.app.classic.accounts.have_pro_options(): + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return @@ -500,8 +546,12 @@ class PlaylistCustomizeBrowserWindow(bui.Window): from bauiv1lib.playlist.editcontroller import PlaylistEditController from bauiv1lib.purchase import PurchaseWindow + if UNDER_CONSTRUCTION: + bui.screenmessage('UNDER CONSTRUCTION') + return + assert bui.app.classic is not None - if not bui.app.classic.accounts.have_pro_options(): + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_playlist_name is None: @@ -584,7 +634,7 @@ class PlaylistCustomizeBrowserWindow(bui.Window): assert plus is not None assert bui.app.classic is not None - if not bui.app.classic.accounts.have_pro_options(): + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return @@ -626,7 +676,7 @@ class PlaylistCustomizeBrowserWindow(bui.Window): from bauiv1lib.confirm import ConfirmWindow assert bui.app.classic is not None - if not bui.app.classic.accounts.have_pro_options(): + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return @@ -666,7 +716,7 @@ class PlaylistCustomizeBrowserWindow(bui.Window): assert plus is not None assert bui.app.classic is not None - if not bui.app.classic.accounts.have_pro_options(): + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_playlist_name is None: diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/edit.py b/src/assets/ba_data/python/bauiv1lib/playlist/edit.py index 3a3fccab..43a39cfa 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/edit.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/edit.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, cast, override import bascenev1 as bs import bauiv1 as bui @@ -14,13 +14,14 @@ if TYPE_CHECKING: from bauiv1lib.playlist.editcontroller import PlaylistEditController -class PlaylistEditWindow(bui.Window): +class PlaylistEditWindow(bui.MainWindow): """Window for editing an individual game playlist.""" def __init__( self, editcontroller: PlaylistEditController, - transition: str = 'in_right', + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals @@ -43,16 +44,17 @@ class PlaylistEditWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), - transition=transition, scale=( - 2.0 + 1.8 if uiscale is bui.UIScale.SMALL else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, -16) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) cancel_button = bui.buttonwidget( parent=self._root_widget, @@ -74,11 +76,10 @@ class PlaylistEditWindow(bui.Window): text_scale=1.2, ) - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, + right_widget=bui.get_special_widget('squad_button'), + ) bui.widget( edit=cancel_button, @@ -270,31 +271,49 @@ class PlaylistEditWindow(bui.Window): edit=self._root_widget, selected_child=scrollwidget ) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + editcontroller = self._editcontroller + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + editcontroller=editcontroller, + ) + ) + def _set_ui_selection(self, selection: str) -> None: self._editcontroller.set_edit_ui_selection(selection) def _cancel(self) -> None: - from bauiv1lib.playlist.customizebrowser import ( - PlaylistCustomizeBrowserWindow, - ) + # from bauiv1lib.playlist.customizebrowser import ( + # PlaylistCustomizeBrowserWindow, + # ) # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return bui.getsound('powerdown01').play() - bui.containerwidget(edit=self._root_widget, transition='out_right') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - PlaylistCustomizeBrowserWindow( - transition='in_left', - sessiontype=self._editcontroller.get_session_type(), - select_playlist=( - self._editcontroller.get_existing_playlist_name() - ), - ).get_root_widget(), - from_window=self._root_widget, - ) + self.main_window_back() + + # bui.containerwidget(edit=self._root_widget, transition='out_right') + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # PlaylistCustomizeBrowserWindow( + # transition='in_left', + # sessiontype=self._editcontroller.get_session_type(), + # select_playlist=( + # self._editcontroller.get_existing_playlist_name() + # ), + # ), + # from_window=self, + # is_back=True, + # ) def _add(self) -> None: # Store list name then tell the session to perform an add. @@ -379,13 +398,14 @@ class PlaylistEditWindow(bui.Window): bui.containerwidget(edit=self._root_widget, transition='out_right') bui.getsound('gunCocking').play() assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( + bui.app.ui_v1.set_main_window( PlaylistCustomizeBrowserWindow( transition='in_left', sessiontype=self._editcontroller.get_session_type(), select_playlist=new_name, - ).get_root_widget(), - from_window=self._root_widget, + ), + from_window=self, + is_back=True, ) def _save_press_with_sound(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py b/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py index 7ed9a92a..2fb584c6 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py @@ -89,10 +89,8 @@ class PlaylistEditController: self._edit_ui_selection = 'add_button' assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - PlaylistEditWindow( - editcontroller=self, transition=transition - ).get_root_widget(), + bui.app.ui_v1.set_main_window( + PlaylistEditWindow(editcontroller=self, transition=transition), from_window=False, # Disable this check. ) @@ -149,10 +147,9 @@ class PlaylistEditController: from bauiv1lib.playlist.addgame import PlaylistAddGameWindow assert bui.app.classic is not None - bui.app.ui_v1.clear_main_menu_window(transition='out_left') - bui.app.ui_v1.set_main_menu_window( - PlaylistAddGameWindow(editcontroller=self).get_root_widget(), - from_window=None, + bui.app.ui_v1.clear_main_window() + bui.app.ui_v1.set_main_window( + PlaylistAddGameWindow(editcontroller=self), from_window=None ) def edit_game_pressed(self) -> None: @@ -168,18 +165,17 @@ class PlaylistEditController: settings=self._playlist[self._selected_index], ) - def add_game_cancelled(self) -> None: - """(internal)""" - from bauiv1lib.playlist.edit import PlaylistEditWindow + # def add_game_cancelled(self) -> None: + # """(internal)""" + # from bauiv1lib.playlist.edit import PlaylistEditWindow - assert bui.app.classic is not None - bui.app.ui_v1.clear_main_menu_window(transition='out_right') - bui.app.ui_v1.set_main_menu_window( - PlaylistEditWindow( - editcontroller=self, transition='in_left' - ).get_root_widget(), - from_window=None, - ) + # assert bui.app.classic is not None + # bui.app.ui_v1.clear_main_window(transition='out_right') + # bui.app.ui_v1.set_main_window( + # PlaylistEditWindow(editcontroller=self, transition='in_left'), + # from_window=None, + # is_back=True, + # ) def _show_edit_ui( self, gametype: type[bs.GameActivity], settings: dict[str, Any] | None @@ -204,22 +200,24 @@ class PlaylistEditController: # If we were editing, go back to our list. if self._editing_game: bui.getsound('powerdown01').play() - bui.app.ui_v1.clear_main_menu_window(transition='out_right') - bui.app.ui_v1.set_main_menu_window( + bui.app.ui_v1.clear_main_window() + bui.app.ui_v1.set_main_window( PlaylistEditWindow( editcontroller=self, transition='in_left' - ).get_root_widget(), + ), from_window=None, + is_back=True, ) # Otherwise we were adding; go back to the add type choice list. else: - bui.app.ui_v1.clear_main_menu_window(transition='out_right') - bui.app.ui_v1.set_main_menu_window( + bui.app.ui_v1.clear_main_window() + bui.app.ui_v1.set_main_window( PlaylistAddGameWindow( editcontroller=self, transition='in_left' - ).get_root_widget(), + ), from_window=None, + is_back=True, ) else: # Make sure type is in there. @@ -237,10 +235,9 @@ class PlaylistEditController: self._selected_index = insert_index bui.getsound('gunCocking').play() - bui.app.ui_v1.clear_main_menu_window(transition='out_right') - bui.app.ui_v1.set_main_menu_window( - PlaylistEditWindow( - editcontroller=self, transition='in_left' - ).get_root_widget(), + bui.app.ui_v1.clear_main_window() + bui.app.ui_v1.set_main_window( + PlaylistEditWindow(editcontroller=self, transition='in_left'), from_window=None, + is_back=True, ) diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py b/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py index 993aac37..5dfbe182 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py @@ -7,7 +7,7 @@ from __future__ import annotations import copy import random import logging -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, cast, override import bascenev1 as bs import bauiv1 as bui @@ -16,7 +16,7 @@ if TYPE_CHECKING: from typing import Any, Callable -class PlaylistEditGameWindow(bui.Window): +class PlaylistEditGameWindow(bui.MainWindow): """Window for editing a game config.""" def __init__( @@ -26,7 +26,8 @@ class PlaylistEditGameWindow(bui.Window): config: dict[str, Any] | None, completion_call: Callable[[dict[str, Any] | None], Any], default_selection: str | None = None, - transition: str = 'in_right', + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, edit_info: dict[str, Any] | None = None, ): # pylint: disable=too-many-branches @@ -64,6 +65,7 @@ class PlaylistEditGameWindow(bui.Window): bui.screenmessage(bui.Lstr(resource='noValidMapsErrorText')) raise RuntimeError('No valid maps found.') + self._config = config self._settings_defs = gametype.get_available_settings(sessiontype) self._completion_call = completion_call @@ -123,16 +125,17 @@ class PlaylistEditGameWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(width, height + top_extra), - transition=transition, scale=( - 2.19 + 1.95 if uiscale is bui.UIScale.SMALL else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, -17) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) btn = bui.buttonwidget( @@ -161,13 +164,12 @@ class PlaylistEditGameWindow(bui.Window): label=( bui.Lstr(resource=f'{self._r}.addGameText') if is_add - else bui.Lstr(resource='doneText') + else bui.Lstr(resource='applyText') ), ) - if bui.app.ui_v1.use_toolbars: - pbtn = bui.get_special_widget('party_button') - bui.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn) + pbtn = bui.get_special_widget('squad_button') + bui.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn) bui.textwidget( parent=self._root_widget, @@ -509,6 +511,29 @@ class PlaylistEditGameWindow(bui.Window): edit=self._subcontainer, selected_child=map_button ) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # Pull things out of self here so we don't refer to self in the + # lambda below which would keep us alive. + gametype = self._gametype + sessiontype = self._sessiontype + config = self._config + completion_call = self._completion_call + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + gametype=gametype, + sessiontype=sessiontype, + config=config, + completion_call=completion_call, + ) + ) + def _get_localized_setting_name(self, name: str) -> bui.Lstr: return bui.Lstr(translate=('settingNames', name)) @@ -523,15 +548,15 @@ class PlaylistEditGameWindow(bui.Window): # Replace ourself with the map-select UI. bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( + bui.app.ui_v1.set_main_window( PlaylistMapSelectWindow( self._gametype, self._sessiontype, copy.deepcopy(self._getconfig()), self._edit_info, self._completion_call, - ).get_root_widget(), - from_window=self._root_widget, + ), + from_window=self, ) def _choice_inc( diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py b/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py index 8e2cc8ea..6b7fc547 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: import bascenev1 as bs -class PlaylistMapSelectWindow(bui.Window): +class PlaylistMapSelectWindow(bui.MainWindow): """Window to select a map.""" def __init__( @@ -25,8 +25,11 @@ class PlaylistMapSelectWindow(bui.Window): config: dict[str, Any], edit_info: dict[str, Any], completion_call: Callable[[dict[str, Any] | None], Any], - transition: str = 'in_right', + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, ): + # pylint: disable=too-many-locals + from bascenev1 import get_filtered_map_name self._gametype = gametype @@ -56,16 +59,18 @@ class PlaylistMapSelectWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(width, height + top_extra), - transition=transition, + # transition=transition, scale=( - 2.17 + 1.95 if uiscale is bui.UIScale.SMALL else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, -27) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) self._cancel_button = btn = bui.buttonwidget( @@ -76,7 +81,7 @@ class PlaylistMapSelectWindow(bui.Window): text_scale=1.0, autoselect=True, label=bui.Lstr(resource='cancelText'), - on_activate_call=self._cancel, + on_activate_call=self.main_window_back, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) @@ -198,10 +203,10 @@ class PlaylistMapSelectWindow(bui.Window): bui.widget(edit=btn, left_widget=self._cancel_button) if y == 0: bui.widget(edit=btn, up_widget=self._cancel_button) - if x == columns - 1 and bui.app.ui_v1.use_toolbars: + if x == columns - 1: bui.widget( edit=btn, - right_widget=bui.get_special_widget('party_button'), + right_widget=bui.get_special_widget('squad_button'), ) bui.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) @@ -267,51 +272,53 @@ class PlaylistMapSelectWindow(bui.Window): self._refresh(select_get_more_maps_button=True) def _select(self, map_name: str) -> None: - from bauiv1lib.playlist.editgame import PlaylistEditGameWindow + # from bauiv1lib.playlist.editgame import PlaylistEditGameWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return self._config['settings']['map'] = map_name - bui.containerwidget(edit=self._root_widget, transition='out_right') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - PlaylistEditGameWindow( - self._gametype, - self._sessiontype, - self._config, - self._completion_call, - default_selection='map', - transition='in_left', - edit_info=self._edit_info, - ).get_root_widget(), - from_window=self._root_widget, - ) + self.main_window_back() + # bui.containerwidget(edit=self._root_widget, transition='out_right') + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # PlaylistEditGameWindow( + # self._gametype, + # self._sessiontype, + # self._config, + # self._completion_call, + # default_selection='map', + # transition='in_left', + # edit_info=self._edit_info, + # ), + # from_window=self, + # ) def _select_with_delay(self, map_name: str) -> None: bui.lock_all_input() bui.apptimer(0.1, bui.unlock_all_input) bui.apptimer(0.1, bui.WeakCall(self._select, map_name)) - def _cancel(self) -> None: - from bauiv1lib.playlist.editgame import PlaylistEditGameWindow + # def _cancel(self) -> None: + # from bauiv1lib.playlist.editgame import PlaylistEditGameWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return + # # no-op if our underlying widget is dead or on its way out. + # if not self._root_widget or self._root_widget.transitioning_out: + # return - bui.containerwidget(edit=self._root_widget, transition='out_right') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - PlaylistEditGameWindow( - self._gametype, - self._sessiontype, - self._config, - self._completion_call, - default_selection='map', - transition='in_left', - edit_info=self._edit_info, - ).get_root_widget(), - from_window=self._root_widget, - ) + # bui.containerwidget(edit=self._root_widget, transition='out_right') + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # PlaylistEditGameWindow( + # self._gametype, + # self._sessiontype, + # self._config, + # self._completion_call, + # default_selection='map', + # transition='in_left', + # edit_info=self._edit_info, + # ), + # from_window=self, + # is_back=True, + # ) diff --git a/src/assets/ba_data/python/bauiv1lib/playoptions.py b/src/assets/ba_data/python/bauiv1lib/playoptions.py index cc69b3d4..2b81de6a 100644 --- a/src/assets/ba_data/python/bauiv1lib/playoptions.py +++ b/src/assets/ba_data/python/bauiv1lib/playoptions.py @@ -41,8 +41,9 @@ class PlayOptionsWindow(PopupWindow): # We behave differently if we're being used for playlist selection # vs starting a game directly (should make this more elegant). - assert bui.app.classic is not None - self._selecting_mode = bui.app.ui_v1.selecting_private_party_playlist + classic = bui.app.classic + assert classic is not None + self._selecting_mode = classic.selecting_private_party_playlist self._do_randomize_val = bui.app.config.get( self._pvars.config_name + ' Playlist Randomize', 0 @@ -512,8 +513,8 @@ class PlayOptionsWindow(PopupWindow): # Note: this is a wonky situation where we aren't actually # the main window but we set it on behalf of the main window # that popped us up. - bui.app.ui_v1.set_main_menu_window( - GatherWindow(transition='in_right').get_root_widget(), + bui.app.ui_v1.set_main_window( + GatherWindow(transition='in_right'), from_window=False, # Disable this test. ) self._transition_out(transition='out_left') diff --git a/src/assets/ba_data/python/bauiv1lib/popup.py b/src/assets/ba_data/python/bauiv1lib/popup.py index ef830f85..df9d111c 100644 --- a/src/assets/ba_data/python/bauiv1lib/popup.py +++ b/src/assets/ba_data/python/bauiv1lib/popup.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, override import bauiv1 as bui if TYPE_CHECKING: - from typing import Any, Sequence, Callable + from typing import Any, Sequence, Callable, Literal class PopupWindow: @@ -27,7 +27,12 @@ class PopupWindow: bg_color: tuple[float, float, float] = (0.35, 0.55, 0.15), focus_position: tuple[float, float] = (0, 0), focus_size: tuple[float, float] | None = None, - toolbar_visibility: str = 'menu_minimal_no_back', + toolbar_visibility: Literal[ + 'inherit', + 'menu_minimal_no_back', + 'menu_store_no_back', + ] = 'menu_minimal_no_back', + edge_buffer_scale: float = 1.0, ): # pylint: disable=too-many-locals if focus_size is None: @@ -45,7 +50,7 @@ class PopupWindow: # we now need to ensure that we're all onscreen by scaling down if # need be and clamping it to the UI bounds. bounds = bui.uibounds() - edge_buffer = 15 + edge_buffer = 15 * edge_buffer_scale bounds_width = bounds[1] - bounds[0] - edge_buffer * 2 bounds_height = bounds[3] - bounds[2] - edge_buffer * 2 diff --git a/src/assets/ba_data/python/bauiv1lib/profile/browser.py b/src/assets/ba_data/python/bauiv1lib/profile/browser.py index 4529cdb6..95464e03 100644 --- a/src/assets/ba_data/python/bauiv1lib/profile/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/profile/browser.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import bauiv1 as bui import bascenev1 as bs @@ -14,18 +14,17 @@ if TYPE_CHECKING: from typing import Any -class ProfileBrowserWindow(bui.Window): +class ProfileBrowserWindow(bui.MainWindow): """Window for browsing player profiles.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', in_main_menu: bool = True, selected_profile: str | None = None, origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements - # pylint: disable=too-many-locals self._in_main_menu = in_main_menu if self._in_main_menu: back_label = bui.Lstr(resource='backText') @@ -46,15 +45,11 @@ class ProfileBrowserWindow(bui.Window): assert bui.app.classic is not None bui.app.classic.pause() - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None + # Need to handle out-transitions ourself for modal mode. if origin_widget is not None: self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' else: self._transition_out = 'out_right' - scale_origin = None self._r = 'playerProfilesWindow' @@ -67,30 +62,48 @@ class ProfileBrowserWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), - transition=transition, - scale_origin_stack_offset=scale_origin, - scale=( - 2.2 + toolbar_visibility=( + 'menu_minimal' if uiscale is bui.UIScale.SMALL - else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0 + else 'menu_full' + ), + scale=( + 2.0 + if uiscale is bui.UIScale.SMALL + else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, -14) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) - self._back_button = btn = bui.buttonwidget( - parent=self._root_widget, - position=(40 + x_inset, self._height - 59), - size=(120, 60), - scale=0.8, - label=back_label, - button_type='back' if self._in_main_menu else None, - autoselect=True, - on_activate_call=self._back, - ) - bui.containerwidget(edit=self._root_widget, cancel_button=btn) + if bui.app.ui_v1.uiscale is bui.UIScale.SMALL: + self._back_button = bui.get_special_widget('back_button') + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self._back + ) + else: + self._back_button = btn = bui.buttonwidget( + parent=self._root_widget, + position=(40 + x_inset, self._height - 59), + size=(120, 60), + scale=0.8, + label=back_label, + button_type='back' if self._in_main_menu else None, + autoselect=True, + on_activate_call=self._back, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + if self._in_main_menu: + bui.buttonwidget( + edit=btn, + button_type='backSmall', + size=(60, 60), + label=bui.charstr(bui.SpecialChar.BACK), + ) bui.textwidget( parent=self._root_widget, @@ -104,14 +117,6 @@ class ProfileBrowserWindow(bui.Window): v_align='center', ) - if self._in_main_menu: - bui.buttonwidget( - edit=btn, - button_type='backSmall', - size=(60, 60), - label=bui.charstr(bui.SpecialChar.BACK), - ) - scroll_height = self._height - 140.0 self._scroll_width = self._width - (188 + x_inset * 2) v = self._height - 84.0 @@ -203,6 +208,20 @@ class ProfileBrowserWindow(bui.Window): self._refresh() self._restore_state() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + def _new_profile(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.profile.edit import EditProfileWindow @@ -249,11 +268,11 @@ class ProfileBrowserWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') - bui.app.ui_v1.set_main_menu_window( + bui.app.ui_v1.set_main_window( EditProfileWindow( existing_profile=None, in_main_menu=self._in_main_menu - ).get_root_widget(), - from_window=self._root_widget if self._in_main_menu else False, + ), + from_window=self if self._in_main_menu else False, ) def _delete_profile(self) -> None: @@ -315,11 +334,11 @@ class ProfileBrowserWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( + bui.app.ui_v1.set_main_window( EditProfileWindow( self._selected_profile, in_main_menu=self._in_main_menu - ).get_root_widget(), - from_window=self._root_widget if self._in_main_menu else False, + ), + from_window=self if self._in_main_menu else False, ) def _select(self, name: str, index: int) -> None: @@ -328,7 +347,10 @@ class ProfileBrowserWindow(bui.Window): def _back(self) -> None: # pylint: disable=cyclic-import - from bauiv1lib.account.settings import AccountSettingsWindow + + if self._in_main_menu: + self.main_window_back() + return # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: @@ -340,16 +362,17 @@ class ProfileBrowserWindow(bui.Window): bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - if self._in_main_menu: - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AccountSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + # if self._in_main_menu: + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # AccountSettingsWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) - # If we're being called up standalone, handle pause/resume ourself. - else: - bui.app.classic.resume() + # # If we're being called up standalone, handle pause/resume ourself. + # else: + bui.app.classic.resume() def _refresh(self) -> None: # pylint: disable=too-many-locals diff --git a/src/assets/ba_data/python/bauiv1lib/profile/edit.py b/src/assets/ba_data/python/bauiv1lib/profile/edit.py index b4747769..982d143b 100644 --- a/src/assets/ba_data/python/bauiv1lib/profile/edit.py +++ b/src/assets/ba_data/python/bauiv1lib/profile/edit.py @@ -5,17 +5,16 @@ from __future__ import annotations import random -from typing import cast +from typing import cast, override from bauiv1lib.colorpicker import ColorPicker import bauiv1 as bui import bascenev1 as bs -class EditProfileWindow(bui.Window): +class EditProfileWindow(bui.MainWindow): """Window for editing a player profile.""" - # FIXME: WILL NEED TO CHANGE THIS FOR UILOCATION. def reload_window(self) -> None: """Transitions out and recreates ourself.""" @@ -25,18 +24,18 @@ class EditProfileWindow(bui.Window): bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - EditProfileWindow( - self.getname(), self._in_main_menu - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + EditProfileWindow(self.getname(), self._in_main_menu), + from_window=self, + is_back=True, ) def __init__( self, existing_profile: str | None, in_main_menu: bool, - transition: str = 'in_right', + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, ): # FIXME: Tidy this up a bit. # pylint: disable=too-many-branches @@ -63,26 +62,32 @@ class EditProfileWindow(bui.Window): self._width = width = 880.0 if uiscale is bui.UIScale.SMALL else 680.0 self._x_inset = x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 self._height = height = ( - 350.0 + 450.0 if uiscale is bui.UIScale.SMALL else 400.0 if uiscale is bui.UIScale.MEDIUM else 450.0 ) spacing = 40 self._base_scale = ( - 2.05 + 1.6 if uiscale is bui.UIScale.SMALL - else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 + else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 ) - top_extra = 15 if uiscale is bui.UIScale.SMALL else 15 + top_extra = 70 if uiscale is bui.UIScale.SMALL else 15 super().__init__( root_widget=bui.containerwidget( size=(width, height + top_extra), - transition=transition, scale=self._base_scale, stack_offset=( - (0, 15) if uiscale is bui.UIScale.SMALL else (0, 0) + (0, -40) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, ) cancel_button = btn = bui.buttonwidget( parent=self._root_widget, @@ -512,6 +517,19 @@ class EditProfileWindow(bui.Window): ) self._update_character() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + existing_profile=self._existing_profile, + in_main_menu=self._in_main_menu, + ) + ) + def assign_random_name(self) -> None: """Assigning a random name to the player.""" names = bs.get_random_names() @@ -672,22 +690,24 @@ class EditProfileWindow(bui.Window): ) def _cancel(self) -> None: - from bauiv1lib.profile.browser import ProfileBrowserWindow + self.main_window_back() + # from bauiv1lib.profile.browser import ProfileBrowserWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return + # # no-op if our underlying widget is dead or on its way out. + # if not self._root_widget or self._root_widget.transitioning_out: + # return - bui.containerwidget(edit=self._root_widget, transition='out_right') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - ProfileBrowserWindow( - 'in_left', - selected_profile=self._existing_profile, - in_main_menu=self._in_main_menu, - ).get_root_widget(), - from_window=self._root_widget, - ) + # bui.containerwidget(edit=self._root_widget, transition='out_right') + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # ProfileBrowserWindow( + # 'in_left', + # selected_profile=self._existing_profile, + # in_main_menu=self._in_main_menu, + # ), + # from_window=self, + # is_back=True, + # ) def _set_color(self, color: tuple[float, float, float]) -> None: self._color = color @@ -783,7 +803,6 @@ class EditProfileWindow(bui.Window): def save(self, transition_out: bool = True) -> bool: """Save has been selected.""" - from bauiv1lib.profile.browser import ProfileBrowserWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: @@ -840,14 +859,17 @@ class EditProfileWindow(bui.Window): if transition_out: plus.run_v1_account_transactions() - bui.containerwidget(edit=self._root_widget, transition='out_right') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - ProfileBrowserWindow( - 'in_left', - selected_profile=new_name, - in_main_menu=self._in_main_menu, - ).get_root_widget(), - from_window=self._root_widget, - ) + self.main_window_back() + # bui.containerwidget(edit=self._root_widget, + # transition='out_right') + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # ProfileBrowserWindow( + # 'in_left', + # selected_profile=new_name, + # in_main_menu=self._in_main_menu, + # ), + # from_window=self, + # is_back=True, + # ) return True diff --git a/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py b/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py index 89340bb4..eadc9903 100644 --- a/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py +++ b/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py @@ -32,12 +32,12 @@ class ProfileUpgradeWindow(bui.Window): self._r = 'editProfileWindow' - self._width = 680 + uiscale = bui.app.ui_v1.uiscale + self._width = 750 if uiscale is bui.UIScale.SMALL else 680 self._height = 350 assert bui.app.classic is not None - uiscale = bui.app.ui_v1.uiscale self._base_scale = ( - 2.05 + 1.9 if uiscale is bui.UIScale.SMALL else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.2 ) @@ -49,17 +49,17 @@ class ProfileUpgradeWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), - toolbar_visibility='menu_currency', + toolbar_visibility='menu_store_no_back', transition=transition, scale=self._base_scale, stack_offset=( - (0, 15) if uiscale is bui.UIScale.SMALL else (0, 0) + (0, -30) if uiscale is bui.UIScale.SMALL else (0, 0) ), ) ) cancel_button = bui.buttonwidget( parent=self._root_widget, - position=(52, 30), + position=(52, 60), size=(155, 60), scale=0.8, autoselect=True, @@ -68,7 +68,7 @@ class ProfileUpgradeWindow(bui.Window): ) self._upgrade_button = bui.buttonwidget( parent=self._root_widget, - position=(self._width - 190, 30), + position=(self._width - 190, 60), size=(155, 60), scale=0.8, autoselect=True, @@ -136,20 +136,20 @@ class ProfileUpgradeWindow(bui.Window): ) self._tickets_text: bui.Widget | None - if not bui.app.ui_v1.use_toolbars: - self._tickets_text = bui.textwidget( - parent=self._root_widget, - position=(self._width * 0.9 - 5, self._height - 30), - size=(0, 0), - text=bui.charstr(bui.SpecialChar.TICKET) + '123', - color=(0.2, 1, 0.2), - maxwidth=100, - scale=0.5, - h_align='right', - v_align='center', - ) - else: - self._tickets_text = None + # if not bui.app.ui_v1.use_toolbars: + # self._tickets_text = bui.textwidget( + # parent=self._root_widget, + # position=(self._width * 0.9 - 5, self._height - 30), + # size=(0, 0), + # text=bui.charstr(bui.SpecialChar.TICKET) + '123', + # color=(0.2, 1, 0.2), + # maxwidth=100, + # scale=0.5, + # h_align='right', + # v_align='center', + # ) + # else: + self._tickets_text = None bui.app.classic.master_server_v1_get( 'bsGlobalProfileCheck', @@ -210,7 +210,7 @@ class ProfileUpgradeWindow(bui.Window): ) def _on_upgrade_press(self) -> None: - from bauiv1lib import gettickets + # from bauiv1lib import gettickets if self._status is None: plus = bui.app.plus @@ -220,7 +220,8 @@ class ProfileUpgradeWindow(bui.Window): tickets = plus.get_v1_account_ticket_count() if tickets < self._cost: bui.getsound('error').play() - gettickets.show_get_tickets_prompt() + print('FIXME - show not-enough-tickets msg.') + # gettickets.show_get_tickets_prompt() return bui.screenmessage( bui.Lstr(resource='purchasingText'), color=(0, 1, 0) diff --git a/src/assets/ba_data/python/bauiv1lib/purchase.py b/src/assets/ba_data/python/bauiv1lib/purchase.py index 60f97b0e..fbb74d75 100644 --- a/src/assets/ba_data/python/bauiv1lib/purchase.py +++ b/src/assets/ba_data/python/bauiv1lib/purchase.py @@ -44,7 +44,7 @@ class PurchaseWindow(bui.Window): root_widget=bui.containerwidget( size=(self._width, self._height), transition=transition, - toolbar_visibility='menu_currency', + toolbar_visibility='menu_store', scale=( 1.2 if uiscale is bui.UIScale.SMALL @@ -162,7 +162,7 @@ class PurchaseWindow(bui.Window): bui.containerwidget(edit=self._root_widget, transition='out_left') def _purchase(self) -> None: - from bauiv1lib import gettickets + # from bauiv1lib import gettickets plus = bui.app.plus assert plus is not None @@ -176,7 +176,8 @@ class PurchaseWindow(bui.Window): except Exception: ticket_count = None if ticket_count is not None and ticket_count < self._price: - gettickets.show_get_tickets_prompt() + # gettickets.show_get_tickets_prompt() + print('FIXME - show not-enough-tickets msg') bui.getsound('error').play() return diff --git a/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py b/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py index a3e39ebe..9ca597c1 100644 --- a/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py +++ b/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py @@ -4,22 +4,29 @@ from __future__ import annotations -from typing import override +from typing import override, TYPE_CHECKING, assert_never from bauiv1lib.popup import PopupWindow import bauiv1 as bui +if TYPE_CHECKING: + from typing import Literal + class ResourceTypeInfoWindow(PopupWindow): """Popup window providing info about resource types.""" - def __init__(self, origin_widget: bui.Widget): + def __init__( + self, + resource_type: Literal['tickets', 'tokens', 'trophies', 'xp'], + origin_widget: bui.Widget, + ): assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale scale = ( - 2.3 + 2.0 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.0 ) self._transitioning_out = False self._width = 570 @@ -31,12 +38,13 @@ class ResourceTypeInfoWindow(PopupWindow): scale=scale, bg_color=bg_color, position=origin_widget.get_screen_space_center(), + edge_buffer_scale=4.0, ) self._cancel_button = bui.buttonwidget( parent=self.root_widget, - position=(50, self._height - 30), + position=(40, self._height - 40), size=(50, 50), - scale=0.5, + scale=0.7, label='', color=bg_color, on_activate_call=self._on_cancel_press, @@ -45,6 +53,26 @@ class ResourceTypeInfoWindow(PopupWindow): iconscale=1.2, ) + if resource_type == 'tickets': + rdesc = 'Will describe tickets.' + elif resource_type == 'tokens': + rdesc = 'Will describe tokens.' + elif resource_type == 'trophies': + rdesc = 'Will show trophies & league rankings.' + elif resource_type == 'xp': + rdesc = 'Will describe xp/levels.' + else: + assert_never(resource_type) + + bui.textwidget( + parent=self.root_widget, + h_align='center', + v_align='center', + size=(0, 0), + position=(self._width * 0.5, self._height * 0.5), + text=(f'UNDER CONSTRUCTION.\n({rdesc})'), + ) + def _on_cancel_press(self) -> None: self._transition_out() diff --git a/src/assets/ba_data/python/bauiv1lib/sendinfo.py b/src/assets/ba_data/python/bauiv1lib/sendinfo.py index 0a1cf59e..97899259 100644 --- a/src/assets/ba_data/python/bauiv1lib/sendinfo.py +++ b/src/assets/ba_data/python/bauiv1lib/sendinfo.py @@ -14,26 +14,25 @@ if TYPE_CHECKING: from typing import Any -class SendInfoWindow(bui.Window): +class SendInfoWindow(bui.MainWindow): """Window for sending info to the developer.""" def __init__( self, modal: bool = False, legacy_code_mode: bool = False, + transition: str | None = 'in_scale', origin_widget: bui.Widget | None = None, ): self._legacy_code_mode = legacy_code_mode - scale_origin: tuple[float, float] | None + # scale_origin: tuple[float, float] | None + + # Need to wrangle our own transition-out in modal mode. if origin_widget is not None: self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' else: self._transition_out = 'out_right' - scale_origin = None - transition = 'in_right' width = 450 if legacy_code_mode else 600 height = 200 if legacy_code_mode else 300 @@ -46,15 +45,19 @@ class SendInfoWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(width, height), - transition=transition, - toolbar_visibility='menu_minimal_no_back', - scale_origin_stack_offset=scale_origin, + toolbar_visibility=( + 'menu_minimal_no_back' + if uiscale is bui.UIScale.SMALL or modal + else 'menu_full' + ), scale=( 2.0 if uiscale is bui.UIScale.SMALL else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) btn = bui.buttonwidget( @@ -165,7 +168,12 @@ class SendInfoWindow(bui.Window): def _do_back(self) -> None: # pylint: disable=cyclic-import - from bauiv1lib.settings.advanced import AdvancedSettingsWindow + + if not self._modal: + self.main_window_back() + return + + # from bauiv1lib.settings.advanced import AdvancedSettingsWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: @@ -174,12 +182,13 @@ class SendInfoWindow(bui.Window): bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - if not self._modal: - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + # if not self._modal: + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # AdvancedSettingsWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) def _activate_enter_button(self) -> None: self._enter_button.activate() @@ -200,9 +209,8 @@ class SendInfoWindow(bui.Window): ) if not self._modal: assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + AdvancedSettingsWindow(transition='in_left'), from_window=self ) description: Any = bui.textwidget(query=self._text_field) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py index 521762ac..b43b95b5 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py @@ -6,7 +6,7 @@ from __future__ import annotations import os import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override from bauiv1lib.popup import PopupMenu import bauiv1 as bui @@ -15,39 +15,28 @@ if TYPE_CHECKING: from typing import Any -class AdvancedSettingsWindow(bui.Window): +class AdvancedSettingsWindow(bui.MainWindow): """Window for editing advanced app settings.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements - import threading if bui.app.classic is None: raise RuntimeError('This requires classic support.') # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. - threading.Thread(target=self._preload_modules).start() + bui.app.threadpool_submit_no_wait(self._preload_modules) app = bui.app assert app.classic is not None - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - uiscale = bui.app.ui_v1.uiscale - self._width = 970.0 if uiscale is bui.UIScale.SMALL else 670.0 + self._width = 1030.0 if uiscale is bui.UIScale.SMALL else 670.0 x_inset = 150 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 390.0 @@ -63,18 +52,22 @@ class AdvancedSettingsWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), scale=( - 2.06 + 2.04 if uiscale is bui.UIScale.SMALL else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( - (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0) + (0, 10) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) self._prev_lang = '' @@ -112,9 +105,9 @@ class AdvancedSettingsWindow(bui.Window): self._r = 'settingsWindowAdvanced' - if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._do_back + edit=self._root_widget, on_cancel_call=self.main_window_back ) self._back_button = None else: @@ -126,7 +119,7 @@ class AdvancedSettingsWindow(bui.Window): autoselect=True, label=bui.Lstr(resource='backText'), button_type='back', - on_activate_call=self._do_back, + on_activate_call=self.main_window_back, ) bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button @@ -134,12 +127,16 @@ class AdvancedSettingsWindow(bui.Window): self._title_text = bui.textwidget( parent=self._root_widget, - position=(0, self._height - 52), - size=(self._width, 25), + position=( + self._width * 0.5, + self._height - (57 if uiscale is bui.UIScale.SMALL else 40), + ), + size=(0, 0), + scale=0.5 if uiscale is bui.UIScale.SMALL else 1.0, text=bui.Lstr(resource=f'{self._r}.titleText'), color=app.ui_v1.title_color, h_align='center', - v_align='top', + v_align='center', ) if self._back_button is not None: @@ -180,10 +177,23 @@ class AdvancedSettingsWindow(bui.Window): callback=bui.WeakCall(self._completed_langs_cb), ) - # noinspection PyUnresolvedReferences + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + @staticmethod def _preload_modules() -> None: - """Preload modules we use; avoids hitches (called in bg thread).""" + """Preload stuff in bg thread to avoid hitches in logic thread""" from babase import modutils as _unused2 from bauiv1lib import config as _unused1 from bauiv1lib.settings import vrtesting as _unused3 @@ -191,7 +201,7 @@ class AdvancedSettingsWindow(bui.Window): from bauiv1lib import appinvite as _unused5 from bauiv1lib import account as _unused6 from bauiv1lib import sendinfo as _unused7 - from bauiv1lib import debug as _unused8 + from bauiv1lib import benchmarks as _unused8 from bauiv1lib.settings import plugins as _unused9 from bauiv1lib.settings import devtools as _unused10 @@ -684,14 +694,13 @@ class AdvancedSettingsWindow(bui.Window): for child in self._subcontainer.get_children(): bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20) - if bui.app.ui_v1.use_toolbars: - pbtn = bui.get_special_widget('party_button') - bui.widget(edit=self._scrollwidget, right_widget=pbtn) - if self._back_button is None: - bui.widget( - edit=self._scrollwidget, - left_widget=bui.get_special_widget('back_button'), - ) + pbtn = bui.get_special_widget('squad_button') + bui.widget(edit=self._scrollwidget, right_widget=pbtn) + if self._back_button is None: + bui.widget( + edit=self._scrollwidget, + left_widget=bui.get_special_widget('back_button'), + ) self._restore_state() @@ -719,9 +728,8 @@ class AdvancedSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - VRTestingWindow(transition='in_right').get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + VRTestingWindow(transition='in_right'), from_window=self ) def _on_net_test_press(self) -> None: @@ -736,9 +744,8 @@ class AdvancedSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - NetTestingWindow(transition='in_right').get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + NetTestingWindow(transition='in_right'), from_window=self ) def _on_friend_promo_code_press(self) -> None: @@ -763,9 +770,8 @@ class AdvancedSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - PluginWindow(origin_widget=self._plugins_button).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + PluginWindow(origin_widget=self._plugins_button), from_window=self ) def _on_dev_tools_button_press(self) -> None: @@ -779,11 +785,9 @@ class AdvancedSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - DevToolsWindow( - origin_widget=self._dev_tools_button - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + DevToolsWindow(origin_widget=self._dev_tools_button), + from_window=self, ) def _on_send_info_press(self) -> None: @@ -799,15 +803,13 @@ class AdvancedSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - SendInfoWindow( - origin_widget=self._send_info_button - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + SendInfoWindow(origin_widget=self._send_info_button), + from_window=self, ) def _on_benchmark_press(self) -> None: - from bauiv1lib.debug import DebugWindow + from bauiv1lib.benchmarks import BenchmarksAndStressTestsWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: @@ -816,9 +818,9 @@ class AdvancedSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - DebugWindow(transition='in_right').get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + BenchmarksAndStressTestsWindow(transition='in_right'), + from_window=self, ) def _save_state(self) -> None: @@ -974,19 +976,20 @@ class AdvancedSettingsWindow(bui.Window): self._complete_langs_error = True bui.apptimer(0.001, bui.WeakCall(self._update_lang_status)) - def _do_back(self) -> None: - from bauiv1lib.settings.allsettings import AllSettingsWindow + # def _do_back(self) -> None: + # from bauiv1lib.settings.allsettings import AllSettingsWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return + # # no-op if our underlying widget is dead or on its way out. + # if not self._root_widget or self._root_widget.transitioning_out: + # return - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AllSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + # self._save_state() + # bui.containerwidget( + # edit=self._root_widget, transition=self._transition_out + # ) + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # AllSettingsWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py index 746000a9..11df974d 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py @@ -4,8 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING -from threading import Thread +from typing import TYPE_CHECKING, override import logging import bauiv1 as bui @@ -14,12 +13,12 @@ if TYPE_CHECKING: pass -class AllSettingsWindow(bui.Window): +class AllSettingsWindow(bui.MainWindow): """Window for selecting a settings category.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements @@ -27,17 +26,9 @@ class AllSettingsWindow(bui.Window): # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. - Thread(target=self._preload_modules).start() + bui.app.threadpool_submit_no_wait(self._preload_modules) bui.set_analytics_screen('Settings Window') - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale width = 1000 if uiscale is bui.UIScale.SMALL else 580 @@ -50,24 +41,28 @@ class AllSettingsWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(width, height + top_extra), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, - scale=( - 1.75 + toolbar_visibility=( + 'menu_minimal' if uiscale is bui.UIScale.SMALL - else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 + else 'menu_full' + ), + scale=( + 1.5 + if uiscale is bui.UIScale.SMALL + else 1.25 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( - (0, -8) if uiscale is bui.UIScale.SMALL else (0, 0) + (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) - if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: self._back_button = None bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._do_back + edit=self._root_widget, on_cancel_call=self.main_window_back ) else: self._back_button = btn = bui.buttonwidget( @@ -79,7 +74,7 @@ class AllSettingsWindow(bui.Window): text_scale=1.2, label=bui.Lstr(resource='backText'), button_type='back', - on_activate_call=self._do_back, + on_activate_call=self.main_window_back, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) @@ -139,7 +134,7 @@ class AllSettingsWindow(bui.Window): label='', on_activate_call=self._do_controllers, ) - if bui.app.ui_v1.use_toolbars and self._back_button is None: + if self._back_button is None: bbtn = bui.get_special_widget('back_button') bui.widget(edit=ctb, left_widget=bbtn) _b_title( @@ -163,9 +158,8 @@ class AllSettingsWindow(bui.Window): label='', on_activate_call=self._do_graphics, ) - if bui.app.ui_v1.use_toolbars: - pbtn = bui.get_special_widget('party_button') - bui.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn) + 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( @@ -219,7 +213,20 @@ class AllSettingsWindow(bui.Window): ) self._restore_state() - # noinspection PyUnresolvedReferences + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + @staticmethod def _preload_modules() -> None: """Preload modules we use; avoids hitches (called in bg thread).""" @@ -229,24 +236,6 @@ class AllSettingsWindow(bui.Window): import bauiv1lib.settings.audio as _unused4 import bauiv1lib.settings.advanced as _unused5 - def _do_back(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.mainmenu import MainMenuWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) - def _do_controllers(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.controls import ControlsSettingsWindow @@ -258,11 +247,9 @@ class AllSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - ControlsSettingsWindow( - origin_widget=self._controllers_button - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + ControlsSettingsWindow(origin_widget=self._controllers_button), + from_window=self, ) def _do_graphics(self) -> None: @@ -276,11 +263,9 @@ class AllSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - GraphicsSettingsWindow( - origin_widget=self._graphics_button - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + GraphicsSettingsWindow(origin_widget=self._graphics_button), + from_window=self, ) def _do_audio(self) -> None: @@ -294,11 +279,9 @@ class AllSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AudioSettingsWindow( - origin_widget=self._audio_button - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + AudioSettingsWindow(origin_widget=self._audio_button), + from_window=self, ) def _do_advanced(self) -> None: @@ -312,11 +295,9 @@ class AllSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow( - origin_widget=self._advanced_button - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + AdvancedSettingsWindow(origin_widget=self._advanced_button), + from_window=self, ) def _save_state(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/settings/audio.py b/src/assets/ba_data/python/bauiv1lib/settings/audio.py index d04cdf68..71d242a9 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/audio.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/audio.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import logging import bauiv1 as bui @@ -13,12 +13,12 @@ if TYPE_CHECKING: pass -class AudioSettingsWindow(bui.Window): +class AudioSettingsWindow(bui.MainWindow): """Window for editing audio settings.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements @@ -30,16 +30,6 @@ class AudioSettingsWindow(bui.Window): assert bui.app.classic is not None music = bui.app.classic.music - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - self._r = 'audioSettingsWindow' spacing = 50.0 @@ -70,13 +60,18 @@ class AudioSettingsWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(width, height), - transition=transition, scale=base_scale, - scale_origin_stack_offset=scale_origin, stack_offset=( (0, -20) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, ) self._back_button = back_button = btn = bui.buttonwidget( @@ -87,7 +82,7 @@ class AudioSettingsWindow(bui.Window): text_scale=1.2, label=bui.Lstr(resource='backText'), button_type='back', - on_activate_call=self._back, + on_activate_call=self.main_window_back, autoselect=True, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) @@ -122,11 +117,10 @@ class AudioSettingsWindow(bui.Window): increment=0.05, as_percent=True, ) - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=svne.plusbutton, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=svne.plusbutton, + right_widget=bui.get_special_widget('squad_button'), + ) v -= spacing self._music_volume_numedit = ConfigNumberEdit( parent=self._root_widget, @@ -226,6 +220,20 @@ class AudioSettingsWindow(bui.Window): self._restore_state() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + def _set_vr_head_relative_audio(self, val: str) -> None: cfg = bui.app.config cfg['VR Head Relative Audio'] = val @@ -255,31 +263,9 @@ class AudioSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - stb.SoundtrackBrowserWindow( - origin_widget=self._soundtrack_button - ).get_root_widget(), - from_window=self._root_widget, - ) - - def _back(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.settings import allsettings - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - allsettings.AllSettingsWindow( - transition='in_left' - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + stb.SoundtrackBrowserWindow(origin_widget=self._soundtrack_button), + from_window=self, ) def _save_state(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/settings/controls.py b/src/assets/ba_data/python/bauiv1lib/settings/controls.py index 6a85dfdf..85620b48 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/controls.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/controls.py @@ -4,17 +4,19 @@ from __future__ import annotations +from typing import override + from bauiv1lib.popup import PopupMenu import bascenev1 as bs import bauiv1 as bui -class ControlsSettingsWindow(bui.Window): +class ControlsSettingsWindow(bui.MainWindow): """Top level control settings window.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # FIXME: should tidy up here. @@ -25,30 +27,22 @@ class ControlsSettingsWindow(bui.Window): self._have_selected_child = False - scale_origin: tuple[float, float] | None - - # If they provided an origin-widget, scale up from that. - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - self._r = 'configControllersWindow' + uiscale = bui.app.ui_v1.uiscale app = bui.app assert app.classic is not None spacing = 50.0 button_width = 350.0 - width = 460.0 - height = 130.0 + width = 800.0 if uiscale is bui.UIScale.SMALL else 460.0 + height = 300 if uiscale is bui.UIScale.SMALL else 130.0 + yoffs = -60 if uiscale is bui.UIScale.SMALL else 0 space_height = spacing * 0.3 - # FIXME: should create vis settings under platform or app-adapter - # to determine whether to show this stuff; not hard code it. + # FIXME: should create vis settings under platform or + # app-adapter to determine whether to show this stuff; not hard + # code it. show_gamepads = False platform = app.classic.platform @@ -111,13 +105,10 @@ class ControlsSettingsWindow(bui.Window): height += spacing assert bui.app.classic is not None - uiscale = bui.app.ui_v1.uiscale smallscale = 1.7 if show_keyboard else 2.2 super().__init__( root_widget=bui.containerwidget( size=(width, height), - transition=transition, - scale_origin_stack_offset=scale_origin, stack_offset=( (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0) ), @@ -126,20 +117,41 @@ class ControlsSettingsWindow(bui.Window): if uiscale is bui.UIScale.SMALL else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ), + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, + ) + + self._back_button: bui.Widget | None + if uiscale is bui.UIScale.SMALL: + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + self._back_button = None + else: + self._back_button = btn = bui.buttonwidget( + parent=self._root_widget, + position=(35, height - 60), + size=(140, 65), + scale=0.8, + text_scale=1.2, + autoselect=True, + label=bui.Lstr(resource='backText'), + button_type='back', + on_activate_call=self.main_window_back, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.buttonwidget( + edit=btn, + button_type='backSmall', + size=(60, 60), + label=bui.charstr(bui.SpecialChar.BACK), ) - ) - self._back_button = btn = bui.buttonwidget( - parent=self._root_widget, - position=(35, height - 60), - size=(140, 65), - scale=0.8, - text_scale=1.2, - autoselect=True, - label=bui.Lstr(resource='backText'), - button_type='back', - on_activate_call=self._back, - ) - bui.containerwidget(edit=self._root_widget, cancel_button=btn) # We need these vars to exist even if the buttons don't. self._gamepads_button: bui.Widget | None = None @@ -150,21 +162,15 @@ class ControlsSettingsWindow(bui.Window): bui.textwidget( parent=self._root_widget, - position=(0, height - 49), + position=(0, height - 49 + yoffs), size=(width, 25), text=bui.Lstr(resource=f'{self._r}.titleText'), color=bui.app.ui_v1.title_color, h_align='center', v_align='top', ) - bui.buttonwidget( - edit=btn, - button_type='backSmall', - size=(60, 60), - label=bui.charstr(bui.SpecialChar.BACK), - ) - v = height - 75 + v = height - 75 + yoffs v -= spacing if show_touch: @@ -176,18 +182,18 @@ class ControlsSettingsWindow(bui.Window): label=bui.Lstr(resource=f'{self._r}.configureTouchText'), on_activate_call=self._do_touchscreen, ) - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, + right_widget=bui.get_special_widget('squad_button'), + ) if not self._have_selected_child: bui.containerwidget( edit=self._root_widget, selected_child=self._touch_button ) - bui.widget( - edit=self._back_button, down_widget=self._touch_button - ) + if self._back_button is not None: + bui.widget( + edit=self._back_button, down_widget=self._touch_button + ) self._have_selected_child = True v -= spacing @@ -200,18 +206,19 @@ class ControlsSettingsWindow(bui.Window): label=bui.Lstr(resource=f'{self._r}.configureControllersText'), on_activate_call=self._do_gamepads, ) - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, + right_widget=bui.get_special_widget('squad_button'), + ) if not self._have_selected_child: bui.containerwidget( edit=self._root_widget, selected_child=self._gamepads_button ) - bui.widget( - edit=self._back_button, down_widget=self._gamepads_button - ) + if self._back_button is not None: + bui.widget( + edit=self._back_button, + down_widget=self._gamepads_button, + ) self._have_selected_child = True v -= spacing else: @@ -232,18 +239,19 @@ class ControlsSettingsWindow(bui.Window): bui.widget( edit=self._keyboard_button, left_widget=self._keyboard_button ) - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, + right_widget=bui.get_special_widget('squad_button'), + ) if not self._have_selected_child: bui.containerwidget( edit=self._root_widget, selected_child=self._keyboard_button ) - bui.widget( - edit=self._back_button, down_widget=self._keyboard_button - ) + if self._back_button is not None: + bui.widget( + edit=self._back_button, + down_widget=self._keyboard_button, + ) self._have_selected_child = True v -= spacing if show_keyboard_p2: @@ -274,18 +282,19 @@ class ControlsSettingsWindow(bui.Window): bui.widget( edit=self._idevices_button, left_widget=self._idevices_button ) - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=btn, - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=btn, + right_widget=bui.get_special_widget('squad_button'), + ) if not self._have_selected_child: bui.containerwidget( edit=self._root_widget, selected_child=self._idevices_button ) - bui.widget( - edit=self._back_button, down_widget=self._idevices_button - ) + if self._back_button is not None: + bui.widget( + edit=self._back_button, + down_widget=self._idevices_button, + ) self._have_selected_child = True v -= spacing @@ -371,6 +380,20 @@ class ControlsSettingsWindow(bui.Window): self._restore_state() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + def _set_mac_controller_subsystem(self, val: str) -> None: cfg = bui.app.config cfg['Mac Controller Subsystem'] = val @@ -387,11 +410,9 @@ class ControlsSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - ConfigKeyboardWindow( - bs.getinputdevice('Keyboard', '#1') - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + ConfigKeyboardWindow(bs.getinputdevice('Keyboard', '#1')), + from_window=self, ) def _config_keyboard2(self) -> None: @@ -405,11 +426,9 @@ class ControlsSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - ConfigKeyboardWindow( - bs.getinputdevice('Keyboard', '#2') - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + ConfigKeyboardWindow(bs.getinputdevice('Keyboard', '#2')), + from_window=self, ) def _do_mobile_devices(self) -> None: @@ -423,9 +442,8 @@ class ControlsSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - RemoteAppSettingsWindow().get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + RemoteAppSettingsWindow(), from_window=self ) def _do_gamepads(self) -> None: @@ -439,10 +457,7 @@ class ControlsSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - GamepadSelectWindow().get_root_widget(), - from_window=self._root_widget, - ) + bui.app.ui_v1.set_main_window(GamepadSelectWindow(), from_window=self) def _do_touchscreen(self) -> None: # pylint: disable=cyclic-import @@ -455,9 +470,8 @@ class ControlsSettingsWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - TouchscreenSettingsWindow().get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + TouchscreenSettingsWindow(), from_window=self ) def _save_state(self) -> None: @@ -500,20 +514,21 @@ class ControlsSettingsWindow(bui.Window): ) bui.containerwidget(edit=self._root_widget, selected_child=sel) - def _back(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.settings.allsettings import AllSettingsWindow + # def _back(self) -> None: + # # pylint: disable=cyclic-import + # from bauiv1lib.settings.allsettings import AllSettingsWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return + # # no-op if our underlying widget is dead or on its way out. + # if not self._root_widget or self._root_widget.transitioning_out: + # return - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AllSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + # self._save_state() + # bui.containerwidget( + # edit=self._root_widget, transition=self._transition_out + # ) + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # AllSettingsWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/devtools.py b/src/assets/ba_data/python/bauiv1lib/settings/devtools.py index 2db6e9c2..4c97422a 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/devtools.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/devtools.py @@ -4,6 +4,8 @@ from __future__ import annotations +from typing import override + import babase import bauiv1 as bui from bauiv1lib.popup import PopupMenu @@ -11,33 +13,23 @@ from bauiv1lib.confirm import ConfirmWindow from bauiv1lib.config import ConfigCheckBox -class DevToolsWindow(bui.Window): +class DevToolsWindow(bui.MainWindow): """Window for accessing modding tools.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): app = bui.app assert app.classic is not None - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - uiscale = app.ui_v1.uiscale - self._width = 970.0 if uiscale is bui.UIScale.SMALL else 670.0 + self._width = 1000.0 if uiscale is bui.UIScale.SMALL else 670.0 x_inset = 150 if uiscale is bui.UIScale.SMALL else 0 self._height = ( - 390.0 + 370.0 if uiscale is bui.UIScale.SMALL else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0 ) @@ -53,25 +45,29 @@ class DevToolsWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), scale=( - 2.06 + 2.13 if uiscale is bui.UIScale.SMALL else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( - (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0) + (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) self._r = 'settingsDevTools' - if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._do_back + edit=self._root_widget, on_cancel_call=self.main_window_back ) self._back_button = None else: @@ -83,7 +79,7 @@ class DevToolsWindow(bui.Window): autoselect=True, label=bui.Lstr(resource='backText'), button_type='back', - on_activate_call=self._do_back, + on_activate_call=self.main_window_back, ) bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button @@ -91,8 +87,12 @@ class DevToolsWindow(bui.Window): self._title_text = bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height - 48), + position=( + self._width * 0.5, + self._height - (64 if uiscale is bui.UIScale.SMALL else 48), + ), size=(0, 25), + scale=(0.6 if uiscale is bui.UIScale.SMALL else 1.0), maxwidth=self._width - 200, text=bui.Lstr(resource='settingsWindowAdvanced.devToolsText'), color=app.ui_v1.title_color, @@ -201,6 +201,16 @@ class DevToolsWindow(bui.Window): on_value_change_call=self._set_uiscale, ) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + def _set_uiscale(self, val: str) -> None: cfg = bui.app.config cfg['UI Scale'] = val @@ -210,19 +220,3 @@ class DevToolsWindow(bui.Window): bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'), color=(1.0, 0.5, 0.0), ) - - def _do_back(self) -> None: - from bauiv1lib.settings.advanced import AdvancedSettingsWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py index 390af52a..1a0cba67 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from bauiv1lib.popup import PopupWindow -class GamepadSettingsWindow(bui.Window): +class GamepadSettingsWindow(bui.MainWindow): """Window for configuring a gamepad.""" # pylint: disable=too-many-public-methods @@ -28,6 +28,7 @@ class GamepadSettingsWindow(bui.Window): is_main_menu: bool = True, transition: str = 'in_right', transition_out: str = 'out_right', + origin_widget: bui.Widget | None = None, settings: dict | None = None, ): self._input = gamepad @@ -63,7 +64,9 @@ class GamepadSettingsWindow(bui.Window): (-20, -16) if uiscale is bui.UIScale.SMALL else (0, 0) ), transition=transition, - ) + ), + transition=transition, + origin_widget=origin_widget, ) self._settings: dict[str, int] = {} @@ -792,7 +795,7 @@ class GamepadSettingsWindow(bui.Window): return btn def _cancel(self) -> None: - from bauiv1lib.settings.controls import ControlsSettingsWindow + # from bauiv1lib.settings.controls import ControlsSettingsWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: @@ -802,11 +805,13 @@ class GamepadSettingsWindow(bui.Window): edit=self._root_widget, transition=self._transition_out ) if self._is_main_menu: - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - ControlsSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + self.main_window_back() + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # ControlsSettingsWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) def _reset(self) -> None: from bauiv1lib.confirm import ConfirmWindow @@ -937,9 +942,10 @@ class GamepadSettingsWindow(bui.Window): from bauiv1lib.settings.controls import ControlsSettingsWindow assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - ControlsSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + ControlsSettingsWindow(transition='in_left'), + from_window=self, + is_back=True, ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py index ab0e7e1c..9040c81d 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import bascenev1 as bs import bauiv1 as bui @@ -16,15 +16,20 @@ if TYPE_CHECKING: def gamepad_configure_callback(event: dict[str, Any]) -> None: """Respond to a gamepad button press during config selection.""" - from bauiv1lib.settings import gamepad + from bauiv1lib.settings.gamepad import GamepadSettingsWindow # Ignore all but button-presses. if event['type'] not in ['BUTTONDOWN', 'HATMOTION']: return bs.release_gamepad_input() + + if bool(True): + bui.screenmessage('UNDER CONSTRUCTION') + return + assert bui.app.classic is not None try: - bui.app.ui_v1.clear_main_menu_window(transition='out_left') + bui.app.ui_v1.clear_main_window() except Exception: logging.exception('Error transitioning out main_menu_window.') bui.getsound('activateBeep').play() @@ -32,25 +37,34 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None: device = event['input_device'] assert isinstance(device, bs.InputDevice) if device.allows_configuring: - bui.app.ui_v1.set_main_menu_window( - gamepad.GamepadSettingsWindow(device).get_root_widget(), - from_window=None, + bui.app.ui_v1.set_main_window( + GamepadSettingsWindow(device), from_window=None ) else: width = 700 height = 200 button_width = 80 uiscale = bui.app.ui_v1.uiscale - dlg = bui.containerwidget( - scale=( - 1.7 - if uiscale is bui.UIScale.SMALL - else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 - ), - size=(width, height), - transition='in_right', - ) - bui.app.ui_v1.set_main_menu_window(dlg, from_window=None) + + class _Window(bui.MainWindow): + def __init__(self) -> None: + super().__init__( + root_widget=bui.containerwidget( + scale=( + 1.7 + if uiscale is bui.UIScale.SMALL + else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 + ), + size=(width, height), + ), + transition='in_right', + origin_widget=None, + ) + + win = _Window() + dlg = win.get_root_widget() + + bui.app.ui_v1.set_main_window(win, from_window=None) if device.allows_configuring_in_system_settings: msg = bui.Lstr( @@ -86,11 +100,10 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None: bui.containerwidget(edit=dlg, transition='out_right') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - controls.ControlsSettingsWindow( - transition='in_left' - ).get_root_widget(), - from_window=dlg, + bui.app.ui_v1.set_main_window( + controls.ControlsSettingsWindow(transition='in_left'), + from_window=win, + is_back=True, ) bui.buttonwidget( @@ -102,10 +115,14 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None: ) -class GamepadSelectWindow(bui.Window): +class GamepadSelectWindow(bui.MainWindow): """Window for selecting a gamepad to configure.""" - def __init__(self) -> None: + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ) -> None: from typing import cast width = 480 @@ -123,8 +140,9 @@ class GamepadSelectWindow(bui.Window): else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ), size=(width, height), - transition='in_right', - ) + ), + transition=transition, + origin_widget=origin_widget, ) btn = bui.buttonwidget( @@ -134,8 +152,9 @@ class GamepadSelectWindow(bui.Window): label=bui.Lstr(resource='backText'), button_type='back', scale=0.8, - on_activate_call=self._back, + on_activate_call=self.main_window_back, ) + # Let's not have anything selected by default; its misleading looking # for the controller getting configured. bui.containerwidget( @@ -190,19 +209,16 @@ class GamepadSelectWindow(bui.Window): bs.capture_gamepad_input(gamepad_configure_callback) - def _back(self) -> None: - from bauiv1lib.settings import controls - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - bs.release_gamepad_input() - bui.containerwidget(edit=self._root_widget, transition='out_right') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - controls.ControlsSettingsWindow( - transition='in_left' - ).get_root_widget(), - from_window=self._root_widget, + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) ) + + @override + def on_main_window_close(self) -> None: + bs.release_gamepad_input() diff --git a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py index 650d3cd4..3088476d 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, cast, override from bauiv1lib.popup import PopupMenu from bauiv1lib.config import ConfigCheckBox @@ -14,28 +14,18 @@ if TYPE_CHECKING: from typing import Any -class GraphicsSettingsWindow(bui.Window): +class GraphicsSettingsWindow(bui.MainWindow): """Window for graphics settings.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements - # if they provided an origin-widget, scale up from that - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - self._r = 'graphicsSettingsWindow' app = bui.app assert app.classic is not None @@ -73,9 +63,9 @@ class GraphicsSettingsWindow(bui.Window): assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale base_scale = ( - 2.0 + 1.5 if uiscale is bui.UIScale.SMALL - else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 + else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 ) popup_menu_scale = base_scale * 1.2 v = height - 50 @@ -83,13 +73,18 @@ class GraphicsSettingsWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(width, height), - transition=transition, - scale_origin_stack_offset=scale_origin, scale=base_scale, stack_offset=( - (0, -30) if uiscale is bui.UIScale.SMALL else (0, 0) + (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, ) back_button = bui.buttonwidget( @@ -102,7 +97,7 @@ class GraphicsSettingsWindow(bui.Window): autoselect=True, label=bui.charstr(bui.SpecialChar.BACK), button_type='backSmall', - on_activate_call=self._back, + on_activate_call=self.main_window_back, ) bui.containerwidget(edit=self._root_widget, cancel_button=back_button) @@ -215,11 +210,10 @@ class GraphicsSettingsWindow(bui.Window): current_choice=bui.app.config.resolve('Texture Quality'), on_value_change_call=self._set_textures, ) - if bui.app.ui_v1.use_toolbars: - bui.widget( - edit=textures_popup.get_button(), - right_widget=bui.get_special_widget('party_button'), - ) + bui.widget( + edit=textures_popup.get_button(), + right_widget=bui.get_special_widget('squad_button'), + ) v -= 80 h_offs = 0 @@ -433,28 +427,20 @@ class GraphicsSettingsWindow(bui.Window): 0.25, bui.WeakCall(self._update_controls), repeat=True ) - def _back(self) -> None: - from bauiv1lib.settings import allsettings + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - # Applying max-fps takes a few moments. Apply if it hasn't been - # yet. + @override + def on_main_window_close(self) -> None: self._apply_max_fps() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - allsettings.AllSettingsWindow( - transition='in_left' - ).get_root_widget(), - from_window=self._root_widget, - ) - def _set_quality(self, quality: str) -> None: cfg = bui.app.config cfg['Graphics Quality'] = quality diff --git a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py index 9bda0fb4..3226f7ae 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override from bauiv1lib.popup import PopupMenuWindow import bauiv1 as bui @@ -12,13 +12,19 @@ import bascenev1 as bs if TYPE_CHECKING: from typing import Any + from bauiv1lib.popup import PopupWindow -class ConfigKeyboardWindow(bui.Window): +class ConfigKeyboardWindow(bui.MainWindow): """Window for configuring keyboards.""" - def __init__(self, c: bs.InputDevice, transition: str = 'in_right'): + def __init__( + self, + c: bs.InputDevice, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): self._r = 'configKeyboardWindow' self._input = c self._name = self._input.name @@ -39,13 +45,15 @@ class ConfigKeyboardWindow(bui.Window): root_widget=bui.containerwidget( size=(self._width, self._height), scale=( - 1.6 + 1.4 if uiscale is bui.UIScale.SMALL else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, 5) if uiscale is bui.UIScale.SMALL else (0, 0), transition=transition, - ) + ), + transition=transition, + origin_widget=origin_widget, ) self._settings: dict[str, int] = {} @@ -53,6 +61,23 @@ class ConfigKeyboardWindow(bui.Window): self._rebuild_ui() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # Pull things from self here; if we do it within the lambda + # we'll keep self alive which is bad. + inputdevice = self._input + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + c=inputdevice, + ) + ) + def _get_config_mapping(self, default: bool = False) -> None: for button in [ 'buttonJump', @@ -87,7 +112,7 @@ class ConfigKeyboardWindow(bui.Window): size=(170, 60), label=bui.Lstr(resource='cancelText'), scale=0.9, - on_activate_call=self._cancel, + on_activate_call=self.main_window_back, ) save_button = bui.buttonwidget( parent=self._root_widget, @@ -287,20 +312,6 @@ class ConfigKeyboardWindow(bui.Window): bui.pushcall(doit) - def _cancel(self) -> None: - from bauiv1lib.settings.controls import ControlsSettingsWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - bui.containerwidget(edit=self._root_widget, transition='out_right') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - ControlsSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) - def _reset(self) -> None: from bauiv1lib.confirm import ConfirmWindow @@ -366,14 +377,14 @@ class ConfigKeyboardWindow(bui.Window): """Called when the popup is closing.""" def _save(self) -> None: - from bauiv1lib.settings.controls import ControlsSettingsWindow + # from bauiv1lib.settings.controls import ControlsSettingsWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return assert bui.app.classic is not None - bui.containerwidget(edit=self._root_widget, transition='out_right') + # bui.containerwidget(edit=self._root_widget, transition='out_right') bui.getsound('gunCocking').play() # There's a chance the device disappeared; handle that gracefully. @@ -405,10 +416,13 @@ class ConfigKeyboardWindow(bui.Window): }, ) bui.app.config.apply_and_commit() - bui.app.ui_v1.set_main_menu_window( - ControlsSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + + self.main_window_back() + # bui.app.ui_v1.set_main_window( + # ControlsSettingsWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) class AwaitKeyboardInputWindow(bui.Window): diff --git a/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py b/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py index 2e7f7e8c..9542c07f 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py @@ -8,7 +8,7 @@ import time import copy import weakref from threading import Thread -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override from efro.error import CleanError from bauiv1lib.settings.testing import TestingWindow @@ -22,40 +22,53 @@ if TYPE_CHECKING: MAX_TEST_SECONDS = 60 * 2 -class NetTestingWindow(bui.Window): +class NetTestingWindow(bui.MainWindow): """Window that runs a networking test suite to help diagnose issues.""" - def __init__(self, transition: str = 'in_right'): + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): + uiscale = bui.app.ui_v1.uiscale self._width = 820 - self._height = 500 + self._height = 400 if uiscale is bui.UIScale.SMALL else 500 self._printed_lines: list[str] = [] assert bui.app.classic is not None - uiscale = bui.app.ui_v1.uiscale super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), scale=( - 1.56 + 1.75 if uiscale is bui.UIScale.SMALL else 1.2 if uiscale is bui.UIScale.MEDIUM else 0.8 ), - stack_offset=(0.0, -7 if uiscale is bui.UIScale.SMALL else 0.0), - transition=transition, - ) + stack_offset=(0, -4 if uiscale is bui.UIScale.SMALL else 0.0), + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, ) - self._done_button = bui.buttonwidget( + self._done_button: bui.Widget | None = bui.buttonwidget( parent=self._root_widget, - position=(40, self._height - 77), + position=(46, self._height - 77), size=(120, 60), scale=0.8, autoselect=True, label=bui.Lstr(resource='doneText'), - on_activate_call=self._done, + on_activate_call=self.main_window_back, ) + # Avoid squads button on small mode. + xinset = -50 if uiscale is bui.UIScale.SMALL else 0 + self._copy_button = bui.buttonwidget( parent=self._root_widget, - position=(self._width - 200, self._height - 77), + position=(self._width - 200 + xinset, self._height - 77), size=(100, 60), scale=0.8, autoselect=True, @@ -65,7 +78,7 @@ class NetTestingWindow(bui.Window): self._settings_button = bui.buttonwidget( parent=self._root_widget, - position=(self._width - 100, self._height - 77), + position=(self._width - 100 + xinset, self._height - 77), size=(60, 60), scale=0.8, autoselect=True, @@ -73,7 +86,7 @@ class NetTestingWindow(bui.Window): on_activate_call=self._show_val_testing, ) - twidth = self._width - 450 + twidth = self._width - 540 bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 55), @@ -94,9 +107,16 @@ class NetTestingWindow(bui.Window): ) self._rows = bui.columnwidget(parent=self._scroll) - bui.containerwidget( - edit=self._root_widget, cancel_button=self._done_button - ) + if uiscale is bui.UIScale.SMALL: + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + self._done_button.delete() + self._done_button = None + else: + bui.containerwidget( + edit=self._root_widget, cancel_button=self._done_button + ) # Now kick off the tests. # Pass a weak-ref to this window so we don't keep it alive @@ -107,6 +127,16 @@ class NetTestingWindow(bui.Window): target=bui.Call(_run_diagnostics, weakref.ref(self)), ).start() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + def print(self, text: str, color: tuple[float, float, float]) -> None: """Print text to our console thingie.""" for line in text.splitlines(): @@ -138,26 +168,24 @@ class NetTestingWindow(bui.Window): if not self._root_widget or self._root_widget.transitioning_out: return - bui.app.ui_v1.set_main_menu_window( - NetValTestingWindow().get_root_widget(), - from_window=self._root_widget, - ) + bui.app.ui_v1.set_main_window(NetValTestingWindow(), from_window=self) bui.containerwidget(edit=self._root_widget, transition='out_left') - def _done(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.settings.advanced import AdvancedSettingsWindow + # def _done(self) -> None: + # # pylint: disable=cyclic-import + # from bauiv1lib.settings.advanced import AdvancedSettingsWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return + # # no-op if our underlying widget is dead or on its way out. + # if not self._root_widget or self._root_widget.transitioning_out: + # return - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) - bui.containerwidget(edit=self._root_widget, transition='out_right') + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # AdvancedSettingsWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) + # bui.containerwidget(edit=self._root_widget, transition='out_right') def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: @@ -471,5 +499,4 @@ class NetValTestingWindow(TestingWindow): title=bui.Lstr(resource='settingsWindowAdvanced.netTestingText'), entries=entries, transition=transition, - back_call=lambda: NetTestingWindow(transition='in_left'), ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/plugins.py b/src/assets/ba_data/python/bauiv1lib/settings/plugins.py index b33216b6..d5f4fd81 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/plugins.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/plugins.py @@ -4,9 +4,9 @@ from __future__ import annotations -from enum import Enum import logging -from typing import TYPE_CHECKING, assert_never +from enum import Enum +from typing import TYPE_CHECKING, assert_never, override import bauiv1 as bui from bauiv1lib import popup @@ -28,35 +28,24 @@ class Category(Enum): return f'{self.value}Text' -class PluginWindow(bui.Window): +class PluginWindow(bui.MainWindow): """Window for configuring plugins.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): - # pylint: disable=too-many-statements app = bui.app self._category = Category.ALL - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale self._width = 870.0 if uiscale is bui.UIScale.SMALL else 670.0 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( - 390.0 + 370.0 if uiscale is bui.UIScale.SMALL else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0 ) @@ -64,18 +53,22 @@ class PluginWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), scale=( - 2.06 + 1.9 if uiscale is bui.UIScale.SMALL else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) self._scroll_width = self._width - (100 + 2 * x_inset) @@ -84,9 +77,9 @@ class PluginWindow(bui.Window): self._sub_height = 724.0 assert app.classic is not None - if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._do_back + edit=self._root_widget, on_cancel_call=self.main_window_back ) self._back_button = None else: @@ -98,7 +91,7 @@ class PluginWindow(bui.Window): autoselect=True, label=bui.Lstr(resource='backText'), button_type='back', - on_activate_call=self._do_back, + on_activate_call=self.main_window_back, ) bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button @@ -213,6 +206,20 @@ class PluginWindow(bui.Window): ) self._restore_state() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + def _check_value_changed(self, plug: bui.PluginSpec, value: bool) -> None: bui.screenmessage( bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'), @@ -235,9 +242,8 @@ class PluginWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - PluginSettingsWindow(transition='in_right').get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + PluginSettingsWindow(transition='in_right'), from_window=self ) def _show_category_options(self) -> None: @@ -451,21 +457,3 @@ class PluginWindow(bui.Window): bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: logging.exception('Error restoring state for %s.', self) - - def _do_back(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.settings.advanced import AdvancedSettingsWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py b/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py index d59554ff..1f3aefda 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py @@ -4,21 +4,24 @@ from __future__ import annotations +from typing import override + import bauiv1 as bui from bauiv1lib.confirm import ConfirmWindow -class PluginSettingsWindow(bui.Window): +class PluginSettingsWindow(bui.MainWindow): """Plugin Settings Window""" - def __init__(self, transition: str = 'in_right'): - scale_origin: tuple[float, float] | None - self._transition_out = 'out_right' - scale_origin = None + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - width = 470.0 if uiscale is bui.UIScale.SMALL else 470.0 + width = 670.0 if uiscale is bui.UIScale.SMALL else 470.0 height = ( 365.0 if uiscale is bui.UIScale.SMALL @@ -29,9 +32,11 @@ class PluginSettingsWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(width, height + top_extra), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), scale=( 2.06 if uiscale is bui.UIScale.SMALL @@ -40,37 +45,50 @@ class PluginSettingsWindow(bui.Window): stack_offset=( (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) - self._back_button = bui.buttonwidget( - parent=self._root_widget, - position=(53, height - 60), - size=(60, 60), - scale=0.8, - autoselect=True, - label=bui.charstr(bui.SpecialChar.BACK), - button_type='backSmall', - on_activate_call=self._do_back, - ) - bui.containerwidget( - edit=self._root_widget, cancel_button=self._back_button - ) + if uiscale is bui.UIScale.SMALL: + xoffs = 90 + self._back_button = bui.get_special_widget('back_button') + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + else: + xoffs = 0 + self._back_button = bui.buttonwidget( + parent=self._root_widget, + position=(53, height - 60), + size=(60, 60), + scale=0.8, + autoselect=True, + label=bui.charstr(bui.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self.main_window_back, + ) + bui.containerwidget( + edit=self._root_widget, cancel_button=self._back_button + ) self._title_text = bui.textwidget( parent=self._root_widget, - position=(0, height - 52), - size=(width, 25), + position=( + width * 0.5, + height - (45 if uiscale is bui.UIScale.SMALL else 35), + ), + size=(0, 0), text=bui.Lstr(resource='pluginSettingsText'), color=bui.app.ui_v1.title_color, h_align='center', - v_align='top', + v_align='center', ) self._y_position = 170 if uiscale is bui.UIScale.MEDIUM else 205 self._enable_plugins_button = bui.buttonwidget( parent=self._root_widget, - position=(65, self._y_position), + position=(xoffs + 65, self._y_position), size=(350, 60), autoselect=True, label=bui.Lstr(resource='pluginsEnableAllText'), @@ -83,7 +101,7 @@ class PluginSettingsWindow(bui.Window): self._y_position -= 70 self._disable_plugins_button = bui.buttonwidget( parent=self._root_widget, - position=(65, self._y_position), + position=(xoffs + 65, self._y_position), size=(350, 60), autoselect=True, label=bui.Lstr(resource='pluginsDisableAllText'), @@ -96,7 +114,7 @@ class PluginSettingsWindow(bui.Window): self._y_position -= 70 self._enable_new_plugins_check_box = bui.checkboxwidget( parent=self._root_widget, - position=(65, self._y_position), + position=(xoffs + 65, self._y_position), size=(350, 60), value=bui.app.config.get( bui.app.plugins.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY, @@ -108,9 +126,10 @@ class PluginSettingsWindow(bui.Window): on_value_change_call=self._update_value, ) - bui.widget( - edit=self._back_button, down_widget=self._enable_plugins_button - ) + if uiscale is not bui.UIScale.SMALL: + bui.widget( + edit=self._back_button, down_widget=self._enable_plugins_button + ) bui.widget( edit=self._disable_plugins_button, @@ -124,6 +143,16 @@ class PluginSettingsWindow(bui.Window): down_widget=self._enable_new_plugins_check_box, ) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + def _enable_all_plugins(self) -> None: cfg = bui.app.config plugs: dict[str, dict] = cfg.setdefault('Plugins', {}) @@ -152,20 +181,3 @@ class PluginSettingsWindow(bui.Window): cfg = bui.app.config cfg[bui.app.plugins.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY] = val cfg.apply_and_commit() - - def _do_back(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.settings.plugins import PluginWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - PluginWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py b/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py index f6a8216e..97af8c44 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py @@ -4,45 +4,70 @@ from __future__ import annotations +from typing import override + import bauiv1 as bui -class RemoteAppSettingsWindow(bui.Window): +class RemoteAppSettingsWindow(bui.MainWindow): """Window showing info/settings related to the remote app.""" - def __init__(self) -> None: + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ) -> None: self._r = 'connectMobileDevicesWindow' - width = 700 + app = bui.app + uiscale = app.ui_v1.uiscale + width = 800 if uiscale is bui.UIScale.SMALL else 700 height = 390 spacing = 40 assert bui.app.classic is not None - uiscale = bui.app.ui_v1.uiscale super().__init__( root_widget=bui.containerwidget( size=(width, height), - transition='in_right', + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), scale=( - 1.85 + 1.76 if uiscale is bui.UIScale.SMALL else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (-10, 0) if uiscale is bui.UIScale.SMALL else (0, 0) ), + ), + transition=transition, + origin_widget=origin_widget, + ) + if uiscale is bui.UIScale.SMALL: + bui.containerwidget( + edit=self.get_root_widget(), + on_cancel_call=self.main_window_back, + ) + else: + btn = bui.buttonwidget( + parent=self._root_widget, + position=(40, height - 67), + size=(140, 65), + scale=0.8, + label=bui.Lstr(resource='backText'), + button_type='back', + text_scale=1.1, + autoselect=True, + on_activate_call=self.main_window_back, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.buttonwidget( + edit=btn, + button_type='backSmall', + size=(60, 60), + label=bui.charstr(bui.SpecialChar.BACK), ) - ) - btn = bui.buttonwidget( - parent=self._root_widget, - position=(40, height - 67), - size=(140, 65), - scale=0.8, - label=bui.Lstr(resource='backText'), - button_type='back', - text_scale=1.1, - autoselect=True, - on_activate_call=self._back, - ) - bui.containerwidget(edit=self._root_widget, cancel_button=btn) bui.textwidget( parent=self._root_widget, @@ -56,13 +81,6 @@ class RemoteAppSettingsWindow(bui.Window): v_align='center', ) - bui.buttonwidget( - edit=btn, - button_type='backSmall', - size=(60, 60), - label=bui.charstr(bui.SpecialChar.BACK), - ) - v = height - 70.0 v -= spacing * 1.2 bui.textwidget( @@ -125,23 +143,17 @@ class RemoteAppSettingsWindow(bui.Window): on_value_change_call=self._on_check_changed, ) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + def _on_check_changed(self, value: bool) -> None: cfg = bui.app.config cfg['Enable Remote App'] = not value cfg.apply_and_commit() - - def _back(self) -> None: - from bauiv1lib.settings import controls - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - bui.containerwidget(edit=self._root_widget, transition='out_right') - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - controls.ControlsSettingsWindow( - transition='in_left' - ).get_root_widget(), - from_window=self._root_widget, - ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/testing.py b/src/assets/ba_data/python/bauiv1lib/settings/testing.py index 5af029bb..de396332 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/testing.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/testing.py @@ -13,50 +13,73 @@ if TYPE_CHECKING: from typing import Any, Callable -class TestingWindow(bui.Window): +class TestingWindow(bui.MainWindow): """Window for conveniently testing various settings.""" def __init__( self, title: bui.Lstr, entries: list[dict[str, Any]], - transition: str = 'in_right', - back_call: Callable[[], bui.Window] | None = None, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, ): assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - self._width = 600 + self._width = 700 if uiscale is bui.UIScale.SMALL else 600 self._height = 324 if uiscale is bui.UIScale.SMALL else 400 self._entries = copy.deepcopy(entries) - self._back_call = back_call super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), - transition=transition, scale=( - 2.5 + 2.27 if uiscale is bui.UIScale.SMALL else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( - (0, -28) if uiscale is bui.UIScale.SMALL else (0, 0) + (0, -20) if uiscale is bui.UIScale.SMALL else (0, 0) ), + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, + ) + + if uiscale is bui.UIScale.SMALL: + self._back_button = bui.get_special_widget('back_button') + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back ) - ) - self._back_button = btn = bui.buttonwidget( - parent=self._root_widget, - autoselect=True, - position=(65, self._height - 59), - size=(130, 60), - scale=0.8, - text_scale=1.2, - label=bui.Lstr(resource='backText'), - button_type='back', - on_activate_call=self._do_back, - ) + else: + self._back_button = btn = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(65, self._height - 59), + size=(130, 60), + scale=0.8, + text_scale=1.2, + label=bui.Lstr(resource='backText'), + button_type='back', + on_activate_call=self.main_window_back, + ) + bui.buttonwidget( + edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=bui.charstr(bui.SpecialChar.BACK), + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height - 35), + position=( + self._width * 0.5, + self._height - (42 if uiscale is bui.UIScale.SMALL else 35), + ), size=(0, 0), color=bui.app.ui_v1.title_color, h_align='center', @@ -65,16 +88,12 @@ class TestingWindow(bui.Window): text=title, ) - bui.buttonwidget( - edit=self._back_button, - button_type='backSmall', - size=(60, 60), - label=bui.charstr(bui.SpecialChar.BACK), - ) - bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height - 75), + position=( + self._width * 0.5, + self._height - (80 if uiscale is bui.UIScale.SMALL else 80), + ), size=(0, 0), color=bui.app.ui_v1.infotextcolor, h_align='center', @@ -82,7 +101,6 @@ class TestingWindow(bui.Window): maxwidth=self._width * 0.75, text=bui.Lstr(resource='settingsWindowAdvanced.forTestingText'), ) - bui.containerwidget(edit=self._root_widget, cancel_button=btn) self._scroll_width = self._width - 130 self._scroll_height = self._height - 140 self._scrollwidget = bui.scrollwidget( @@ -138,7 +156,6 @@ class TestingWindow(bui.Window): ) if i == 0: bui.widget(edit=btn, up_widget=self._back_button) - # pylint: disable=consider-using-f-string entry['widget'] = bui.textwidget( parent=self._subcontainer, position=(h + 100, v), @@ -146,7 +163,7 @@ class TestingWindow(bui.Window): h_align='center', v_align='center', maxwidth=60, - text='%.4g' % bui.app.classic.value_test(entry_name), + text=f'{bui.app.classic.value_test(entry_name):.4g}', ) btn = bui.buttonwidget( parent=self._subcontainer, @@ -185,10 +202,9 @@ class TestingWindow(bui.Window): entry['name'], absolute=bui.app.classic.value_test_defaults[entry['name']], ) - # pylint: disable=consider-using-f-string bui.textwidget( edit=entry['widget'], - text='%.4g' % bui.app.classic.value_test(entry['name']), + text=f'{bui.app.classic.value_test(entry['name']):.4g}', ) def _on_minus_press(self, entry_name: str) -> None: @@ -210,22 +226,3 @@ class TestingWindow(bui.Window): edit=entry['widget'], text='%.4g' % bui.app.classic.value_test(entry['name']), ) - - def _do_back(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.settings.advanced import AdvancedSettingsWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - bui.containerwidget(edit=self._root_widget, transition='out_right') - backwin = ( - self._back_call() - if self._back_call is not None - else AdvancedSettingsWindow(transition='in_left') - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - backwin.get_root_widget(), from_window=self._root_widget - ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py b/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py index 38aec5f9..ea965829 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py @@ -3,11 +3,13 @@ """UI settings functionality related to touchscreens.""" from __future__ import annotations +from typing import override + import bauiv1 as bui import bascenev1 as bs -class TouchscreenSettingsWindow(bui.Window): +class TouchscreenSettingsWindow(bui.MainWindow): """Settings window for touchscreens.""" def __del__(self) -> None: @@ -18,7 +20,11 @@ class TouchscreenSettingsWindow(bui.Window): # thing that exists. bs.set_touchscreen_editing(False) - def __init__(self) -> None: + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ) -> None: self._width = 650 self._height = 380 self._spacing = 40 @@ -31,13 +37,14 @@ class TouchscreenSettingsWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), - transition='in_right', scale=( 1.9 if uiscale is bui.UIScale.SMALL else 1.55 if uiscale is bui.UIScale.MEDIUM else 1.2 ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) btn = bui.buttonwidget( @@ -95,6 +102,16 @@ class TouchscreenSettingsWindow(bui.Window): ) self._build_gui() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + def _build_gui(self) -> None: from bauiv1lib.config import ConfigNumberEdit, ConfigCheckBox from bauiv1lib.radiogroup import make_radio_group @@ -280,10 +297,9 @@ class TouchscreenSettingsWindow(bui.Window): bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - controls.ControlsSettingsWindow( - transition='in_left' - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + controls.ControlsSettingsWindow(transition='in_left'), + from_window=self, + is_back=True, ) bs.set_touchscreen_editing(False) diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py index f16073bd..a052b075 100644 --- a/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py @@ -6,35 +6,29 @@ from __future__ import annotations import copy import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import bauiv1 as bui if TYPE_CHECKING: from typing import Any +REQUIRE_PRO = False -class SoundtrackBrowserWindow(bui.Window): +# Temp. +UNDER_CONSTRUCTION = True + + +class SoundtrackBrowserWindow(bui.MainWindow): """Window for browsing soundtracks.""" def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): - # pylint: disable=too-many-locals # pylint: disable=too-many-statements - # If they provided an origin-widget, scale up from that. - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - self._r = 'editSoundtrackWindow' assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale @@ -52,22 +46,26 @@ class SoundtrackBrowserWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, + toolbar_visibility=( + 'menu_minimal' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), scale=( - 2.3 + 2.1 if uiscale is bui.UIScale.SMALL else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, -18) if uiscale is bui.UIScale.SMALL else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) assert bui.app.classic is not None - if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: self._back_button = None else: self._back_button = bui.buttonwidget( @@ -239,11 +237,7 @@ class SoundtrackBrowserWindow(bui.Window): bui.widget( edit=self._scrollwidget, left_widget=self._new_button, - right_widget=( - bui.get_special_widget('party_button') - if bui.app.ui_v1.use_toolbars - else self._scrollwidget - ), + right_widget=bui.get_special_widget('squad_button'), ) self._col = bui.columnwidget(parent=scrollwidget, border=2, margin=0) @@ -255,23 +249,39 @@ class SoundtrackBrowserWindow(bui.Window): self._refresh() if self._back_button is not None: bui.buttonwidget( - edit=self._back_button, on_activate_call=self._back + edit=self._back_button, on_activate_call=self.main_window_back ) bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) else: bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._back + edit=self._root_widget, on_cancel_call=self.main_window_back ) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + def _update(self) -> None: - have = ( + have_pro = ( bui.app.classic is None or bui.app.classic.accounts.have_pro_options() ) for lock in self._lock_images: - bui.imagewidget(edit=lock, opacity=0.0 if have else 1.0) + bui.imagewidget( + edit=lock, opacity=0.0 if (have_pro or not REQUIRE_PRO) else 1.0 + ) def _do_delete_soundtrack(self) -> None: cfg = bui.app.config @@ -292,7 +302,7 @@ class SoundtrackBrowserWindow(bui.Window): from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.confirm import ConfirmWindow - if ( + if REQUIRE_PRO and ( bui.app.classic is not None and not bui.app.classic.accounts.have_pro_options() ): @@ -321,7 +331,7 @@ class SoundtrackBrowserWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.purchase import PurchaseWindow - if ( + if REQUIRE_PRO and ( bui.app.classic is not None and not bui.app.classic.accounts.have_pro_options() ): @@ -387,29 +397,30 @@ class SoundtrackBrowserWindow(bui.Window): music.music_types[bui.app.classic.MusicPlayMode.REGULAR] ) - def _back(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.settings.audio import AudioSettingsWindow + # def _back(self) -> None: + # # pylint: disable=cyclic-import + # from bauiv1lib.settings.audio import AudioSettingsWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return + # # no-op if our underlying widget is dead or on its way out. + # if not self._root_widget or self._root_widget.transitioning_out: + # return - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - AudioSettingsWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + # self._save_state() + # bui.containerwidget( + # edit=self._root_widget, transition=self._transition_out + # ) + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window( + # AudioSettingsWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) def _edit_soundtrack_with_sound(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.purchase import PurchaseWindow - if ( + if REQUIRE_PRO and ( bui.app.classic is not None and not bui.app.classic.accounts.have_pro_options() ): @@ -423,11 +434,15 @@ class SoundtrackBrowserWindow(bui.Window): from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.soundtrack.edit import SoundtrackEditWindow + if UNDER_CONSTRUCTION: + bui.screenmessage('UNDER CONSTRUCTION') + return + # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return - if ( + if REQUIRE_PRO and ( bui.app.classic is not None and not bui.app.classic.accounts.have_pro_options() ): @@ -446,11 +461,9 @@ class SoundtrackBrowserWindow(bui.Window): self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - SoundtrackEditWindow( - existing_soundtrack=self._selected_soundtrack - ).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + SoundtrackEditWindow(existing_soundtrack=self._selected_soundtrack), + from_window=self, ) def _get_soundtrack_display_name(self, soundtrack: str) -> bui.Lstr: @@ -541,7 +554,11 @@ class SoundtrackBrowserWindow(bui.Window): from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.soundtrack.edit import SoundtrackEditWindow - if ( + if UNDER_CONSTRUCTION: + bui.screenmessage('UNDER CONSTRUCTION') + return + + if REQUIRE_PRO and ( bui.app.classic is not None and not bui.app.classic.accounts.have_pro_options() ): @@ -549,9 +566,8 @@ class SoundtrackBrowserWindow(bui.Window): return self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') - bui.app.ui_v1.set_main_menu_window( - SoundtrackEditWindow(existing_soundtrack=None).get_root_widget(), - from_window=self._root_widget, + bui.app.ui_v1.set_main_window( + SoundtrackEditWindow(existing_soundtrack=None), from_window=self ) def _create_done(self, new_soundtrack: str) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py index 9a155a51..77bcae57 100644 --- a/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py +++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py @@ -6,7 +6,7 @@ from __future__ import annotations import copy import os -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, cast, override import bascenev1 as bs import bauiv1 as bui @@ -15,22 +15,26 @@ if TYPE_CHECKING: from typing import Any -class SoundtrackEditWindow(bui.Window): +class SoundtrackEditWindow(bui.MainWindow): """Window for editing a soundtrack.""" def __init__( self, existing_soundtrack: str | dict[str, Any] | None, - transition: str = 'in_right', + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements + + print('SPAWNING EDIT') + appconfig = bui.app.config self._r = 'editSoundtrackWindow' self._folder_tex = bui.gettexture('folder') self._file_tex = bui.gettexture('file') assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - self._width = 848 if uiscale is bui.UIScale.SMALL else 648 + self._width = 900 if uiscale is bui.UIScale.SMALL else 648 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 395 @@ -40,18 +44,19 @@ class SoundtrackEditWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), - transition=transition, scale=( - 2.08 + 1.8 if uiscale is bui.UIScale.SMALL else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( - (0, -48) + (0, -37) if uiscale is bui.UIScale.SMALL else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) cancel_button = bui.buttonwidget( parent=self._root_widget, @@ -93,6 +98,7 @@ class SoundtrackEditWindow(bui.Window): appconfig['Soundtracks'] = {} self._soundtrack_name: str | None + self._existing_soundtrack = existing_soundtrack self._existing_soundtrack_name: str | None if existing_soundtrack is not None: # if they passed just a name, pull info from that soundtrack @@ -189,6 +195,22 @@ class SoundtrackEditWindow(bui.Window): bui.widget(edit=self._text_field, up_widget=cancel_button) bui.widget(edit=cancel_button, down_widget=self._text_field) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # Pull this out of self here; if we do it in the lambda we'll + # keep our window alive due to the 'self' reference. + existing_soundtrack = self._existing_soundtrack + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + existing_soundtrack=existing_soundtrack, + ) + ) + def _refresh(self) -> None: for widget in self._col.get_children(): widget.delete() @@ -326,6 +348,8 @@ class SoundtrackEditWindow(bui.Window): assert bui.app.classic is not None music = bui.app.classic.music + print('GoT RESTORE', state, musictype, entry) + # Apply the change and recreate the window. soundtrack = state['soundtrack'] existing_entry = ( @@ -346,11 +370,6 @@ class SoundtrackEditWindow(bui.Window): else: soundtrack[musictype] = entry - bui.app.ui_v1.set_main_menu_window( - cls(state, transition='in_left').get_root_widget(), - from_window=False, # Disable check here. - ) - def _get_entry( self, song_type: str, entry: Any, selection_target_name: str ) -> None: @@ -369,17 +388,23 @@ class SoundtrackEditWindow(bui.Window): 'soundtrack': self._soundtrack, 'last_edited_song_type': song_type, } - bui.containerwidget(edit=self._root_widget, transition='out_left') - bui.app.ui_v1.set_main_menu_window( - music.get_music_player() - .select_entry( + self.main_window_replace( + music.get_music_player().select_entry( bui.Call(self._restore_editor, state, song_type), entry, selection_target_name, - ) - .get_root_widget(), - from_window=self._root_widget, + ), + group_id='soundtrackentryselect', ) + # bui.containerwidget(edit=self._root_widget, transition='out_left') + # bui.app.ui_v1.set_main_window( + # music.get_music_player().select_entry( + # bui.Call(self._restore_editor, state, song_type), + # entry, + # selection_target_name, + # ), + # from_window=self, + # ) def _test(self, song_type: bs.MusicType) -> None: assert bui.app.classic is not None @@ -423,7 +448,7 @@ class SoundtrackEditWindow(bui.Window): return None def _cancel(self) -> None: - from bauiv1lib.soundtrack.browser import SoundtrackBrowserWindow + # from bauiv1lib.soundtrack.browser import SoundtrackBrowserWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: @@ -434,14 +459,17 @@ class SoundtrackEditWindow(bui.Window): # Resets music back to normal. music.set_music_play_mode(bui.app.classic.MusicPlayMode.REGULAR) - bui.containerwidget(edit=self._root_widget, transition='out_right') - bui.app.ui_v1.set_main_menu_window( - SoundtrackBrowserWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + + self.main_window_back() + # bui.containerwidget(edit=self._root_widget, transition='out_right') + # bui.app.ui_v1.set_main_window( + # SoundtrackBrowserWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) def _do_it(self) -> None: - from bauiv1lib.soundtrack.browser import SoundtrackBrowserWindow + # from bauiv1lib.soundtrack.browser import SoundtrackBrowserWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: @@ -487,17 +515,19 @@ class SoundtrackEditWindow(bui.Window): cfg.commit() bui.getsound('gunCocking').play() - bui.containerwidget(edit=self._root_widget, transition='out_right') + # bui.containerwidget(edit=self._root_widget, transition='out_right') # Resets music back to normal. music.set_music_play_mode( bui.app.classic.MusicPlayMode.REGULAR, force_restart=True ) - bui.app.ui_v1.set_main_menu_window( - SoundtrackBrowserWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + self.main_window_back() + # bui.app.ui_v1.set_main_window( + # SoundtrackBrowserWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) def _do_it_with_sound(self) -> None: bui.getsound('swish').play() diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py index d0e6547f..4556e00f 100644 --- a/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py +++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py @@ -4,7 +4,7 @@ from __future__ import annotations import copy -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import bauiv1 as bui @@ -12,7 +12,7 @@ if TYPE_CHECKING: from typing import Any, Callable -class SoundtrackEntryTypeSelectWindow(bui.Window): +class SoundtrackEntryTypeSelectWindow(bui.MainWindow): """Window for selecting a soundtrack entry type.""" def __init__( @@ -20,12 +20,15 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): callback: Callable[[Any], Any], current_entry: Any, selection_target_name: str, - transition: str = 'in_right', + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, ): + # pylint: disable=too-many-locals assert bui.app.classic is not None music = bui.app.classic.music self._r = 'editSoundtrackWindow' + self._selection_target_name = selection_target_name self._callback = callback self._current_entry = copy.deepcopy(current_entry) @@ -53,13 +56,12 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): # NOTE: When something is selected, we close our UI and kick off # another window which then calls us back when its done, so the - # standard UI-cleanup-check complains that something is holding on - # to our instance after its ui is gone. Should restructure in a - # cleaner way, but just disabling that check for now. + # standard UI-cleanup-check complains that something is holding + # on to our instance after its ui is gone. Should restructure in + # a cleaner way, but just disabling that check for now. super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), - transition=transition, scale=( 1.7 if uiscale is bui.UIScale.SMALL @@ -67,6 +69,8 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): ), ), cleanupcheck=False, + transition=transition, + origin_widget=origin_widget, ) btn = bui.buttonwidget( parent=self._root_widget, @@ -157,6 +161,27 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): bui.containerwidget(edit=self._root_widget, selected_child=btn) v -= spacing + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # Pull these out of self here; if we reference self in the + # lambda we'll keep our window alive which is bad. + current_entry = self._current_entry + callback = self._callback + selection_target_name = self._selection_target_name + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + current_entry=current_entry, + callback=callback, + selection_target_name=selection_target_name, + ) + ) + def _on_mac_music_app_playlist_press(self) -> None: assert bui.app.classic is not None music = bui.app.classic.music @@ -168,7 +193,7 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): if not self._root_widget or self._root_widget.transitioning_out: return - bui.containerwidget(edit=self._root_widget, transition='out_left') + # bui.containerwidget(edit=self._root_widget, transition='out_left') current_playlist_entry: str | None if ( @@ -180,12 +205,18 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): ) else: current_playlist_entry = None - bui.app.ui_v1.set_main_menu_window( + + self.main_window_replace( MacMusicAppPlaylistSelectWindow( self._callback, current_playlist_entry, self._current_entry - ).get_root_widget(), - from_window=self._root_widget, + ), + group_id='soundtrackentryselect', ) + # MacMusicAppPlaylistSelectWindow( + # self._callback, current_playlist_entry, self._current_entry + # ), + # from_window=self, + # ) def _on_music_file_press(self) -> None: from babase import android_get_external_files_dir @@ -196,10 +227,11 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): if not self._root_widget or self._root_widget.transitioning_out: return - bui.containerwidget(edit=self._root_widget, transition='out_left') + # bui.containerwidget(edit=self._root_widget, transition='out_left') base_path = android_get_external_files_dir() assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( + + self.main_window_replace( FileSelectorWindow( base_path, callback=self._music_file_selector_cb, @@ -208,9 +240,21 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): OSMusicPlayer.get_valid_music_file_extensions() ), allow_folders=False, - ).get_root_widget(), - from_window=self._root_widget, + ), + group_id='soundtrackentryselect', ) + # bui.app.ui_v1.set_main_window( + # FileSelectorWindow( + # base_path, + # callback=self._music_file_selector_cb, + # show_base_path=False, + # valid_file_extensions=( + # OSMusicPlayer.get_valid_music_file_extensions() + # ), + # allow_folders=False, + # ), + # from_window=self, + # ) def _on_music_folder_press(self) -> None: from bauiv1lib.fileselector import FileSelectorWindow @@ -220,19 +264,30 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): if not self._root_widget or self._root_widget.transitioning_out: return - bui.containerwidget(edit=self._root_widget, transition='out_left') + # bui.containerwidget(edit=self._root_widget, transition='out_left') base_path = android_get_external_files_dir() assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( + + self.main_window_replace( FileSelectorWindow( base_path, callback=self._music_folder_selector_cb, show_base_path=False, valid_file_extensions=[], allow_folders=True, - ).get_root_widget(), - from_window=self._root_widget, + ), + group_id='soundtrackentryselect', ) + # bui.app.ui_v1.set_main_window( + # FileSelectorWindow( + # base_path, + # callback=self._music_folder_selector_cb, + # show_base_path=False, + # valid_file_extensions=[], + # allow_folders=True, + # ), + # from_window=self, + # ) def _music_file_selector_cb(self, result: str | None) -> None: if result is None: @@ -247,9 +302,11 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): self._callback({'type': 'musicFolder', 'name': result}) def _on_default_press(self) -> None: - bui.containerwidget(edit=self._root_widget, transition='out_right') + self.main_window_back() + # bui.containerwidget(edit=self._root_widget, transition='out_right') self._callback(None) def _on_cancel_press(self) -> None: - bui.containerwidget(edit=self._root_widget, transition='out_right') + self.main_window_back() + # bui.containerwidget(edit=self._root_widget, transition='out_right') self._callback(self._current_entry) diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/macmusicapp.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/macmusicapp.py index 99c60a69..b4c34ef8 100644 --- a/src/assets/ba_data/python/bauiv1lib/soundtrack/macmusicapp.py +++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/macmusicapp.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from typing import Any, Callable -class MacMusicAppPlaylistSelectWindow(bui.Window): +class MacMusicAppPlaylistSelectWindow(bui.MainWindow): """Window for selecting an iTunes playlist.""" def __init__( @@ -21,6 +21,8 @@ class MacMusicAppPlaylistSelectWindow(bui.Window): callback: Callable[[Any], Any], existing_playlist: str | None, existing_entry: Any, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, ): from baclassic.macmusicapp import MacMusicAppMusicPlayer @@ -34,9 +36,9 @@ class MacMusicAppPlaylistSelectWindow(bui.Window): v = self._height - 90.0 v -= self._spacing * 1.0 super().__init__( - root_widget=bui.containerwidget( - size=(self._width, self._height), transition='in_right' - ) + root_widget=bui.containerwidget(size=(self._width, self._height)), + transition=transition, + origin_widget=origin_widget, ) btn = bui.buttonwidget( parent=self._root_widget, @@ -112,9 +114,12 @@ class MacMusicAppPlaylistSelectWindow(bui.Window): def _sel(self, selection: str) -> None: if self._root_widget: - bui.containerwidget(edit=self._root_widget, transition='out_right') + # bui.containerwidget( + # edit=self._root_widget, transition='out_right') self._callback({'type': 'iTunesPlaylist', 'name': selection}) + self.main_window_back() def _back(self) -> None: - bui.containerwidget(edit=self._root_widget, transition='out_right') + # bui.containerwidget(edit=self._root_widget, transition='out_right') + self.main_window_back() self._callback(self._existing_entry) diff --git a/src/assets/ba_data/python/bauiv1lib/specialoffer.py b/src/assets/ba_data/python/bauiv1lib/specialoffer.py index 33853420..8d092e3d 100644 --- a/src/assets/ba_data/python/bauiv1lib/specialoffer.py +++ b/src/assets/ba_data/python/bauiv1lib/specialoffer.py @@ -283,24 +283,24 @@ class SpecialOfferWindow(bui.Window): ) # Add ticket button only if this is ticket-purchasable. - if isinstance(offer.get('price'), int): - self._get_tickets_button = bui.buttonwidget( - parent=self._root_widget, - position=(self._width - 125, self._height - 68), - size=(90, 55), - scale=1.0, - button_type='square', - color=(0.7, 0.5, 0.85), - textcolor=(0.2, 1, 0.2), - autoselect=True, - label=bui.Lstr(resource='getTicketsWindow.titleText'), - on_activate_call=self._on_get_more_tickets_press, - ) + # if isinstance(offer.get('price'), int): + # self._get_tickets_button = bui.buttonwidget( + # parent=self._root_widget, + # position=(self._width - 125, self._height - 68), + # size=(90, 55), + # scale=1.0, + # button_type='square', + # color=(0.7, 0.5, 0.85), + # textcolor=(0.2, 1, 0.2), + # autoselect=True, + # label=bui.Lstr(resource='getTicketsWindow.titleText'), + # on_activate_call=self._on_get_more_tickets_press, + # ) - self._ticket_text_update_timer = bui.AppTimer( - 1.0, bui.WeakCall(self._update_tickets_text), repeat=True - ) - self._update_tickets_text() + # self._ticket_text_update_timer = bui.AppTimer( + # 1.0, bui.WeakCall(self._update_tickets_text), repeat=True + # ) + # self._update_tickets_text() self._update_timer = bui.AppTimer( 1.0, bui.WeakCall(self._update), repeat=True @@ -421,37 +421,37 @@ class SpecialOfferWindow(bui.Window): bui.containerwidget(edit=self._root_widget, transition=transition) - def _update_tickets_text(self) -> None: - from babase import SpecialChar + # def _update_tickets_text(self) -> None: + # from babase import SpecialChar - plus = bui.app.plus - assert plus is not None + # plus = bui.app.plus + # assert plus is not None - if not self._root_widget: - return - sval: str | bui.Lstr - if plus.get_v1_account_state() == 'signed_in': - sval = bui.charstr(SpecialChar.TICKET) + str( - plus.get_v1_account_ticket_count() - ) - else: - sval = bui.Lstr(resource='getTicketsWindow.titleText') - bui.buttonwidget(edit=self._get_tickets_button, label=sval) + # if not self._root_widget: + # return + # sval: str | bui.Lstr + # if plus.get_v1_account_state() == 'signed_in': + # sval = bui.charstr(SpecialChar.TICKET) + str( + # plus.get_v1_account_ticket_count() + # ) + # else: + # sval = bui.Lstr(resource='getTicketsWindow.titleText') + # bui.buttonwidget(edit=self._get_tickets_button, label=sval) - def _on_get_more_tickets_press(self) -> None: - from bauiv1lib import account - from bauiv1lib import gettickets + # def _on_get_more_tickets_press(self) -> None: + # from bauiv1lib import account + # from bauiv1lib import gettickets - plus = bui.app.plus - assert plus is not None + # plus = bui.app.plus + # assert plus is not None - if plus.get_v1_account_state() != 'signed_in': - account.show_sign_in_prompt() - return - gettickets.GetTicketsWindow(modal=True).get_root_widget() + # if plus.get_v1_account_state() != 'signed_in': + # account.show_sign_in_prompt() + # return + # gettickets.GetTicketsWindow(modal=True).get_root_widget() def _purchase(self) -> None: - from bauiv1lib import gettickets + # from bauiv1lib import gettickets from bauiv1lib import confirm plus = bui.app.plus @@ -474,7 +474,7 @@ class SpecialOfferWindow(bui.Window): except Exception: ticket_count = None if ticket_count is not None and ticket_count < self._offer['price']: - gettickets.show_get_tickets_prompt() + # gettickets.show_get_tickets_prompt() bui.getsound('error').play() return diff --git a/src/assets/ba_data/python/bauiv1lib/store/browser.py b/src/assets/ba_data/python/bauiv1lib/store/browser.py index 8ab52102..6a41feed 100644 --- a/src/assets/ba_data/python/bauiv1lib/store/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/store/browser.py @@ -13,7 +13,7 @@ import weakref import datetime from enum import Enum from threading import Thread -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override from efro.util import utc_now from efro.error import CommunicationError @@ -26,7 +26,7 @@ if TYPE_CHECKING: MERCH_LINK_KEY = 'Merch Link' -class StoreBrowserWindow(bui.Window): +class StoreBrowserWindow(bui.MainWindow): """Window for browsing the store.""" class TabID(Enum): @@ -40,7 +40,7 @@ class StoreBrowserWindow(bui.Window): def __init__( self, - transition: str = 'in_right', + transition: str | None = 'in_right', modal: bool = False, show_tab: StoreBrowserWindow.TabID | None = None, on_close_call: Callable[[], Any] | None = None, @@ -58,16 +58,11 @@ class StoreBrowserWindow(bui.Window): bui.set_analytics_screen('Store Window') - scale_origin: tuple[float, float] | None - - # If they provided an origin-widget, scale up from that. + # Need to store this ourself for modal mode. if origin_widget is not None: self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' else: self._transition_out = 'out_right' - scale_origin = None self.button_infos: dict[str, dict[str, Any]] | None = None self.update_buttons_timer: bui.AppTimer | None = None @@ -77,10 +72,10 @@ class StoreBrowserWindow(bui.Window): self._on_close_call = on_close_call self._show_tab = show_tab self._modal = modal - self._width = 1440 if uiscale is bui.UIScale.SMALL else 1040 - self._x_inset = x_inset = 200 if uiscale is bui.UIScale.SMALL else 0 + self._width = 1670 if uiscale is bui.UIScale.SMALL else 1040 + self._x_inset = x_inset = 310 if uiscale is bui.UIScale.SMALL else 0 self._height = ( - 578 + 538 if uiscale is bui.UIScale.SMALL else 645 if uiscale is bui.UIScale.MEDIUM else 800 ) @@ -94,20 +89,24 @@ class StoreBrowserWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + extra_top), - transition=transition, - toolbar_visibility='menu_full', + toolbar_visibility=( + 'menu_store' + if uiscale is bui.UIScale.SMALL + else 'menu_full' + ), scale=( 1.3 if uiscale is bui.UIScale.SMALL else 0.9 if uiscale is bui.UIScale.MEDIUM else 0.8 ), - scale_origin_stack_offset=scale_origin, stack_offset=( - (0, -5) + (0, 10) if uiscale is bui.UIScale.SMALL else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) self._back_button = btn = bui.buttonwidget( @@ -120,48 +119,60 @@ class StoreBrowserWindow(bui.Window): button_type=None if self._modal else 'back', on_activate_call=self._back, ) - bui.containerwidget(edit=self._root_widget, cancel_button=btn) - self._ticket_count_text: bui.Widget | None = None - self._get_tickets_button: bui.Widget | None = None - - if app.classic.allow_ticket_purchases: - self._get_tickets_button = bui.buttonwidget( - parent=self._root_widget, - size=(210, 65), - on_activate_call=self._on_get_more_tickets_press, - autoselect=True, - scale=0.9, - text_scale=1.4, - left_widget=self._back_button, - color=(0.7, 0.5, 0.85), - textcolor=(0.2, 1.0, 0.2), - label=bui.Lstr(resource='getTicketsWindow.titleText'), + if uiscale is bui.UIScale.SMALL: + self._back_button.delete() + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self._back ) + # backbutton = bui.get_special_widget('back_button') + backbuttonspecial = True else: - self._ticket_count_text = bui.textwidget( - parent=self._root_widget, - size=(210, 64), - color=(0.2, 1.0, 0.2), - h_align='center', - v_align='center', - ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + # backbutton = self._back_button + backbuttonspecial = False + + # self._ticket_count_text: bui.Widget | None = None + # self._get_tickets_button: bui.Widget | None = None + + # if bool(False): + # if app.classic.allow_ticket_purchases: + # self._get_tickets_button = bui.buttonwidget( + # parent=self._root_widget, + # size=(210, 65), + # on_activate_call=self._on_get_more_tickets_press, + # autoselect=True, + # scale=0.9, + # text_scale=1.4, + # left_widget=backbutton, + # color=(0.7, 0.5, 0.85), + # textcolor=(0.2, 1.0, 0.2), + # label=bui.Lstr(resource='getTicketsWindow.titleText'), + # ) + # else: + # self._ticket_count_text = bui.textwidget( + # parent=self._root_widget, + # size=(210, 64), + # color=(0.2, 1.0, 0.2), + # h_align='center', + # v_align='center', + # ) # Move this dynamically to keep it out of the way of the party icon. - self._update_get_tickets_button_pos() - self._get_ticket_pos_update_timer = bui.AppTimer( - 1.0, - bui.WeakCall(self._update_get_tickets_button_pos), - repeat=True, - ) - if self._get_tickets_button: - bui.widget( - edit=self._back_button, right_widget=self._get_tickets_button - ) - self._ticket_text_update_timer = bui.AppTimer( - 1.0, bui.WeakCall(self._update_tickets_text), repeat=True - ) - self._update_tickets_text() + # self._update_get_tickets_button_pos() + # self._get_ticket_pos_update_timer = bui.AppTimer( + # 1.0, + # bui.WeakCall(self._update_get_tickets_button_pos), + # repeat=True, + # ) + # if self._get_tickets_button and not backbuttonspecial: + # bui.widget( + # edit=self._back_button, right_widget=self._get_tickets_button + # ) + # self._ticket_text_update_timer = bui.AppTimer( + # 1.0, bui.WeakCall(self._update_tickets_text), repeat=True + # ) + # self._update_tickets_text() if ( app.classic.platform in ['mac', 'ios'] @@ -183,17 +194,20 @@ class StoreBrowserWindow(bui.Window): bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height - 44), + position=( + self._width * 0.5, + self._height - (53 if uiscale is bui.UIScale.SMALL else 44), + ), size=(0, 0), color=app.ui_v1.title_color, scale=1.5, h_align='center', v_align='center', text=bui.Lstr(resource='storeText'), - maxwidth=420, + maxwidth=290, ) - if not self._modal: + if not self._modal and not backbuttonspecial: bui.buttonwidget( edit=self._back_button, button_type='backSmall', @@ -302,15 +316,27 @@ class StoreBrowserWindow(bui.Window): ) self._update_tabs() - if self._get_tickets_button: + # if self._get_tickets_button: + # last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button + # bui.widget( + # edit=self._get_tickets_button, down_widget=last_tab_button + # ) + # bui.widget( + # edit=last_tab_button, + # up_widget=self._get_tickets_button, + # right_widget=self._get_tickets_button, + # ) + + if uiscale is bui.UIScale.SMALL: + first_tab_button = self._tab_row.tabs[tabs_def[0][0]].button last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button bui.widget( - edit=self._get_tickets_button, down_widget=last_tab_button + edit=first_tab_button, + left_widget=bui.get_special_widget('back_button'), ) bui.widget( edit=last_tab_button, - up_widget=self._get_tickets_button, - right_widget=self._get_tickets_button, + right_widget=bui.get_special_widget('squad_button'), ) self._scroll_width = self._width - scroll_buffer_h @@ -320,27 +346,27 @@ class StoreBrowserWindow(bui.Window): self._status_textwidget: bui.Widget | None = None self._restore_state() - def _update_get_tickets_button_pos(self) -> None: - assert bui.app.classic is not None - uiscale = bui.app.ui_v1.uiscale - pos = ( - self._width - - 252 - - ( - self._x_inset - + ( - 47 - if uiscale is bui.UIScale.SMALL - and bui.is_party_icon_visible() - else 0 - ) - ), - self._height - 70, - ) - if self._get_tickets_button: - bui.buttonwidget(edit=self._get_tickets_button, position=pos) - if self._ticket_count_text: - bui.textwidget(edit=self._ticket_count_text, position=pos) + # def _update_get_tickets_button_pos(self) -> None: + # assert bui.app.classic is not None + # uiscale = bui.app.ui_v1.uiscale + # pos = ( + # self._width + # - 252 + # - ( + # self._x_inset + # + ( + # 47 + # if uiscale is bui.UIScale.SMALL + # and bui.is_party_icon_visible() + # else 0 + # ) + # ), + # self._height - 70, + # ) + # if self._get_tickets_button: + # bui.buttonwidget(edit=self._get_tickets_button, position=pos) + # if self._ticket_count_text: + # bui.textwidget(edit=self._ticket_count_text, position=pos) def _restore_purchases(self) -> None: from bauiv1lib import account @@ -385,24 +411,24 @@ class StoreBrowserWindow(bui.Window): bui.textwidget(edit=tab_data['text'], text='') bui.imagewidget(edit=tab_data['img'], opacity=0.0) - def _update_tickets_text(self) -> None: - from bauiv1 import SpecialChar + # def _update_tickets_text(self) -> None: + # from bauiv1 import SpecialChar - if not self._root_widget: - return - plus = bui.app.plus - assert plus is not None - sval: str | bui.Lstr - if plus.get_v1_account_state() == 'signed_in': - sval = bui.charstr(SpecialChar.TICKET) + str( - plus.get_v1_account_ticket_count() - ) - else: - sval = bui.Lstr(resource='getTicketsWindow.titleText') - if self._get_tickets_button: - bui.buttonwidget(edit=self._get_tickets_button, label=sval) - if self._ticket_count_text: - bui.textwidget(edit=self._ticket_count_text, text=sval) + # if not self._root_widget: + # return + # plus = bui.app.plus + # assert plus is not None + # sval: str | bui.Lstr + # if plus.get_v1_account_state() == 'signed_in': + # sval = bui.charstr(SpecialChar.TICKET) + str( + # plus.get_v1_account_ticket_count() + # ) + # else: + # sval = bui.Lstr(resource='getTicketsWindow.titleText') + # if self._get_tickets_button: + # bui.buttonwidget(edit=self._get_tickets_button, label=sval) + # if self._ticket_count_text: + # bui.textwidget(edit=self._ticket_count_text, text=sval) def _set_tab(self, tab_id: TabID) -> None: if self._current_tab is tab_id: @@ -574,7 +600,8 @@ class StoreBrowserWindow(bui.Window): """Attempt to purchase the provided item.""" from bauiv1lib import account from bauiv1lib.confirm import ConfirmWindow - from bauiv1lib import gettickets + + # from bauiv1lib import gettickets assert bui.app.classic is not None store = bui.app.classic.store @@ -620,7 +647,8 @@ class StoreBrowserWindow(bui.Window): our_tickets = plus.get_v1_account_ticket_count() if price is not None and our_tickets < price: bui.getsound('error').play() - gettickets.show_get_tickets_prompt() + print('FIXME - show not-enough-tickets info.') + # gettickets.show_get_tickets_prompt() else: def do_it() -> None: @@ -1242,6 +1270,20 @@ class StoreBrowserWindow(bui.Window): maxwidth=self._scroll_width * 0.9, ) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + def _save_state(self) -> None: try: sel = self._root_widget.get_selected_child() @@ -1250,9 +1292,9 @@ class StoreBrowserWindow(bui.Window): for tab_id, tab in self._tab_row.tabs.items() if sel == tab.button ] - if sel == self._get_tickets_button: - sel_name = 'GetTickets' - elif sel == self._scrollwidget: + # if sel == self._get_tickets_button: + # sel_name = 'GetTickets' + if sel == self._scrollwidget: sel_name = 'Scroll' elif sel == self._back_button: sel_name = 'Back' @@ -1269,7 +1311,6 @@ class StoreBrowserWindow(bui.Window): logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: - from efro.util import enum_by_value try: sel: bui.Widget | None @@ -1280,25 +1321,21 @@ class StoreBrowserWindow(bui.Window): assert isinstance(sel_name, (str, type(None))) try: - current_tab = enum_by_value( - self.TabID, bui.app.config.get('Store Tab') - ) + current_tab = self.TabID(bui.app.config.get('Store Tab')) except ValueError: current_tab = self.TabID.CHARACTERS if self._show_tab is not None: current_tab = self._show_tab - if sel_name == 'GetTickets' and self._get_tickets_button: - sel = self._get_tickets_button - elif sel_name == 'Back': + # if sel_name == 'GetTickets' and self._get_tickets_button: + # sel = self._get_tickets_button + if sel_name == 'Back': sel = self._back_button elif sel_name == 'Scroll': sel = self._scrollwidget elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): try: - sel_tab_id = enum_by_value( - self.TabID, sel_name.split(':')[-1] - ) + sel_tab_id = self.TabID(sel_name.split(':')[-1]) except ValueError: sel_tab_id = self.TabID.CHARACTERS sel = self._tab_row.tabs[sel_tab_id].button @@ -1317,58 +1354,63 @@ class StoreBrowserWindow(bui.Window): except Exception: logging.exception('Error restoring state for %s.', self) - def _on_get_more_tickets_press(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.account import show_sign_in_prompt - from bauiv1lib.gettickets import GetTicketsWindow + # def _on_get_more_tickets_press(self) -> None: + # # pylint: disable=cyclic-import + # from bauiv1lib.account import show_sign_in_prompt + # from bauiv1lib.gettickets import GetTicketsWindow - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return + # # no-op if our underlying widget is dead or on its way out. + # if not self._root_widget or self._root_widget.transitioning_out: + # return - plus = bui.app.plus - assert plus is not None + # plus = bui.app.plus + # assert plus is not None - if plus.get_v1_account_state() != 'signed_in': - show_sign_in_prompt() - return - self._save_state() - bui.containerwidget(edit=self._root_widget, transition='out_left') - window = GetTicketsWindow( - from_modal_store=self._modal, - store_back_location=self._back_location, - ).get_root_widget() - if not self._modal: - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - window, from_window=self._root_widget - ) + # if plus.get_v1_account_state() != 'signed_in': + # show_sign_in_prompt() + # return + # self._save_state() + # bui.containerwidget(edit=self._root_widget, transition='out_left') + # window = GetTicketsWindow( + # from_modal_store=self._modal, + # store_back_location=self._back_location, + # ) + # if not self._modal: + # assert bui.app.classic is not None + # bui.app.ui_v1.set_main_window(window, from_window=self) def _back(self) -> None: # pylint: disable=cyclic-import - from bauiv1lib.coop.browser import CoopBrowserWindow - from bauiv1lib.mainmenu import MainMenuWindow + # from bauiv1lib.coop.browser import CoopBrowserWindow + # from bauiv1lib.mainmenu import MainMenuWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - if not self._modal: - assert bui.app.classic is not None - if self._back_location == 'CoopBrowserWindow': - bui.app.ui_v1.set_main_menu_window( - CoopBrowserWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) - else: - bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) + + if self._modal: + bui.containerwidget( + edit=self._root_widget, transition=self._transition_out + ) + else: + self.main_window_back() + # if not self._modal: + # assert bui.app.classic is not None + # if self._back_location == 'CoopBrowserWindow': + # bui.app.ui_v1.set_main_window( + # CoopBrowserWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # ) + # else: + # bui.app.ui_v1.set_main_window( + # MainMenuWindow(transition='in_left'), + # from_window=self, + # is_back=True, + # is_top_level=True, + # ) if self._on_close_call is not None: self._on_close_call() diff --git a/src/assets/ba_data/python/bauiv1lib/tournamententry.py b/src/assets/ba_data/python/bauiv1lib/tournamententry.py index 972fddcf..025ae53b 100644 --- a/src/assets/ba_data/python/bauiv1lib/tournamententry.py +++ b/src/assets/ba_data/python/bauiv1lib/tournamententry.py @@ -100,7 +100,7 @@ class TournamentEntryWindow(PopupWindow): scale=scale, bg_color=bg_color, offset=offset, - toolbar_visibility='menu_currency', + toolbar_visibility='menu_store_no_back', ) self._last_ad_press_time = -9999.0 @@ -273,28 +273,6 @@ class TournamentEntryWindow(PopupWindow): self._get_tickets_button: bui.Widget | None = None self._ticket_count_text: bui.Widget | None = None - if not bui.app.ui_v1.use_toolbars: - if bui.app.classic.allow_ticket_purchases: - self._get_tickets_button = bui.buttonwidget( - parent=self.root_widget, - position=(self._width - 190 + 105, self._height - 34), - autoselect=True, - scale=0.5, - size=(120, 60), - textcolor=(0.2, 1, 0.2), - label=bui.charstr(bui.SpecialChar.TICKET), - color=(0.65, 0.5, 0.8), - on_activate_call=self._on_get_tickets_press, - ) - else: - self._ticket_count_text = bui.textwidget( - parent=self.root_widget, - scale=0.5, - position=(self._width - 190 + 125, self._height - 34), - color=(0.2, 1, 0.2), - h_align='center', - v_align='center', - ) self._seconds_remaining = None @@ -632,7 +610,7 @@ class TournamentEntryWindow(PopupWindow): bui.apptimer(0 if practice else 1.25, self._transition_out) def _on_pay_with_tickets_press(self) -> None: - from bauiv1lib import gettickets + # from bauiv1lib import gettickets plus = bui.app.plus assert plus is not None @@ -675,7 +653,8 @@ class TournamentEntryWindow(PopupWindow): ticket_count = None ticket_cost = self._purchase_price if ticket_count is not None and ticket_count < ticket_cost: - gettickets.show_get_tickets_prompt() + # gettickets.show_get_tickets_prompt() + print('FIXME - show not-enough-tickets msg.') bui.getsound('error').play() self._transition_out() return @@ -780,19 +759,19 @@ class TournamentEntryWindow(PopupWindow): plus.run_v1_account_transactions() self._launch() - def _on_get_tickets_press(self) -> None: - from bauiv1lib import gettickets + # def _on_get_tickets_press(self) -> None: + # from bauiv1lib import gettickets - # If we're already entering, ignore presses. - if self._entering: - return + # # 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() + # # 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 diff --git a/src/assets/ba_data/python/bauiv1lib/v2upgrade.py b/src/assets/ba_data/python/bauiv1lib/v2upgrade.py index 1ce5b3be..381500e0 100644 --- a/src/assets/ba_data/python/bauiv1lib/v2upgrade.py +++ b/src/assets/ba_data/python/bauiv1lib/v2upgrade.py @@ -11,7 +11,6 @@ class V2UpgradeWindow(bui.Window): """A window presenting a URL to the user visually.""" def __init__(self, login_name: str, code: str): - from bauiv1lib.account.settings import show_what_is_v2_page app = bui.app assert app.classic is not None @@ -116,3 +115,12 @@ class V2UpgradeWindow(bui.Window): def _done(self) -> None: bui.containerwidget(edit=self._root_widget, transition='out_left') + + +def show_what_is_v2_page() -> None: + """Show the webpage describing V2 accounts.""" + plus = bui.app.plus + assert plus is not None + + bamasteraddr = plus.get_master_server_address(version=2) + bui.open_url(f'{bamasteraddr}/whatisv2') diff --git a/src/assets/ba_data/python/bauiv1lib/watch.py b/src/assets/ba_data/python/bauiv1lib/watch.py index 84ef3c87..7d6ced1a 100644 --- a/src/assets/ba_data/python/bauiv1lib/watch.py +++ b/src/assets/ba_data/python/bauiv1lib/watch.py @@ -7,7 +7,7 @@ from __future__ import annotations import os import logging from enum import Enum -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, cast, override import bascenev1 as bs import bauiv1 as bui @@ -16,7 +16,7 @@ if TYPE_CHECKING: from typing import Any -class WatchWindow(bui.Window): +class WatchWindow(bui.MainWindow): """Window for watching replays.""" class TabID(Enum): @@ -31,20 +31,9 @@ class WatchWindow(bui.Window): origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-locals - # pylint: disable=too-many-statements from bauiv1lib.tabs import TabRow bui.set_analytics_screen('Watch Window') - scale_origin: tuple[float, float] | None - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_location('Watch') self._tab_data: dict[str, Any] = {} self._my_replays_scroll_width: float | None = None self._my_replays_watch_replay_button: bui.Widget | None = None @@ -58,7 +47,7 @@ class WatchWindow(bui.Window): self._width = 1440 if uiscale is bui.UIScale.SMALL else 1040 x_inset = 200 if uiscale is bui.UIScale.SMALL else 0 self._height = ( - 578 + 570 if uiscale is bui.UIScale.SMALL else 670 if uiscale is bui.UIScale.MEDIUM else 800 ) @@ -68,25 +57,29 @@ class WatchWindow(bui.Window): super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + extra_top), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, - scale=( - 1.3 + toolbar_visibility=( + 'menu_minimal' if uiscale is bui.UIScale.SMALL - else 0.97 if uiscale is bui.UIScale.MEDIUM else 0.8 + else 'menu_full' + ), + scale=( + 1.32 + if uiscale is bui.UIScale.SMALL + else 0.85 if uiscale is bui.UIScale.MEDIUM else 0.65 ), stack_offset=( - (0, -10) + (0, 30) if uiscale is bui.UIScale.SMALL - else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0) + else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) ), - ) + ), + transition=transition, + origin_widget=origin_widget, ) - if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars: + if uiscale is bui.UIScale.SMALL: bui.containerwidget( - edit=self._root_widget, on_cancel_call=self._back + edit=self._root_widget, on_cancel_call=self.main_window_back ) self._back_button = None else: @@ -98,7 +91,7 @@ class WatchWindow(bui.Window): scale=1.1, label=bui.Lstr(resource='backText'), button_type='back', - on_activate_call=self._back, + on_activate_call=self.main_window_back, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) bui.buttonwidget( @@ -110,13 +103,20 @@ class WatchWindow(bui.Window): bui.textwidget( parent=self._root_widget, - position=(self._width * 0.5, self._height - 38), + position=( + self._width * 0.5, + self._height - (65 if uiscale is bui.UIScale.SMALL else 38), + ), size=(0, 0), color=bui.app.ui_v1.title_color, - scale=1.5, + scale=0.7 if uiscale is bui.UIScale.SMALL else 1.5, h_align='center', v_align='center', - text=bui.Lstr(resource=f'{self._r}.titleText'), + text=( + '' + if uiscale is bui.UIScale.SMALL + else bui.Lstr(resource=f'{self._r}.titleText') + ), maxwidth=400, ) @@ -139,18 +139,15 @@ class WatchWindow(bui.Window): on_select_call=self._set_tab, ) - if bui.app.ui_v1.use_toolbars: - first_tab = self._tab_row.tabs[tabdefs[0][0]] - last_tab = self._tab_row.tabs[tabdefs[-1][0]] - bui.widget( - edit=last_tab.button, - right_widget=bui.get_special_widget('party_button'), - ) - if uiscale is bui.UIScale.SMALL: - bbtn = bui.get_special_widget('back_button') - bui.widget( - edit=first_tab.button, up_widget=bbtn, left_widget=bbtn - ) + first_tab = self._tab_row.tabs[tabdefs[0][0]] + last_tab = self._tab_row.tabs[tabdefs[-1][0]] + bui.widget( + edit=last_tab.button, + right_widget=bui.get_special_widget('squad_button'), + ) + if uiscale is bui.UIScale.SMALL: + bbtn = bui.get_special_widget('back_button') + bui.widget(edit=first_tab.button, up_widget=bbtn, left_widget=bbtn) self._scroll_width = self._width - scroll_buffer_h self._scroll_height = self._height - 180 @@ -174,6 +171,20 @@ class WatchWindow(bui.Window): self._restore_state() + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def on_main_window_close(self) -> None: + self._save_state() + def _set_tab(self, tab_id: TabID) -> None: # pylint: disable=too-many-locals @@ -281,7 +292,7 @@ class WatchWindow(bui.Window): ) bui.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button) assert bui.app.classic is not None - if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars: + if uiscale is bui.UIScale.SMALL: bui.widget( edit=btn1, left_widget=bui.get_special_widget('back_button'), @@ -611,8 +622,6 @@ class WatchWindow(bui.Window): logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: - from efro.util import enum_by_value - try: sel: bui.Widget | None assert bui.app.classic is not None @@ -621,9 +630,7 @@ class WatchWindow(bui.Window): ) assert isinstance(sel_name, (str, type(None))) try: - current_tab = enum_by_value( - self.TabID, bui.app.config.get('Watch Tab') - ) + current_tab = self.TabID(bui.app.config.get('Watch Tab')) except ValueError: current_tab = self.TabID.MY_REPLAYS self._set_tab(current_tab) @@ -634,9 +641,7 @@ class WatchWindow(bui.Window): sel = self._tab_container elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): try: - sel_tab_id = enum_by_value( - self.TabID, sel_name.split(':')[-1] - ) + sel_tab_id = self.TabID(sel_name.split(':')[-1]) except ValueError: sel_tab_id = self.TabID.MY_REPLAYS sel = self._tab_row.tabs[sel_tab_id].button @@ -648,20 +653,3 @@ class WatchWindow(bui.Window): bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: logging.exception('Error restoring state for %s.', self) - - def _back(self) -> None: - from bauiv1lib.mainmenu import MainMenuWindow - - # no-op if our underlying widget is dead or on its way out. - if not self._root_widget or self._root_widget.transitioning_out: - return - - self._save_state() - bui.containerwidget( - edit=self._root_widget, transition=self._transition_out - ) - assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget(), - from_window=self._root_widget, - ) diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.cc b/src/ballistica/base/app_adapter/app_adapter_sdl.cc index 0a3e6a92..d574f665 100644 --- a/src/ballistica/base/app_adapter/app_adapter_sdl.cc +++ b/src/ballistica/base/app_adapter/app_adapter_sdl.cc @@ -647,8 +647,14 @@ void AppAdapterSDL::ReloadRenderer_(const GraphicsSettings_* settings) { fullscreen_ = settings->fullscreen; // A reasonable default window size. - auto width = static_cast(kBaseVirtualResX * 0.8f); - auto height = static_cast(kBaseVirtualResY * 0.8f); + int width, height; + if (g_base->ui->scale() == UIScale::kSmall) { + width = static_cast(kBaseVirtualResSmallX * 0.8f); + height = static_cast(kBaseVirtualResSmallY * 0.8f); + } else { + width = static_cast(kBaseVirtualResX * 0.8f); + height = static_cast(kBaseVirtualResY * 0.8f); + } uint32_t flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE; diff --git a/src/ballistica/base/app_mode/app_mode_empty.cc b/src/ballistica/base/app_mode/empty_app_mode.cc similarity index 65% rename from src/ballistica/base/app_mode/app_mode_empty.cc rename to src/ballistica/base/app_mode/empty_app_mode.cc index 6f409f28..2cd147a1 100644 --- a/src/ballistica/base/app_mode/app_mode_empty.cc +++ b/src/ballistica/base/app_mode/empty_app_mode.cc @@ -1,35 +1,50 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/base/app_mode/app_mode_empty.h" +#include "ballistica/base/app_mode/empty_app_mode.h" #include "ballistica/base/graphics/component/simple_component.h" namespace ballistica::base { -static AppModeEmpty* g_app_mode_empty{}; +static EmptyAppMode* g_empty_app_mode{}; -AppModeEmpty::AppModeEmpty() = default; +EmptyAppMode::EmptyAppMode() = default; -auto AppModeEmpty::GetSingleton() -> AppModeEmpty* { +auto EmptyAppMode::GetSingleton() -> EmptyAppMode* { assert(g_base == nullptr || g_base->InLogicThread()); - if (g_app_mode_empty == nullptr) { - g_app_mode_empty = new AppModeEmpty(); + if (g_empty_app_mode == nullptr) { + g_empty_app_mode = new EmptyAppMode(); } - return g_app_mode_empty; + return g_empty_app_mode; } -void AppModeEmpty::Reset() { +void EmptyAppMode::OnActivate() { + assert(g_base->InLogicThread()); + + Reset_(); +} + +void EmptyAppMode::Reset_() { + reset_count_++; + // When we are first created (for use as a placeholder before any // app-modes are set) we just draw nothing. However once we actually get // reset for use as a an explicit app mode, we do our hello thing. - hello_mode_ = true; + hello_mode_ = (reset_count_ > 1); - // Fade in if we currently aren't. - g_base->graphics->FadeScreen(true, 250, nullptr); + // Reset the engine to a default state. + g_base->Reset(); + + // When we're a 'real' app-mode, fade in if we currently aren't. Otherwise + // let's stay faded out and let the first actual app-mode do the fading + // in. + if (hello_mode_) { + g_base->graphics->FadeScreen(true, 250, nullptr); + } } -void AppModeEmpty::DrawWorld(base::FrameDef* frame_def) { +void EmptyAppMode::DrawWorld(base::FrameDef* frame_def) { if (!hello_mode_) { return; } diff --git a/src/ballistica/base/app_mode/app_mode_empty.h b/src/ballistica/base/app_mode/empty_app_mode.h similarity index 62% rename from src/ballistica/base/app_mode/app_mode_empty.h rename to src/ballistica/base/app_mode/empty_app_mode.h index 17c97b53..256763d9 100644 --- a/src/ballistica/base/app_mode/app_mode_empty.h +++ b/src/ballistica/base/app_mode/empty_app_mode.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_BASE_APP_MODE_APP_MODE_EMPTY_H_ -#define BALLISTICA_BASE_APP_MODE_APP_MODE_EMPTY_H_ +#ifndef BALLISTICA_BASE_APP_MODE_EMPTY_APP_MODE_H_ +#define BALLISTICA_BASE_APP_MODE_EMPTY_APP_MODE_H_ #include @@ -13,19 +13,21 @@ namespace ballistica::base { /// An app-mode that doesn't do much of anything in particular. It is set as /// a default when starting the app, but can also be used for 'hello world' /// type stuff. -class AppModeEmpty : public AppMode { +class EmptyAppMode : public AppMode { public: - AppModeEmpty(); + EmptyAppMode(); - static auto GetSingleton() -> AppModeEmpty*; - void Reset(); + static auto GetSingleton() -> EmptyAppMode*; + void OnActivate() override; void DrawWorld(base::FrameDef* frame_def) override; private: + void Reset_(); Object::Ref hello_text_group_; + int reset_count_{}; bool hello_mode_{}; }; } // namespace ballistica::base -#endif // BALLISTICA_BASE_APP_MODE_APP_MODE_EMPTY_H_ +#endif // BALLISTICA_BASE_APP_MODE_EMPTY_APP_MODE_H_ diff --git a/src/ballistica/base/assets/assets.cc b/src/ballistica/base/assets/assets.cc index 2673d025..2f135ecd 100644 --- a/src/ballistica/base/assets/assets.cc +++ b/src/ballistica/base/assets/assets.cc @@ -1352,7 +1352,9 @@ void Assets::SetLanguageKeys( language_state_++; // Let some subsystems know that language has changed. - g_base->app_mode()->LanguageChanged(); + if (auto* app_mode = g_base->app_mode()) { + app_mode->LanguageChanged(); + } g_base->ui->LanguageChanged(); g_base->graphics->LanguageChanged(); } diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc index c8919dbd..6266558e 100644 --- a/src/ballistica/base/base.cc +++ b/src/ballistica/base/base.cc @@ -3,7 +3,7 @@ #include "ballistica/base/base.h" #include "ballistica/base/app_adapter/app_adapter.h" -#include "ballistica/base/app_mode/app_mode_empty.h" +#include "ballistica/base/app_mode/empty_app_mode.h" #include "ballistica/base/assets/assets_server.h" #include "ballistica/base/audio/audio.h" #include "ballistica/base/audio/audio_server.h" @@ -40,7 +40,7 @@ BaseFeatureSet* g_base{}; BaseFeatureSet::BaseFeatureSet() : app_adapter{BaseBuildSwitches::CreateAppAdapter()}, app_config{new AppConfig()}, - app_mode_{AppModeEmpty::GetSingleton()}, + app_mode_{EmptyAppMode::GetSingleton()}, assets{new Assets()}, assets_server{new AssetsServer()}, audio{new Audio()}, @@ -440,7 +440,7 @@ void BaseFeatureSet::set_app_mode(AppMode* mode) { // Redundant sets should not happen (make an exception here for empty mode // since that's in place before any app mode is officially set). - if (mode == app_mode_ && mode != AppModeEmpty::GetSingleton()) { + if (mode == app_mode_ && mode != EmptyAppMode::GetSingleton()) { Log(LogLevel::kWarning, "set_app_mode called with already-current app-mode; unexpected."); } @@ -979,4 +979,12 @@ void BaseFeatureSet::SetAppActive(bool active) { [] { g_base->logic->OnAppActiveChanged(); }); } +void BaseFeatureSet::Reset() { + ui->Reset(); + input->Reset(); + graphics->Reset(); + python->Reset(); + audio->Reset(); +} + } // namespace ballistica::base diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h index 93acf16d..8c0c76c4 100644 --- a/src/ballistica/base/base.h +++ b/src/ballistica/base/base.h @@ -713,6 +713,11 @@ class BaseFeatureSet : public FeatureSetNativeComponent, /// High level screen-message call usable from any thread. void ScreenMessage(const std::string& s, const Vector3f& color) override; + /// Has the app bootstrapping phase completed? The bootstrapping phase + /// involves initial screen/graphics setup. Asset loading is not allowed + /// until it is complete. + auto IsAppBootstrapped() const -> bool override; + /// Has StartApp been called (and completely finished its work)? Code that /// sends calls/messages to other threads or otherwise uses app /// functionality may want to check this to avoid crashes. Note that some @@ -720,11 +725,6 @@ class BaseFeatureSet : public FeatureSetNativeComponent, /// IsAppBootstrapped returns true. This call is thread safe. auto IsAppStarted() const -> bool override; - /// Has the app bootstrapping phase completed? The bootstrapping phase - /// involves initial screen/graphics setup. Asset loading is not allowed - /// until it is complete. - auto IsAppBootstrapped() const -> bool override; - void PlusDirectSendV1CloudLogs(const std::string& prefix, const std::string& suffix, bool instant, int* result) override; @@ -773,14 +773,14 @@ class BaseFeatureSet : public FeatureSetNativeComponent, /// Return whether there is currently text on the clipboard. auto ClipboardHasText() -> bool; - /// Set current clipboard text. Raises an Exception if clipboard is - /// unsupported. - void ClipboardSetText(const std::string& text); - /// Return current text from the clipboard. Raises an Exception if /// clipboard is unsupported or if there's no text on the clipboard. auto ClipboardGetText() -> std::string; + /// Set current clipboard text. Raises an Exception if clipboard is + /// unsupported. + void ClipboardSetText(const std::string& text); + // Const subsystems. AppAdapter* const app_adapter; AppConfig* const app_config; @@ -819,6 +819,8 @@ class BaseFeatureSet : public FeatureSetNativeComponent, auto app_active() -> bool const { return app_active_; } + void Reset(); + private: BaseFeatureSet(); void LogVersionInfo_(); diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc index 41adaba9..772a0497 100644 --- a/src/ballistica/base/graphics/graphics.cc +++ b/src/ballistica/base/graphics/graphics.cc @@ -19,6 +19,7 @@ #include "ballistica/base/python/support/python_context_call.h" #include "ballistica/base/support/app_config.h" #include "ballistica/base/ui/ui.h" +#include "ballistica/shared/ballistica.h" #include "ballistica/shared/foundation/event_loop.h" namespace ballistica::base { @@ -728,6 +729,9 @@ void Graphics::DrawUI(FrameDef* frame_def) { // Just do generic thing in our default implementation. // Special variants like GraphicsVR may do fancier stuff here. g_base->ui->Draw(frame_def); + + // We may want to see the bounds of our virtual screen. + DrawUIBounds(frame_def->overlay_pass()); } void Graphics::DrawDevUI(FrameDef* frame_def) { @@ -1500,14 +1504,25 @@ void Graphics::DrawRadialMeter(MeshIndexedSimpleFull* m, float amt) { void Graphics::OnScreenSizeChange() {} void Graphics::CalcVirtualRes_(float* x, float* y) { + assert(g_base); + float base_virtual_res_x; + float base_virtual_res_y; + if (g_base->ui->scale() == UIScale::kSmall) { + base_virtual_res_x = kBaseVirtualResSmallX; + base_virtual_res_y = kBaseVirtualResSmallY; + } else { + base_virtual_res_x = kBaseVirtualResX; + base_virtual_res_y = kBaseVirtualResY; + } + float x_in = *x; float y_in = *y; - if (*x / *y > static_cast(kBaseVirtualResX) - / static_cast(kBaseVirtualResY)) { - *y = kBaseVirtualResY; + if (*x / *y > static_cast(base_virtual_res_x) + / static_cast(base_virtual_res_y)) { + *y = base_virtual_res_y; *x = *y * (x_in / y_in); } else { - *x = kBaseVirtualResX; + *x = base_virtual_res_x; *y = *x * (y_in / x_in); } } @@ -1701,4 +1716,35 @@ void Graphics::UpdatePlaceholderSettings() { settings()->texture_quality, client_context()->auto_texture_quality); } +void Graphics::DrawUIBounds(RenderPass* pass) { + // We can optionally draw a guide to show the edges of the overlay pass + if (draw_ui_bounds_) { + SimpleComponent c(pass); + c.SetColor(1, 0, 0); + { + auto xf = c.ScopedTransform(); + // float width = pass->virtual_width(); + // float height = pass->virtual_height(); + + // printf("DIMS %.2f %.2f\n", width, height); + + float width, height; + if (g_base->ui->scale() == UIScale::kSmall) { + width = kBaseVirtualResSmallX; + height = kBaseVirtualResSmallY; + } else { + width = kBaseVirtualResX; + height = kBaseVirtualResY; + } + + // Slight offset in z to reduce z fighting. + c.Translate(0.5f * pass->virtual_width(), 0.5f * pass->virtual_height(), + 0.0f); + c.Scale(width, height, 0.01f); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kOverlayGuide)); + } + c.Submit(); + } +} + } // namespace ballistica::base diff --git a/src/ballistica/base/graphics/graphics.h b/src/ballistica/base/graphics/graphics.h index 687f07ea..5fc0c02c 100644 --- a/src/ballistica/base/graphics/graphics.h +++ b/src/ballistica/base/graphics/graphics.h @@ -150,6 +150,8 @@ class Graphics { r, g, b, a); } + void DrawUIBounds(RenderPass* pass); + // Enable progress bar drawing locally. void EnableProgressBar(bool fade_in); @@ -356,6 +358,8 @@ class Graphics { assert(client_context_snapshot_.Exists()); return client_context_snapshot_.Get()->Get(); } + auto draw_ui_bounds() const { return draw_ui_bounds_; } + void set_draw_ui_bounds(bool val) { draw_ui_bounds_ = val; } ScreenMessages* const screenmessages; @@ -415,6 +419,7 @@ class Graphics { bool applied_app_config_{}; bool sent_initial_graphics_settings_{}; bool got_screen_resolution_{}; + bool draw_ui_bounds_{}; Vector3f shadow_offset_{0.0f, 0.0f, 0.0f}; Vector2f shadow_scale_{1.0f, 1.0f}; Vector3f tint_{1.0f, 1.0f, 1.0f}; diff --git a/src/ballistica/base/graphics/graphics_vr.cc b/src/ballistica/base/graphics/graphics_vr.cc index 3325555b..ab60fcdd 100644 --- a/src/ballistica/base/graphics/graphics_vr.cc +++ b/src/ballistica/base/graphics/graphics_vr.cc @@ -87,8 +87,6 @@ auto GraphicsVR::ValueTest(const std::string& arg, double* absval, *outval = ValueTestFloat(&vr_overlay_scale_, absval, deltaval); } else if (arg == "lockVROverlay") { *outval = ValueTestBool(&lock_vr_overlay_, absval, deltaval); - } else if (arg == "showOverlayBounds") { - *outval = ValueTestBool(&draw_overlay_bounds_, absval, deltaval); } else if (arg == "headScale") { *outval = ValueTestFloat(&vr_test_head_scale_, absval, deltaval); } else if (arg == "vrCamOffsetY") { @@ -143,9 +141,6 @@ void GraphicsVR::DrawUI(FrameDef* frame_def) { // In VR mode we have to draw our overlay-flat texture into space as // part of the regular overlay pass. DrawVROverlay(frame_def); - - // We may want to see the bounds of our overlay. - DrawOverlayBounds(frame_def->overlay_pass()); } void GraphicsVR::CalcVROverlayMatrices(FrameDef* frame_def) { @@ -293,25 +288,6 @@ void GraphicsVR::DrawVROverlay(FrameDef* frame_def) { } } -void GraphicsVR::DrawOverlayBounds(RenderPass* pass) { - // We can optionally draw a guide to show the edges of the overlay pass - if (draw_overlay_bounds_) { - SimpleComponent c(pass); - c.SetColor(1, 0, 0); - { - auto xf = c.ScopedTransform(); - float width = screen_virtual_width(); - float height = screen_virtual_height(); - - // Slight offset in z to reduce z fighting. - c.Translate(0.5f * width, 0.5f * height, 1.0f); - c.Scale(width, height, 100.0f); - c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kOverlayGuide)); - } - c.Submit(); - } -} - void GraphicsVR::DrawVRControllers(FrameDef* frame_def) { if (!g_core->vr_mode()) { return; diff --git a/src/ballistica/base/graphics/graphics_vr.h b/src/ballistica/base/graphics/graphics_vr.h index a365c743..a1d176dd 100644 --- a/src/ballistica/base/graphics/graphics_vr.h +++ b/src/ballistica/base/graphics/graphics_vr.h @@ -71,7 +71,6 @@ class GraphicsVR : public Graphics { auto CalcVROverlayMatrix(const Vector3f& cam_pt, const Vector3f& cam_target_pt) const -> Matrix44f; void DrawVROverlay(FrameDef* frame_def); - void DrawOverlayBounds(RenderPass* pass); void DrawVRControllers(FrameDef* frame_def); float vr_overlay_scale_{1.0f}; @@ -84,7 +83,6 @@ class GraphicsVR : public Graphics { Vector3f vr_overlay_center_{0.0f, 0.0f, 0.0f}; bool vr_overlay_center_enabled_{}; bool lock_vr_overlay_{}; - bool draw_overlay_bounds_{}; float vr_test_head_scale_{kDefaultVRHeadScale}; VRHandsState vr_hands_state_; }; diff --git a/src/ballistica/base/python/base_python.cc b/src/ballistica/base/python/base_python.cc index 7d78f684..251544eb 100644 --- a/src/ballistica/base/python/base_python.cc +++ b/src/ballistica/base/python/base_python.cc @@ -463,18 +463,10 @@ auto BasePython::GetPyEnum_SpecialChar(PyObject* obj) -> SpecialChar { return GetPyEnum(BasePython::ObjID::kSpecialCharClass, obj); } -// auto BasePython::GetPyEnum_TimeType(PyObject* obj) -> TimeType { -// return GetPyEnum(BasePython::ObjID::kTimeTypeClass, obj); -// } - auto BasePython::GetPyEnum_QuitType(PyObject* obj) -> QuitType { return GetPyEnum(BasePython::ObjID::kQuitTypeClass, obj); } -// auto BasePython::GetPyEnum_TimeFormat(PyObject* obj) -> TimeFormat { -// return GetPyEnum(BasePython::ObjID::kTimeFormatClass, obj); -// } - auto BasePython::IsPyEnum_InputType(PyObject* obj) -> bool { return IsPyEnum(BasePython::ObjID::kInputTypeClass, obj); } diff --git a/src/ballistica/base/python/base_python.h b/src/ballistica/base/python/base_python.h index 61237013..ed3dc3ed 100644 --- a/src/ballistica/base/python/base_python.h +++ b/src/ballistica/base/python/base_python.h @@ -154,8 +154,6 @@ class BasePython { static auto GetPyEnum_Permission(PyObject* obj) -> Permission; static auto GetPyEnum_SpecialChar(PyObject* obj) -> SpecialChar; - // static auto GetPyEnum_TimeType(PyObject* obj) -> TimeType; - // static auto GetPyEnum_TimeFormat(PyObject* obj) -> TimeFormat; static auto IsPyEnum_InputType(PyObject* obj) -> bool; static auto GetPyEnum_InputType(PyObject* obj) -> InputType; static auto GetPyEnum_QuitType(PyObject* obj) -> QuitType; 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 97d285bb..4ca96254 100644 --- a/src/ballistica/base/python/methods/python_methods_base_1.cc +++ b/src/ballistica/base/python/methods/python_methods_base_1.cc @@ -3,7 +3,7 @@ #include "ballistica/base/python/methods/python_methods_base_1.h" #include "ballistica/base/app_adapter/app_adapter.h" -#include "ballistica/base/app_mode/app_mode_empty.h" +#include "ballistica/base/app_mode/empty_app_mode.h" #include "ballistica/base/audio/audio_server.h" #include "ballistica/base/graphics/graphics_server.h" #include "ballistica/base/logic/logic.h" @@ -1321,29 +1321,27 @@ static PyMethodDef PyUserAgentStringDef = { "(internal)\n", }; -// --------------------- on_empty_app_mode_activate ---------------------------- +// --------------------- empty_app_mode_activate ---------------------------- static auto PyOnEmptyAppModeActivate(PyObject* self) -> PyObject* { BA_PYTHON_TRY; BA_PRECONDITION(g_base->InLogicThread()); - g_base->set_app_mode(AppModeEmpty::GetSingleton()); - AppModeEmpty::GetSingleton()->Reset(); - + g_base->set_app_mode(EmptyAppMode::GetSingleton()); Py_RETURN_NONE; BA_PYTHON_CATCH; } static PyMethodDef PyOnEmptyAppModeActivateDef = { - "on_empty_app_mode_activate", // name + "empty_app_mode_activate", // name (PyCFunction)PyOnEmptyAppModeActivate, // method METH_NOARGS, // flags - "on_empty_app_mode_activate() -> None\n" + "empty_app_mode_activate() -> None\n" "\n" "(internal)\n", }; -// --------------------- on_empty_app_mode_deactivate -------------------------- +// --------------------- empty_app_mode_deactivate -------------------------- static auto PyOnEmptyAppModeDeactivate(PyObject* self) -> PyObject* { BA_PYTHON_TRY; @@ -1354,38 +1352,38 @@ static auto PyOnEmptyAppModeDeactivate(PyObject* self) -> PyObject* { } static PyMethodDef PyOnEmptyAppModeDeactivateDef = { - "on_empty_app_mode_deactivate", // name + "empty_app_mode_deactivate", // name (PyCFunction)PyOnEmptyAppModeDeactivate, // method METH_NOARGS, // flags - "on_empty_app_mode_deactivate() -> None\n" + "empty_app_mode_deactivate() -> None\n" "\n" "(internal)\n", }; -// ----------------- empty_app_mode_handle_intent_default ---------------------- +// --------------- empty_app_mode_handle_app_intent_default -------------------- -static auto PyEmptyAppModeHandleIntentDefault(PyObject* self) -> PyObject* { +static auto PyEmptyAppModeHandleAppIntentDefault(PyObject* self) -> PyObject* { BA_PYTHON_TRY; BA_PRECONDITION(g_base->InLogicThread()); Py_RETURN_NONE; BA_PYTHON_CATCH; } -static PyMethodDef PyEmptyAppModeHandleIntentDefaultDef = { - "empty_app_mode_handle_intent_default", // name - (PyCFunction)PyEmptyAppModeHandleIntentDefault, // method - METH_NOARGS, // flags +static PyMethodDef PyEmptyAppModeHandleAppIntentDefaultDef = { + "empty_app_mode_handle_app_intent_default", // name + (PyCFunction)PyEmptyAppModeHandleAppIntentDefault, // method + METH_NOARGS, // flags - "empty_app_mode_handle_intent_default() -> None\n" + "empty_app_mode_handle_app_intent_default() -> None\n" "\n" "(internal)\n", }; -// ------------------ empty_app_mode_handle_intent_exec ------------------------ +// ---------------- empty_app_mode_handle_app_intent_exec ---------------------- -static auto PyEmptyAppModeHandleIntentExec(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { +static auto PyEmptyAppModeHandleAppIntentExec(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; const char* command; static const char* kwlist[] = {"command", nullptr}; @@ -1408,12 +1406,12 @@ static auto PyEmptyAppModeHandleIntentExec(PyObject* self, PyObject* args, BA_PYTHON_CATCH; } -static PyMethodDef PyEmptyAppModeHandleIntentExecDef = { - "empty_app_mode_handle_intent_exec", // name - (PyCFunction)PyEmptyAppModeHandleIntentExec, // method - METH_VARARGS | METH_KEYWORDS, // flags +static PyMethodDef PyEmptyAppModeHandleAppIntentExecDef = { + "empty_app_mode_handle_app_intent_exec", // name + (PyCFunction)PyEmptyAppModeHandleAppIntentExec, // method + METH_VARARGS | METH_KEYWORDS, // flags - "empty_app_mode_handle_intent_exec(command: str) -> None\n" + "empty_app_mode_handle_app_intent_exec(command: str) -> None\n" "\n" "(internal)", }; @@ -1739,8 +1737,8 @@ auto PythonMethodsBase1::GetMethods() -> std::vector { PyUserAgentStringDef, PyOnEmptyAppModeActivateDef, PyOnEmptyAppModeDeactivateDef, - PyEmptyAppModeHandleIntentDefaultDef, - PyEmptyAppModeHandleIntentExecDef, + PyEmptyAppModeHandleAppIntentDefaultDef, + PyEmptyAppModeHandleAppIntentExecDef, PyGetImmediateReturnCodeDef, PyCompleteShutdownDef, PyShutdownSuppressBeginDef, 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 f0606522..64ea4617 100644 --- a/src/ballistica/base/python/methods/python_methods_base_2.cc +++ b/src/ballistica/base/python/methods/python_methods_base_2.cc @@ -182,32 +182,6 @@ static auto PyScreenMessage(PyObject* self, PyObject* args, return nullptr; } - // TEMP - we used to have a single ba.screenmessage() call that would - // broadcast messages to all clients when called by a server in a game context - // and simply print them locally in other cases. In 1.7.20 the broadcast form - // has been moved to bascenev1.broadcastmessage(). But there's probably lots - // of code out there using screenmessage() not realizing it won't do what they - // intended anymore. So for now let's issue a warning when it *would* have - // done the broadcast thing. (just assuming that's the case any time there's a - // non-empty context) - static bool did_warning{}; - if (!did_warning && !g_base->CurrentContext().IsEmpty()) { - did_warning = true; - auto* envval = getenv("BA_SUPPRESS_SCREEN_MESSAGE_WARNING"); - bool suppress = (envval && strcmp(envval, "1") == 0); - if (!suppress) { - Log(LogLevel::kWarning, - "WARNING! screenmessage() is being called in a gameplay situation.\n" - "Previously this would send a message to all connected clients," - " but as of 1.7.20 it only shows a message on the local device.\n" - "To get the old behavior, change your code to use" - " bascenev1.broadcastmessage() instead.\n" - "You can set env var BA_SUPPRESS_SCREEN_MESSAGE_WARNING=1 to" - " suppress this warning."); - g_base->PrintPythonStackTrace(); - } - } - std::string message_str = g_base->python->GetPyLString(message_obj); Vector3f color{1, 1, 1}; if (color_obj != Py_None) { @@ -862,6 +836,29 @@ static PyMethodDef PyFullscreenControlSetDef = { "(internal)\n", }; +// -------------------------- allows_ticket_sales ------------------------------ + +static auto PyAllowsTicketSales(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + + BA_PRECONDITION(g_base->InLogicThread()); + + Py_RETURN_TRUE; + + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAllowsTicketSalesDef = { + "allows_ticket_sales", // name + (PyCFunction)PyAllowsTicketSales, // method + METH_NOARGS, // flags + + "allows_ticket_sales() -> bool\n" + "\n" + "(internal)\n", +}; + // ----------------------------- supports_vsync -------------------------------- static auto PySupportsVSync(PyObject* self) -> PyObject* { @@ -955,6 +952,7 @@ auto PythonMethodsBase2::GetMethods() -> std::vector { PySafeColorDef, PyCharStrDef, PyFullscreenControlAvailableDef, + PyAllowsTicketSalesDef, PySupportsVSyncDef, PySupportsMaxFPSDef, PyShowProgressBarDef, diff --git a/src/ballistica/base/python/methods/python_methods_base_3.cc b/src/ballistica/base/python/methods/python_methods_base_3.cc index 6a6aa4d1..831031dd 100644 --- a/src/ballistica/base/python/methods/python_methods_base_3.cc +++ b/src/ballistica/base/python/methods/python_methods_base_3.cc @@ -14,6 +14,7 @@ #include "ballistica/base/support/app_config.h" #include "ballistica/base/ui/dev_console.h" #include "ballistica/base/ui/ui.h" +#include "ballistica/shared/foundation/macros.h" #include "ballistica/shared/generic/native_stack_trace.h" #include "ballistica/shared/generic/utils.h" @@ -1826,6 +1827,85 @@ static PyMethodDef PyGetInputIdleTimeDef = { "\n" "Return seconds since any local input occurred (touch, keypress, etc.).", }; + +// --------------------------- get_draw_ui_bounds ----------------------------- + +static auto PyGetDrawUIBounds(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + + BA_PRECONDITION(g_base->InLogicThread()); + if (g_base->graphics->draw_ui_bounds()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetDrawUIBoundsDef = { + "get_draw_ui_bounds", // name + (PyCFunction)PyGetDrawUIBounds, // method + METH_NOARGS, // flags + + "get_draw_ui_bounds() -> bool\n" + "\n" + "(internal)", +}; + +// --------------------------- set_draw_ui_bounds ----------------------------- + +static auto PySetDrawUIBounds(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + + BA_PRECONDITION(g_base->InLogicThread()); + + int value; + static const char* kwlist[] = {"value", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &value)) { + return nullptr; + } + + // if (g_base->graphics->draw_ui_bounds()) { + // Py_RETURN_TRUE; + // } + // Py_RETURN_FALSE; + g_base->graphics->set_draw_ui_bounds(value); + Py_RETURN_NONE; + + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetDrawUIBoundsDef = { + "set_draw_ui_bounds", // name + (PyCFunction)PySetDrawUIBounds, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_draw_ui_bounds(value: bool) -> None\n" + "\n" + "(internal)", +}; + +// ----------------------------- push_back_press ------------------------------- + +static auto PyPushBackPress(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + g_base->ui->PushBackButtonCall(nullptr); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyPushBackPressDef = { + "push_back_press", // name + (PyCFunction)PyPushBackPress, // method + METH_NOARGS, // flags + + "push_back_press() -> None\n" + "\n" + "(internal)", +}; + // ----------------------------------------------------------------------------- auto PythonMoethodsBase3::GetMethods() -> std::vector { @@ -1895,6 +1975,9 @@ auto PythonMoethodsBase3::GetMethods() -> std::vector { PyTempTestingDef, PyOpenFileExternallyDef, PyGetInputIdleTimeDef, + PyPushBackPressDef, + PyGetDrawUIBoundsDef, + PySetDrawUIBoundsDef, }; } diff --git a/src/ballistica/base/ui/dev_console.cc b/src/ballistica/base/ui/dev_console.cc index 739c93fc..00c938c4 100644 --- a/src/ballistica/base/ui/dev_console.cc +++ b/src/ballistica/base/ui/dev_console.cc @@ -85,7 +85,7 @@ static auto XOffs(DevConsoleHAnchor_ attach) -> float { } static auto IsValidHungryChar_(uint32_t this_char) -> bool { - // Include letters, numbers, and underscore. + // Include letters, numbers, and underscore. return ((this_char >= 65 && this_char <= 90) || (this_char >= 97 && this_char <= 122) || (this_char >= 48 && this_char <= 57) || this_char == '_'); @@ -1204,15 +1204,21 @@ void DevConsole::Draw(FrameDef* frame_def) { border_height * bs); { SimpleComponent c(pass); + + // Backing. c.SetTransparent(true); - c.SetColor(0, 0, 0.1f, 0.9f); + c.SetColor(0.04f, 0, 0.15f, 0.86f); c.DrawMesh(&bg_mesh_); c.Submit(); + + // Stripe. if (python_terminal_visible_) { c.SetColor(1.0f, 1.0f, 1.0f, 0.1f); c.DrawMesh(&stripe_mesh_); c.Submit(); } + + // Border. c.SetColor(0.25f, 0.2f, 0.3f, 1.0f); c.DrawMesh(&border_mesh_); } @@ -1336,7 +1342,7 @@ void DevConsole::Draw(FrameDef* frame_def) { c.SetTransparent(true); c.SetColor(1, 1, 1, 1); c.SetFlatness(1.0f); - float draw_scale = 0.6f; + float draw_scale = 0.64f; float v_inc = 18.0f; float h = 0.5f * (g_base->graphics->screen_virtual_width() diff --git a/src/ballistica/base/ui/ui.cc b/src/ballistica/base/ui/ui.cc index d57d3118..af6ee26c 100644 --- a/src/ballistica/base/ui/ui.cc +++ b/src/ballistica/base/ui/ui.cc @@ -235,11 +235,11 @@ auto UI::HandleMouseDown(int button, float x, float y, handled = dev_console_->HandleMouseDown(button, x, y); } - if (!handled) { - if (auto* ui_delegate = g_base->ui->delegate()) { - handled = ui_delegate->HandleLegacyRootUIMouseDown(x, y); - } - } + // if (!handled) { + // if (auto* ui_delegate = g_base->ui->delegate()) { + // handled = ui_delegate->HandleLegacyRootUIMouseDown(x, y); + // } + // } if (!handled) { handled = SendWidgetMessage(WidgetMessage( @@ -268,9 +268,9 @@ void UI::HandleMouseUp(int button, float x, float y) { } } - if (auto* ui_delegate = g_base->ui->delegate()) { - ui_delegate->HandleLegacyRootUIMouseUp(x, y); - } + // if (auto* ui_delegate = g_base->ui->delegate()) { + // ui_delegate->HandleLegacyRootUIMouseUp(x, y); + // } } auto UI::UIHasDirectKeyboardInput() const -> bool { @@ -296,9 +296,9 @@ void UI::HandleMouseMotion(float x, float y) { SendWidgetMessage( WidgetMessage(WidgetMessage::Type::kMouseMove, nullptr, x, y)); - if (auto* ui_delegate = g_base->ui->delegate()) { - ui_delegate->HandleLegacyRootUIMouseMotion(x, y); - } + // if (auto* ui_delegate = g_base->ui->delegate()) { + // ui_delegate->HandleLegacyRootUIMouseMotion(x, y); + // } } void UI::PushBackButtonCall(InputDevice* input_device) { @@ -338,8 +338,11 @@ void UI::SetUIInputDevice(InputDevice* input_device) { } void UI::Reset() { + assert(g_base->InLogicThread()); + // Reset and then deactivate any current delegate. if (auto* ui_delegate = g_base->ui->delegate()) { ui_delegate->Reset(); + g_base->ui->set_ui_delegate(nullptr); } } @@ -645,19 +648,25 @@ void UI::PushUIOperationRunnable(Runnable* runnable) { assert(g_base->InLogicThread()); if (operation_context_ != nullptr) { + // Once we're finishing the context, nothing else should be adding calls + // to it. UPDATE - this is actually ok. Things like widget-select + // commands can happen as part of user callbacks which themselves add + // additional callbacks to the current ui-operation. + // + // if (operation_context_->ran_finish()) { + // auto trace = g_core->platform->GetNativeStackTrace(); + // BA_LOG_ERROR_NATIVE_TRACE( + // "UI::PushUIOperationRunnable() called during UI operation + // finish."); + // return; + // } + operation_context_->AddRunnable(runnable); return; + } else { + BA_LOG_ERROR_NATIVE_TRACE( + "UI::PushUIOperationRunnable() called outside of UI operation."); } - - // For now, gracefully fall back to pushing an event if there's no current - // operation. Once we've got any bugs cleared out, can leave this as just - // an error log. - - auto trace = g_core->platform->GetNativeStackTrace(); - BA_LOG_ERROR_NATIVE_TRACE_ONCE( - "UI::PushUIOperationRunnable() called outside of UI operation."); - - g_base->logic->event_loop()->PushRunnable(runnable); } auto UI::InUIOperation() -> bool { diff --git a/src/ballistica/base/ui/ui.h b/src/ballistica/base/ui/ui.h index fa903322..bda94f57 100644 --- a/src/ballistica/base/ui/ui.h +++ b/src/ballistica/base/ui/ui.h @@ -135,6 +135,7 @@ class UI { /// call. void Finish(); void AddRunnable(Runnable* runnable); + auto ran_finish() const { return ran_finish_; } private: bool ran_finish_{}; diff --git a/src/ballistica/base/ui/ui_delegate.h b/src/ballistica/base/ui/ui_delegate.h index 5bcebc81..a668f78f 100644 --- a/src/ballistica/base/ui/ui_delegate.h +++ b/src/ballistica/base/ui/ui_delegate.h @@ -34,9 +34,9 @@ class UIDelegateInterface { virtual auto MainMenuVisible() -> bool = 0; virtual auto PartyIconVisible() -> bool = 0; virtual void ActivatePartyIcon() = 0; - virtual void HandleLegacyRootUIMouseMotion(float x, float y) = 0; - virtual auto HandleLegacyRootUIMouseDown(float x, float y) -> bool = 0; - virtual void HandleLegacyRootUIMouseUp(float x, float y) = 0; + // virtual void HandleLegacyRootUIMouseMotion(float x, float y) = 0; + // virtual auto HandleLegacyRootUIMouseDown(float x, float y) -> bool = 0; + // virtual void HandleLegacyRootUIMouseUp(float x, float y) = 0; virtual void Draw(FrameDef* frame_def) = 0; virtual auto PartyWindowOpen() -> bool = 0; virtual void Reset() = 0; diff --git a/src/ballistica/classic/classic.h b/src/ballistica/classic/classic.h index efba8d9d..0d01d7a5 100644 --- a/src/ballistica/classic/classic.h +++ b/src/ballistica/classic/classic.h @@ -26,6 +26,7 @@ class SceneV1FeatureSet; namespace ballistica::classic { // Predeclared types our feature-set provides. +class ClassicAppMode; class ClassicFeatureSet; class ClassicPython; class StressTest; diff --git a/src/ballistica/classic/python/classic_python.cc b/src/ballistica/classic/python/classic_python.cc index 70769f9c..41d52348 100644 --- a/src/ballistica/classic/python/classic_python.cc +++ b/src/ballistica/classic/python/classic_python.cc @@ -3,7 +3,7 @@ #include "ballistica/classic/python/classic_python.h" #include "ballistica/classic/python/methods/python_methods_classic.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/shared/python/python_command.h" #include "ballistica/shared/python/python_module_builder.h" @@ -91,7 +91,7 @@ auto ClassicPython::GetControllerFloatValue( } auto ClassicPython::BuildPublicPartyStateVal() -> PyObject* { - auto* appmode = scene_v1::SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = ClassicAppMode::GetActiveOrThrow(); auto&& public_ipv4 = appmode->public_party_public_address_ipv4(); PyObject* ipv4obj; diff --git a/src/ballistica/classic/python/methods/python_methods_classic.cc b/src/ballistica/classic/python/methods/python_methods_classic.cc index 482d2565..eab05e73 100644 --- a/src/ballistica/classic/python/methods/python_methods_classic.cc +++ b/src/ballistica/classic/python/methods/python_methods_classic.cc @@ -6,10 +6,11 @@ #include "ballistica/base/graphics/support/camera.h" #include "ballistica/base/input/input.h" #include "ballistica/base/logic/logic.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/classic/support/stress_test.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/shared/foundation/event_loop.h" #include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_command.h" #include "ballistica/shared/python/python_sys.h" namespace ballistica::classic { @@ -50,7 +51,7 @@ static auto PyValueTest(PyObject* self, PyObject* args, } double return_val = 0.0f; if (!strcmp(arg, "bufferTime")) { - auto* appmode = scene_v1::SceneV1AppMode::GetSingleton(); + auto* appmode = ClassicAppMode::GetSingleton(); if (have_change) { appmode->set_buffer_time(appmode->buffer_time() @@ -62,7 +63,7 @@ static auto PyValueTest(PyObject* self, PyObject* args, appmode->set_buffer_time(std::max(0, appmode->buffer_time())); return_val = appmode->buffer_time(); } else if (!strcmp(arg, "delaySampling")) { - auto* appmode = scene_v1::SceneV1AppMode::GetSingleton(); + auto* appmode = ClassicAppMode::GetSingleton(); if (have_change) { appmode->set_delay_bucket_samples(appmode->delay_bucket_samples() + static_cast(change)); @@ -74,7 +75,7 @@ static auto PyValueTest(PyObject* self, PyObject* args, std::max(1, appmode->delay_bucket_samples())); return_val = appmode->delay_bucket_samples(); } else if (!strcmp(arg, "dynamicsSyncTime")) { - auto* appmode = scene_v1::SceneV1AppMode::GetSingleton(); + auto* appmode = ClassicAppMode::GetSingleton(); if (have_change) { appmode->set_dynamics_sync_time(appmode->dynamics_sync_time() + static_cast(change)); @@ -179,12 +180,121 @@ static PyMethodDef PySetStressTestingDef = { "(internal)", }; +// --------------- classic_app_mode_handle_app_intent_exec --------------------- + +static auto PyClassicAppModeHandleAppIntentExec(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* command; + static const char* kwlist[] = {"command", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &command)) { + return nullptr; + } + auto* appmode = ClassicAppMode::GetActiveOrThrow(); + + // Run the command. + if (g_core->core_config().exec_command.has_value()) { + bool success = PythonCommand(*g_core->core_config().exec_command, + BA_BUILD_COMMAND_FILENAME) + .Exec(true, nullptr, nullptr); + if (!success) { + // TODO(ericf): what should we do in this case? + // Obviously if we add return/success values for intents we should set + // that here. + } + } + // If the stuff we just ran didn't result in a session, create a default + // one. + if (!appmode->GetForegroundSession()) { + appmode->RunMainMenu(); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyClassicAppModeHandleAppIntentExecDef = { + "classic_app_mode_handle_app_intent_exec", // name + (PyCFunction)PyClassicAppModeHandleAppIntentExec, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "classic_app_mode_handle_app_intent_exec(command: str) -> None\n" + "\n" + "(internal)", +}; + +// -------------- classic_app_mode_handle_app_intent_default ------------------ + +static auto PyClassicAppModeHandleAppIntentDefault(PyObject* self) + -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + auto* appmode = ClassicAppMode::GetActiveOrThrow(); + appmode->RunMainMenu(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyClassicAppModeHandleAppIntentDefaultDef = { + "classic_app_mode_handle_app_intent_default", // name + (PyCFunction)PyClassicAppModeHandleAppIntentDefault, // method + METH_NOARGS, // flags + + "classic_app_mode_handle_app_intent_default() -> None\n" + "\n" + "(internal)\n", +}; + +// ------------------------ classic_app_mode_activate -------------------------- + +static auto PyClassicAppModeActivate(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + g_base->set_app_mode(ClassicAppMode::GetSingleton()); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyClassicAppModeActivateDef = { + "classic_app_mode_activate", // name + (PyCFunction)PyClassicAppModeActivate, // method + METH_NOARGS, // flags + + "classic_app_mode_activate() -> None\n" + "\n" + "(internal)\n", +}; + +// ---------------------- classic_app_mode_deactivate -------------------------- + +static auto PyClassicAppModeDeactivate(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + // Currently doing nothing. + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyClassicAppModeDeactivateDef = { + "classic_app_mode_deactivate", // name + (PyCFunction)PyClassicAppModeDeactivate, // method + METH_NOARGS, // flags + + "classic_app_mode_deactivate() -> None\n" + "\n" + "(internal)\n", +}; + // ----------------------------------------------------------------------------- auto PythonMethodsClassic::GetMethods() -> std::vector { return { PyValueTestDef, PySetStressTestingDef, + PyClassicAppModeHandleAppIntentExecDef, + PyClassicAppModeHandleAppIntentDefaultDef, + PyClassicAppModeActivateDef, + PyClassicAppModeDeactivateDef, }; } diff --git a/src/ballistica/scene_v1/support/scene_v1_app_mode.cc b/src/ballistica/classic/support/classic_app_mode.cc similarity index 84% rename from src/ballistica/scene_v1/support/scene_v1_app_mode.cc rename to src/ballistica/classic/support/classic_app_mode.cc index c22cff02..364c03eb 100644 --- a/src/ballistica/scene_v1/support/scene_v1_app_mode.cc +++ b/src/ballistica/classic/support/classic_app_mode.cc @@ -1,6 +1,6 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/base/audio/audio.h" #include "ballistica/base/audio/audio_source.h" @@ -25,7 +25,7 @@ #include "ballistica/shared/generic/utils.h" #include "ballistica/ui_v1/ui_v1.h" -namespace ballistica::scene_v1 { +namespace ballistica::classic { const int kMaxChatMessages = 40; @@ -44,82 +44,106 @@ const int kKickVoteFailRetryDelayInitiatorExtra = 120000; // to kick). const int kKickVoteMinimumClients = (g_buildconfig.headless_build() ? 3 : 4); -struct SceneV1AppMode::ScanResultsEntryPriv_ { +struct ClassicAppMode::ScanResultsEntryPriv_ { scene_v1::PlayerSpec player_spec; std::string address; uint32_t last_query_id{}; millisecs_t last_contact_time{}; }; -base::InputDeviceDelegate* SceneV1AppMode::CreateInputDeviceDelegate( +base::InputDeviceDelegate* ClassicAppMode::CreateInputDeviceDelegate( base::InputDevice* device) { // We create a special delegate for our special ClientInputDevice types; // everything else gets our regular delegate. - if (auto* client_device = dynamic_cast(device)) { - auto* obj = Object::NewDeferred(); + if (auto* client_device = + dynamic_cast(device)) { + auto* obj = Object::NewDeferred(); obj->StoreClientDeviceInfo(client_device); return obj; } - return Object::NewDeferred(); + return Object::NewDeferred(); } // Go with 5 minute ban. const int kKickBanSeconds = 5 * 60; -bool SceneV1AppMode::InClassicMainMenuSession() const { - HostSession* hostsession = - ContextRefSceneV1::FromAppForegroundContext().GetHostSession(); +bool ClassicAppMode::InClassicMainMenuSession() const { + scene_v1::HostSession* hostsession = + scene_v1::ContextRefSceneV1::FromAppForegroundContext().GetHostSession(); return (hostsession && hostsession->is_main_menu()); } -static SceneV1AppMode* g_scene_v1_app_mode{}; +static ClassicAppMode* g_scene_v1_app_mode{}; -void SceneV1AppMode::OnActivate() { +void ClassicAppMode::OnActivate() { assert(g_base->InLogicThread()); // Make sure we pull this only once when we are first active. if (host_protocol_version_ == -1) { - host_protocol_version_ = - std::clamp(g_base->app_config->Resolve( - base::AppConfig::IntID::kSceneV1HostProtocol), - kProtocolVersionHostMin, kProtocolVersionMax); + host_protocol_version_ = std::clamp( + g_base->app_config->Resolve( + base::AppConfig::IntID::kSceneV1HostProtocol), + scene_v1::kProtocolVersionHostMin, scene_v1::kProtocolVersionMax); } Reset_(); - // We use UIV1. - if (!g_core->HeadlessMode()) { - g_base->ui->set_ui_delegate(ui_v1::UIV1FeatureSet::Import()); - } - // To set initial states, explicitly fire some of our 'On-Foo-Changed' // callbacks. DoApplyAppConfig(); LanguageChanged(); } -void SceneV1AppMode::OnAppStart() { assert(g_base->InLogicThread()); } +void ClassicAppMode::OnAppStart() { assert(g_base->InLogicThread()); } -void SceneV1AppMode::OnAppShutdown() { +void ClassicAppMode::OnAppShutdown() { assert(g_base->InLogicThread()); connections_->Shutdown(); } -void SceneV1AppMode::OnAppSuspend() { +void ClassicAppMode::OnAppSuspend() { assert(g_base->InLogicThread()); // App is going into background or whatnot. Kill any sockets/etc. EndHostScanning(); } -void SceneV1AppMode::OnAppUnsuspend() { assert(g_base->InLogicThread()); } +void ClassicAppMode::OnAppUnsuspend() { assert(g_base->InLogicThread()); } + +// Reset everything to a blank slate. +void ClassicAppMode::Reset_() { + assert(g_base); + assert(g_base->InLogicThread()); + + // Tear down any existing session. + foreground_session_.Clear(); + PruneSessions_(); + + // If all is well our sessions should all be dead at this point. + if (g_scene_v1->session_count != 0) { + Log(LogLevel::kError, "SceneV1 session count is non-zero (" + + std::to_string(g_scene_v1->session_count) + + ") on ClassicAppMode::Reset_()."); + } + + // Reset the engine itself to a default state. + g_base->Reset(); + + // Import UIV1 and wire it up for UI duty. + if (!g_core->HeadlessMode()) { + g_base->ui->set_ui_delegate(ui_v1::UIV1FeatureSet::Import()); + } + + // Fade in if we currently aren't. + g_base->graphics->FadeScreen(true, 250, nullptr); +} // Note: for now we're making our host-scan network calls directly from the // logic thread. This is generally not a good idea since it appears that even // in non-blocking mode they're still blocking for 3-4ms sometimes. But for // now since this is only used minimally and only while in the UI I guess it's // ok. -void SceneV1AppMode::HostScanCycle() { +void ClassicAppMode::HostScanCycle() { assert(g_base->InLogicThread()); // We need to create a scanner socket - an ipv4 socket we can send out @@ -291,14 +315,14 @@ void SceneV1AppMode::HostScanCycle() { } } -void SceneV1AppMode::EndHostScanning() { +void ClassicAppMode::EndHostScanning() { if (scan_socket_ != -1) { g_core->platform->CloseSocket(scan_socket_); scan_socket_ = -1; } } -void SceneV1AppMode::PruneScanResults_() { +void ClassicAppMode::PruneScanResults_() { millisecs_t t = g_core->GetAppTimeMillisecs(); auto i = scan_results_.begin(); while (i != scan_results_.end()) { @@ -311,8 +335,8 @@ void SceneV1AppMode::PruneScanResults_() { } } -auto SceneV1AppMode::GetScanResults() - -> std::vector { +auto ClassicAppMode::GetScanResults() + -> std::vector { std::vector results; results.resize(scan_results_.size()); { @@ -330,7 +354,7 @@ auto SceneV1AppMode::GetScanResults() return results; } -auto SceneV1AppMode::GetActive() -> SceneV1AppMode* { +auto ClassicAppMode::GetActive() -> ClassicAppMode* { // Note: this gets called by non-logic threads, and not // doing any locking here so bg thread callers should // keep in mind that app-mode may change under them. @@ -342,63 +366,64 @@ auto SceneV1AppMode::GetActive() -> SceneV1AppMode* { return nullptr; } -bool SceneV1AppMode::HasConnectionToHost() const { +bool ClassicAppMode::HasConnectionToHost() const { return connections()->has_connection_to_host(); } -millisecs_t SceneV1AppMode::LastClientJoinTime() const { +millisecs_t ClassicAppMode::LastClientJoinTime() const { return last_connection_to_client_join_time(); } -bool SceneV1AppMode::HasConnectionToClients() const { +bool ClassicAppMode::HasConnectionToClients() const { return connections()->HasConnectionToClients(); } -auto SceneV1AppMode::GetActiveOrWarn() -> SceneV1AppMode* { +auto ClassicAppMode::GetActiveOrWarn() -> ClassicAppMode* { auto* val{GetActive()}; if (val == nullptr) { Log(LogLevel::kWarning, - "Attempting to access SceneAppMode while it is inactive."); + "Attempting to access ClassicAppMode while it is inactive."); } return val; } -auto SceneV1AppMode::GetActiveOrThrow() -> SceneV1AppMode* { +auto ClassicAppMode::GetActiveOrThrow() -> ClassicAppMode* { auto* val{GetActive()}; if (val == nullptr) { - throw Exception("Attempting to access SceneAppMode while it is inactive."); + throw Exception( + "Attempting to access ClassicAppMode while it is inactive."); } return val; } -auto SceneV1AppMode::GetActiveOrFatal() -> SceneV1AppMode* { +auto ClassicAppMode::GetActiveOrFatal() -> ClassicAppMode* { auto* val{GetActive()}; if (val == nullptr) { - FatalError("Attempting to access SceneAppMode while it is inactive."); + FatalError("Attempting to access ClassicAppMode while it is inactive."); } return val; } -auto SceneV1AppMode::GetSingleton() -> SceneV1AppMode* { +auto ClassicAppMode::GetSingleton() -> ClassicAppMode* { assert(g_base->InLogicThread()); if (g_scene_v1_app_mode == nullptr) { - g_scene_v1_app_mode = new SceneV1AppMode(); + g_scene_v1_app_mode = new ClassicAppMode(); } return g_scene_v1_app_mode; } -SceneV1AppMode::SceneV1AppMode() +ClassicAppMode::ClassicAppMode() : game_roster_(cJSON_CreateArray()), - connections_(std::make_unique()) {} + connections_(std::make_unique()) {} -void SceneV1AppMode::HandleIncomingUDPPacket(const std::vector& data, +void ClassicAppMode::HandleIncomingUDPPacket(const std::vector& data, const SockAddr& addr) { // Just forward it along to our connection-set to handle. connections()->HandleIncomingUDPPacket(data, addr); } -auto SceneV1AppMode::HandleJSONPing(const std::string& data_str) +auto ClassicAppMode::HandleJSONPing(const std::string& data_str) -> std::string { // Note to self - this is called in a non-logic thread. cJSON* data = cJSON_Parse(data_str.c_str()); @@ -415,20 +440,20 @@ auto SceneV1AppMode::HandleJSONPing(const std::string& data_str) return buffer; } -void SceneV1AppMode::SetGameRoster(cJSON* r) { +void ClassicAppMode::SetGameRoster(cJSON* r) { if (game_roster_ != nullptr) { cJSON_Delete(game_roster_); } game_roster_ = r; } -auto SceneV1AppMode::GetPartySize() const -> int { +auto ClassicAppMode::GetPartySize() const -> int { assert(g_base->InLogicThread()); assert(game_roster_ != nullptr); return cJSON_GetArraySize(game_roster_); } -auto SceneV1AppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t { +auto ClassicAppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t { std::optional min_time_to_next; for (auto&& i : sessions_) { if (!i.Exists()) { @@ -447,7 +472,7 @@ auto SceneV1AppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t { : base::kHeadlessMaxDisplayTimeStep; } -void SceneV1AppMode::StepDisplayTime() { +void ClassicAppMode::StepDisplayTime() { assert(g_base->InLogicThread()); auto startms{core::CorePlatform::GetCurrentMillisecs()}; @@ -529,7 +554,7 @@ void SceneV1AppMode::StepDisplayTime() { } } -auto SceneV1AppMode::GetGameRosterMessage_() -> std::vector { +auto ClassicAppMode::GetGameRosterMessage_() -> std::vector { // This message is simply a flattened json string of our roster (including // terminating char). char* s = cJSON_PrintUnformatted(game_roster_); @@ -542,8 +567,8 @@ auto SceneV1AppMode::GetGameRosterMessage_() -> std::vector { return msg; } -base::ContextRef SceneV1AppMode::GetForegroundContext() { - Session* s = GetForegroundSession(); +base::ContextRef ClassicAppMode::GetForegroundContext() { + scene_v1::Session* s = GetForegroundSession(); if (s) { return s->GetForegroundContext(); } else { @@ -551,7 +576,7 @@ base::ContextRef SceneV1AppMode::GetForegroundContext() { } } -void SceneV1AppMode::UpdateGameRoster() { +void ClassicAppMode::UpdateGameRoster() { assert(g_base->InLogicThread()); assert(game_roster_ != nullptr); @@ -570,14 +595,15 @@ void SceneV1AppMode::UpdateGameRoster() { bool include_self = (connections()->GetConnectedClientCount() > 0); - if (auto* hs = dynamic_cast(GetForegroundSession())) { + if (auto* hs = dynamic_cast(GetForegroundSession())) { // Add our host-y self. if (include_self) { cJSON* client_dict = cJSON_CreateObject(); cJSON_AddItemToObject( client_dict, "spec", - cJSON_CreateString( - PlayerSpec::GetAccountPlayerSpec().GetSpecString().c_str())); + cJSON_CreateString(scene_v1::PlayerSpec::GetAccountPlayerSpec() + .GetSpecString() + .c_str())); // Add our list of local players. cJSON* player_array = cJSON_CreateArray(); @@ -632,10 +658,11 @@ void SceneV1AppMode::UpdateGameRoster() { if (p->accepted() && p->name_is_real() && delegate->IsRemoteClient()) { auto* client_delegate = - static_cast(delegate); - assert(dynamic_cast(delegate) + static_cast(delegate); + assert(dynamic_cast(delegate) == client_delegate); - ConnectionToClient* ctc = client_delegate->connection_to_client(); + scene_v1::ConnectionToClient* ctc = + client_delegate->connection_to_client(); // Add some basic info for each remote player. if (ctc != nullptr && ctc == i.second.Get()) { @@ -669,12 +696,12 @@ void SceneV1AppMode::UpdateGameRoster() { game_roster_dirty_ = true; } -void SceneV1AppMode::UpdateKickVote_() { +void ClassicAppMode::UpdateKickVote_() { if (!kick_vote_in_progress_) { return; } - ConnectionToClient* kick_vote_starter = kick_vote_starter_.Get(); - ConnectionToClient* kick_vote_target = kick_vote_target_.Get(); + scene_v1::ConnectionToClient* kick_vote_starter = kick_vote_starter_.Get(); + scene_v1::ConnectionToClient* kick_vote_target = kick_vote_target_.Get(); // If the target is no longer with us, silently end. if (kick_vote_target == nullptr) { @@ -688,7 +715,8 @@ void SceneV1AppMode::UpdateKickVote_() { // Tally current votes for connected clients; if anything has changed, print // the update and possibly perform the kick. - for (ConnectionToClient* client : connections()->GetConnectionsToClients()) { + for (scene_v1::ConnectionToClient* client : + connections()->GetConnectionsToClients()) { ++total_client_count; if (client->kick_voted()) { if (client->kick_vote_choice()) { @@ -716,7 +744,7 @@ void SceneV1AppMode::UpdateKickVote_() { // Disallow kicking for a while for everyone.. but ESPECIALLY so for the // guy who launched the failed vote. - for (ConnectionToClient* client : + for (scene_v1::ConnectionToClient* client : connections()->GetConnectionsToClients()) { millisecs_t delay = kKickVoteFailRetryDelay; if (client == kick_vote_starter) { @@ -774,8 +802,8 @@ void SceneV1AppMode::UpdateKickVote_() { } } -void SceneV1AppMode::StartKickVote(ConnectionToClient* starter, - ConnectionToClient* target) { +void ClassicAppMode::StartKickVote(scene_v1::ConnectionToClient* starter, + scene_v1::ConnectionToClient* target) { // Restrict votes per client. millisecs_t current_time = g_core->GetAppTimeMillisecs(); @@ -825,7 +853,7 @@ void SceneV1AppMode::StartKickVote(ConnectionToClient* starter, + "\"]]}", 1, 0, 0); } else { - std::vector connected_clients = + std::vector connected_clients = connections()->GetConnectionsToClients(); // Ok, kick off a vote.. (send the question and instructions to everyone @@ -861,7 +889,7 @@ void SceneV1AppMode::StartKickVote(ConnectionToClient* starter, kick_vote_target_ = target; // Reset votes for all connected clients. - for (ConnectionToClient* client : + for (scene_v1::ConnectionToClient* client : connections()->GetConnectionsToClients()) { if (client == starter) { client->set_kick_voted(true); @@ -873,19 +901,19 @@ void SceneV1AppMode::StartKickVote(ConnectionToClient* starter, } } -void SceneV1AppMode::SetForegroundScene(Scene* sg) { +void ClassicAppMode::SetForegroundScene(scene_v1::Scene* sg) { assert(g_base->InLogicThread()); if (foreground_scene_.Get() != sg) { foreground_scene_ = sg; // If this scene has a globals-node, put it in charge of stuff. - if (GlobalsNode* g = sg->globals_node()) { + if (scene_v1::GlobalsNode* g = sg->globals_node()) { g->SetAsForeground(); } } } -auto SceneV1AppMode::GetNetworkDebugString() -> std::string { +auto ClassicAppMode::GetNetworkDebugString() -> std::string { char net_info_str[128]; int64_t in_count = 0; int64_t in_size = 0; @@ -898,7 +926,7 @@ auto SceneV1AppMode::GetNetworkDebugString() -> std::string { bool show = false; // Add in/out data for any host connection. - if (ConnectionToHost* connection_to_host = + if (scene_v1::ConnectionToHost* connection_to_host = connections()->connection_to_host()) { if (connection_to_host->can_communicate()) show = true; in_size += connection_to_host->GetBytesInPerSecond(); @@ -912,7 +940,7 @@ auto SceneV1AppMode::GetNetworkDebugString() -> std::string { } else { int connected_count = 0; for (auto&& i : connections()->connections_to_clients()) { - ConnectionToClient* client = i.second.Get(); + scene_v1::ConnectionToClient* client = i.second.Get(); if (client->can_communicate()) { show = true; connected_count += 1; @@ -942,8 +970,8 @@ auto SceneV1AppMode::GetNetworkDebugString() -> std::string { static_cast_check_fit(resends)); return net_info_str; } -auto SceneV1AppMode::GetDisplayPing() -> std::optional { - if (ConnectionToHost* connection_to_host = +auto ClassicAppMode::GetDisplayPing() -> std::optional { + if (scene_v1::ConnectionToHost* connection_to_host = connections()->connection_to_host()) { if (connection_to_host->can_communicate()) { return connection_to_host->current_ping(); @@ -952,7 +980,7 @@ auto SceneV1AppMode::GetDisplayPing() -> std::optional { return {}; } -void SceneV1AppMode::CleanUpBeforeConnectingToHost() { +void ClassicAppMode::CleanUpBeforeConnectingToHost() { // We can't have connected clients and a host-connection at the same time. // Make a minimal attempt to disconnect any client connections we have, but // get them off the list immediately. @@ -965,7 +993,7 @@ void SceneV1AppMode::CleanUpBeforeConnectingToHost() { SetPublicPartyEnabled(false); } -void SceneV1AppMode::LaunchHostSession(PyObject* session_type_obj, +void ClassicAppMode::LaunchHostSession(PyObject* session_type_obj, base::BenchmarkType benchmark_type) { if (in_update_) { throw Exception( @@ -983,10 +1011,11 @@ void SceneV1AppMode::LaunchHostSession(PyObject* session_type_obj, // This should kill any current session and get us back to a blank slate. Reset_(); - Object::WeakRef old_foreground_session(foreground_session_); + Object::WeakRef old_foreground_session( + foreground_session_); try { // Create the new session. - auto s(Object::New(session_type_obj)); + auto s(Object::New(session_type_obj)); s->set_benchmark_type(benchmark_type); sessions_.emplace_back(s); @@ -1000,7 +1029,7 @@ void SceneV1AppMode::LaunchHostSession(PyObject* session_type_obj, } } -void SceneV1AppMode::LaunchReplaySession(const std::string& file_name) { +void ClassicAppMode::LaunchReplaySession(const std::string& file_name) { if (in_update_) throw Exception( "can't launch a session from within a session update; use " @@ -1015,9 +1044,11 @@ void SceneV1AppMode::LaunchReplaySession(const std::string& file_name) { Reset_(); // Create the new session. - Object::WeakRef old_foreground_session(foreground_session_); + Object::WeakRef old_foreground_session( + foreground_session_); try { - auto s(Object::New(file_name)); + auto s(Object::New( + file_name)); sessions_.push_back(s); // It should have set itself as FG. @@ -1030,7 +1061,7 @@ void SceneV1AppMode::LaunchReplaySession(const std::string& file_name) { } } -void SceneV1AppMode::LaunchClientSession() { +void ClassicAppMode::LaunchClientSession() { if (in_update_) { throw Exception( "can't launch a session from within a session update; use " @@ -1045,9 +1076,10 @@ void SceneV1AppMode::LaunchClientSession() { Reset_(); // Create the new session. - Object::WeakRef old_foreground_session(foreground_session_); + Object::WeakRef old_foreground_session( + foreground_session_); try { - auto s(Object::New()); + auto s(Object::New()); sessions_.push_back(s); // It should have set itself as FG. @@ -1059,35 +1091,11 @@ void SceneV1AppMode::LaunchClientSession() { } } -// Reset to a blank slate. -void SceneV1AppMode::Reset_() { - assert(g_base); - assert(g_base->InLogicThread()); - - // Tear down our existing session. - foreground_session_.Clear(); - PruneSessions_(); - - // If all is well our sessions should all be dead. - if (g_scene_v1->session_count != 0) { - Log(LogLevel::kError, "Session-count is non-zero (" - + std::to_string(g_scene_v1->session_count) - + ") on Logic::Reset."); - } - - g_scene_v1->Reset(); - g_base->ui->Reset(); - g_base->input->Reset(); - g_base->graphics->Reset(); - g_base->python->Reset(); - g_base->audio->Reset(); -} - -void SceneV1AppMode::ChangeGameSpeed(int offs) { +void ClassicAppMode::ChangeGameSpeed(int offs) { assert(g_base->InLogicThread()); // If we're in a replay session, adjust playback speed there. - if (dynamic_cast(GetForegroundSession())) { + if (dynamic_cast(GetForegroundSession())) { int old_speed = replay_speed_exponent(); SetReplaySpeedExponent(replay_speed_exponent() + offs); if (old_speed != replay_speed_exponent()) { @@ -1103,27 +1111,27 @@ void SceneV1AppMode::ChangeGameSpeed(int offs) { debug_speed_exponent_ += offs; debug_speed_mult_ = powf(2.0f, static_cast(debug_speed_exponent_)); ScreenMessage("DEBUG GAME SPEED TO " + std::to_string(debug_speed_mult_)); - Session* s = GetForegroundSession(); + scene_v1::Session* s = GetForegroundSession(); if (s) { s->DebugSpeedMultChanged(); } } } -void SceneV1AppMode::OnScreenSizeChange() { - if (Session* session = GetForegroundSession()) { +void ClassicAppMode::OnScreenSizeChange() { + if (scene_v1::Session* session = GetForegroundSession()) { session->OnScreenSizeChange(); } } // Called by a newly made Session instance to set itself as the current // session. -void SceneV1AppMode::SetForegroundSession(Session* s) { +void ClassicAppMode::SetForegroundSession(scene_v1::Session* s) { assert(g_base->InLogicThread()); foreground_session_ = s; } -void SceneV1AppMode::LocalDisplayChatMessage( +void ClassicAppMode::LocalDisplayChatMessage( const std::vector& buffer) { // 1 type byte, 1 spec-len byte, 1 or more spec chars, 0 or more msg chars. if (buffer.size() > 3) { @@ -1140,7 +1148,7 @@ void SceneV1AppMode::LocalDisplayChatMessage( b2[msg_len] = 0; std::string final_message = - PlayerSpec(b1.data()).GetDisplayString() + ": " + b2.data(); + scene_v1::PlayerSpec(b1.data()).GetDisplayString() + ": " + b2.data(); // Store it locally. chat_messages_.push_back(final_message); @@ -1165,9 +1173,10 @@ void SceneV1AppMode::LocalDisplayChatMessage( } } -void SceneV1AppMode::DoApplyAppConfig() { +void ClassicAppMode::DoApplyAppConfig() { // Kick-idle-players setting (hmm is this still relevant?). - auto* host_session = dynamic_cast(foreground_session_.Get()); + auto* host_session = + dynamic_cast(foreground_session_.Get()); kick_idle_players_ = g_base->app_config->Resolve(base::AppConfig::BoolID::kKickIdlePlayers); if (host_session) { @@ -1181,7 +1190,7 @@ void SceneV1AppMode::DoApplyAppConfig() { base::AppConfig::OptionalFloatID::kIdleExitMinutes); } -void SceneV1AppMode::PruneSessions_() { +void ClassicAppMode::PruneSessions_() { bool have_dead_session = false; for (auto&& i : sessions_) { if (i.Exists()) { @@ -1200,7 +1209,7 @@ void SceneV1AppMode::PruneSessions_() { } } if (have_dead_session) { - std::vector > live_list; + std::vector > live_list; for (auto&& i : sessions_) { if (i.Exists()) { live_list.push_back(i); @@ -1210,31 +1219,31 @@ void SceneV1AppMode::PruneSessions_() { } } -void SceneV1AppMode::LanguageChanged() { +void ClassicAppMode::LanguageChanged() { assert(g_base && g_base->InLogicThread()); - if (Session* session = GetForegroundSession()) { + if (scene_v1::Session* session = GetForegroundSession()) { session->LanguageChanged(); } } -void SceneV1AppMode::SetReplaySpeedExponent(int val) { +void ClassicAppMode::SetReplaySpeedExponent(int val) { replay_speed_exponent_ = std::min(3, std::max(-3, val)); replay_speed_mult_ = powf(2.0f, static_cast(replay_speed_exponent_)); } -void SceneV1AppMode::PauseReplay() { replay_paused_ = true; } +void ClassicAppMode::PauseReplay() { replay_paused_ = true; } -void SceneV1AppMode::ResumeReplay() { replay_paused_ = false; } +void ClassicAppMode::ResumeReplay() { replay_paused_ = false; } -void SceneV1AppMode::SetDebugSpeedExponent(int val) { +void ClassicAppMode::SetDebugSpeedExponent(int val) { debug_speed_exponent_ = val; debug_speed_mult_ = powf(2.0f, static_cast(debug_speed_exponent_)); - Session* s = GetForegroundSession(); + scene_v1::Session* s = GetForegroundSession(); if (s) s->DebugSpeedMultChanged(); } -void SceneV1AppMode::SetPublicPartyEnabled(bool val) { +void ClassicAppMode::SetPublicPartyEnabled(bool val) { assert(g_base->InLogicThread()); if (val == public_party_enabled_) { return; @@ -1243,7 +1252,7 @@ void SceneV1AppMode::SetPublicPartyEnabled(bool val) { g_base->plus()->PushPublicPartyState(); } -void SceneV1AppMode::SetPublicPartySize(int count) { +void ClassicAppMode::SetPublicPartySize(int count) { assert(g_base->InLogicThread()); if (count == public_party_size_) { return; @@ -1257,7 +1266,7 @@ void SceneV1AppMode::SetPublicPartySize(int count) { } } -void SceneV1AppMode::SetPublicPartyQueueEnabled(bool enabled) { +void ClassicAppMode::SetPublicPartyQueueEnabled(bool enabled) { assert(g_base->InLogicThread()); if (enabled == public_party_queue_enabled_) { return; @@ -1271,7 +1280,7 @@ void SceneV1AppMode::SetPublicPartyQueueEnabled(bool enabled) { } } -void SceneV1AppMode::SetPublicPartyMaxSize(int count) { +void ClassicAppMode::SetPublicPartyMaxSize(int count) { assert(g_base->InLogicThread()); if (count == public_party_max_size_) { return; @@ -1285,7 +1294,7 @@ void SceneV1AppMode::SetPublicPartyMaxSize(int count) { } } -void SceneV1AppMode::SetPublicPartyName(const std::string& name) { +void ClassicAppMode::SetPublicPartyName(const std::string& name) { assert(g_base->InLogicThread()); if (name == public_party_name_) { return; @@ -1299,7 +1308,7 @@ void SceneV1AppMode::SetPublicPartyName(const std::string& name) { } } -void SceneV1AppMode::SetPublicPartyStatsURL(const std::string& url) { +void ClassicAppMode::SetPublicPartyStatsURL(const std::string& url) { assert(g_base->InLogicThread()); if (url == public_party_stats_url_) { return; @@ -1313,7 +1322,7 @@ void SceneV1AppMode::SetPublicPartyStatsURL(const std::string& url) { } } -void SceneV1AppMode::SetPublicPartyPlayerCount(int count) { +void ClassicAppMode::SetPublicPartyPlayerCount(int count) { assert(g_base->InLogicThread()); if (count == public_party_player_count_) { return; @@ -1327,21 +1336,21 @@ void SceneV1AppMode::SetPublicPartyPlayerCount(int count) { } } -auto SceneV1AppMode::DoesWorldFillScreen() -> bool { +auto ClassicAppMode::DoesWorldFillScreen() -> bool { if (auto* session = GetForegroundSession()) { return session->DoesFillScreen(); } return false; } -void SceneV1AppMode::DrawWorld(base::FrameDef* frame_def) { +void ClassicAppMode::DrawWorld(base::FrameDef* frame_def) { if (auto* session = GetForegroundSession()) { session->Draw(frame_def); frame_def->set_benchmark_type(session->benchmark_type()); } } -auto SceneV1AppMode::ShouldAnnouncePartyJoinsAndLeaves() -> bool { +auto ClassicAppMode::ShouldAnnouncePartyJoinsAndLeaves() -> bool { assert(g_base->InLogicThread()); // At the moment we don't announce these for public internet parties.. (too @@ -1349,7 +1358,7 @@ auto SceneV1AppMode::ShouldAnnouncePartyJoinsAndLeaves() -> bool { return !public_party_enabled(); } -auto SceneV1AppMode::IsPlayerBanned(const PlayerSpec& spec) -> bool { +auto ClassicAppMode::IsPlayerBanned(const scene_v1::PlayerSpec& spec) -> bool { millisecs_t current_time = g_core->GetAppTimeMillisecs(); // Now is a good time to prune no-longer-banned specs. @@ -1366,11 +1375,12 @@ auto SceneV1AppMode::IsPlayerBanned(const PlayerSpec& spec) -> bool { return false; } -void SceneV1AppMode::BanPlayer(const PlayerSpec& spec, millisecs_t duration) { +void ClassicAppMode::BanPlayer(const scene_v1::PlayerSpec& spec, + millisecs_t duration) { banned_players_.emplace_back(g_core->GetAppTimeMillisecs() + duration, spec); } -void SceneV1AppMode::HandleQuitOnIdle_() { +void ClassicAppMode::HandleQuitOnIdle_() { if (idle_exit_minutes_) { auto idle_seconds{static_cast(g_base->input->input_idle_time()) * 0.001f}; @@ -1383,7 +1393,7 @@ void SceneV1AppMode::HandleQuitOnIdle_() { } } -void SceneV1AppMode::SetInternalMusic(base::SoundAsset* music, float volume, +void ClassicAppMode::SetInternalMusic(base::SoundAsset* music, float volume, bool loop) { // Stop any playing music. if (internal_music_play_id_) { @@ -1405,7 +1415,7 @@ void SceneV1AppMode::SetInternalMusic(base::SoundAsset* music, float volume, } } -void SceneV1AppMode::HandleGameQuery(const char* buffer, size_t size, +void ClassicAppMode::HandleGameQuery(const char* buffer, size_t size, sockaddr_storage* from) { if (size == 5) { // If we're already in a party, don't advertise since they @@ -1467,18 +1477,19 @@ void SceneV1AppMode::HandleGameQuery(const char* buffer, size_t size, } } -void SceneV1AppMode::RunMainMenu() { +void ClassicAppMode::RunMainMenu() { assert(g_base->InLogicThread()); if (g_base->logic->shutting_down()) { return; } assert(g_base->InLogicThread()); - PythonRef result = g_scene_v1->python->objs() - .Get(SceneV1Python::ObjID::kLaunchMainMenuSessionCall) - .Call(); + PythonRef result = + g_scene_v1->python->objs() + .Get(scene_v1::SceneV1Python::ObjID::kLaunchMainMenuSessionCall) + .Call(); if (!result.Exists()) { throw Exception("Error running scene_v1 main menu."); } } -} // namespace ballistica::scene_v1 +} // namespace ballistica::classic diff --git a/src/ballistica/scene_v1/support/scene_v1_app_mode.h b/src/ballistica/classic/support/classic_app_mode.h similarity index 85% rename from src/ballistica/scene_v1/support/scene_v1_app_mode.h rename to src/ballistica/classic/support/classic_app_mode.h index 45bcd8b7..0fa75159 100644 --- a/src/ballistica/scene_v1/support/scene_v1_app_mode.h +++ b/src/ballistica/classic/support/classic_app_mode.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_APP_MODE_H_ -#define BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_APP_MODE_H_ +#ifndef BALLISTICA_CLASSIC_SUPPORT_CLASSIC_APP_MODE_H_ +#define BALLISTICA_CLASSIC_SUPPORT_CLASSIC_APP_MODE_H_ #include #include @@ -12,37 +12,38 @@ #include "ballistica/base/app_mode/app_mode.h" #include "ballistica/base/base.h" +#include "ballistica/classic/classic.h" #include "ballistica/scene_v1/scene_v1.h" #include "ballistica/shared/foundation/object.h" -namespace ballistica::scene_v1 { +namespace ballistica::classic { const int kMaxPartyNameCombinedSize = 25; /// Defines high level app behavior when we're active. -class SceneV1AppMode : public base::AppMode { +class ClassicAppMode : public base::AppMode { public: /// Create or return our singleton (regardless of active state). /// Will never return nullptr. - static auto GetSingleton() -> SceneV1AppMode*; + static auto GetSingleton() -> ClassicAppMode*; /// Return our singleton if it is active and nullptr otherwise. /// Be sure to handle the case where it is not. - static auto GetActive() -> SceneV1AppMode*; + static auto GetActive() -> ClassicAppMode*; /// Return our singleton if it is active and log a warning and return nullptr /// if not. Use when you're gracefully handling the nullptr case but don't /// expect it to ever occur. - static auto GetActiveOrWarn() -> SceneV1AppMode*; + static auto GetActiveOrWarn() -> ClassicAppMode*; /// Return our singleton if it is active and throw an Exception if not. /// Use when exception logic can gracefully handle the fail case. - static auto GetActiveOrThrow() -> SceneV1AppMode*; + static auto GetActiveOrThrow() -> ClassicAppMode*; /// Return our singleton if it is active and fatal-error otherwise. /// Use when you are not handling the nullptr case and don't expect /// it to ever occur. - static auto GetActiveOrFatal() -> SceneV1AppMode*; + static auto GetActiveOrFatal() -> ClassicAppMode*; auto HandleJSONPing(const std::string& data_str) -> std::string override; void HandleIncomingUDPPacket(const std::vector& data_in, @@ -55,9 +56,10 @@ class SceneV1AppMode : public base::AppMode { void SetGameRoster(cJSON* r); auto GetPartySize() const -> int override; auto kick_vote_in_progress() const -> bool { return kick_vote_in_progress_; } - void StartKickVote(ConnectionToClient* starter, ConnectionToClient* target); + void StartKickVote(scene_v1::ConnectionToClient* starter, + scene_v1::ConnectionToClient* target); void set_kick_voting_enabled(bool enable) { kick_voting_enabled_ = enable; } - void SetForegroundScene(Scene* sg); + void SetForegroundScene(scene_v1::Scene* sg); void LaunchHostSession( PyObject* session_type_obj, @@ -69,13 +71,13 @@ class SceneV1AppMode : public base::AppMode { auto GetDisplayPing() -> std::optional override; auto HasConnectionToHost() const -> bool override; auto HasConnectionToClients() const -> bool override; - auto connections() const -> ConnectionSet* { + auto connections() const -> scene_v1::ConnectionSet* { assert(connections_.get()); return connections_.get(); } void CleanUpBeforeConnectingToHost(); void ChangeGameSpeed(int offs) override; - void SetForegroundSession(Session* s); + void SetForegroundSession(scene_v1::Session* s); void LocalDisplayChatMessage(const std::vector& buffer); auto chat_messages() const -> const std::list& { return chat_messages_; @@ -83,12 +85,12 @@ class SceneV1AppMode : public base::AppMode { void DoApplyAppConfig() override; // Return whichever session is front and center. - auto GetForegroundSession() const -> Session* { + auto GetForegroundSession() const -> scene_v1::Session* { return foreground_session_.Get(); } // Used to know which globals is in control currently/etc. - auto GetForegroundScene() const -> Scene* { + auto GetForegroundScene() const -> scene_v1::Scene* { assert(g_base->InLogicThread()); return foreground_scene_.Get(); } @@ -148,8 +150,8 @@ class SceneV1AppMode : public base::AppMode { void set_require_client_authentication(bool enable) { require_client_authentication_ = enable; } - auto IsPlayerBanned(const PlayerSpec& spec) -> bool; - void BanPlayer(const PlayerSpec& spec, millisecs_t duration); + auto IsPlayerBanned(const scene_v1::PlayerSpec& spec) -> bool; + void BanPlayer(const scene_v1::PlayerSpec& spec, millisecs_t duration); void OnAppStart() override; void OnAppSuspend() override; void OnAppUnsuspend() override; @@ -208,7 +210,7 @@ class SceneV1AppMode : public base::AppMode { } private: - SceneV1AppMode(); + ClassicAppMode(); void PruneScanResults_(); void UpdateKickVote_(); auto GetGameRosterMessage_() -> std::vector; @@ -228,9 +230,9 @@ class SceneV1AppMode : public base::AppMode { std::list chat_messages_; // *All* existing sessions (including old ones waiting to shut down). - std::vector > sessions_; - Object::WeakRef foreground_scene_; - Object::WeakRef foreground_session_; + std::vector > sessions_; + Object::WeakRef foreground_scene_; + Object::WeakRef foreground_session_; bool chat_muted_{}; bool in_update_{}; @@ -246,9 +248,9 @@ class SceneV1AppMode : public base::AppMode { cJSON* game_roster_{}; millisecs_t last_game_roster_send_time_{}; - std::unique_ptr connections_; - Object::WeakRef kick_vote_starter_; - Object::WeakRef kick_vote_target_; + std::unique_ptr connections_; + Object::WeakRef kick_vote_starter_; + Object::WeakRef kick_vote_target_; millisecs_t kick_vote_end_time_{}; int last_kick_votes_needed_{-1}; millisecs_t legacy_display_time_millisecs_{}; @@ -277,13 +279,13 @@ class SceneV1AppMode : public base::AppMode { std::string public_party_name_; std::string public_party_min_league_; std::string public_party_stats_url_; - std::list > banned_players_; + std::list > banned_players_; std::optional idle_exit_minutes_{}; std::optional internal_music_play_id_{}; std::optional public_party_public_address_ipv4_{}; std::optional public_party_public_address_ipv6_{}; }; -} // namespace ballistica::scene_v1 +} // namespace ballistica::classic -#endif // BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_APP_MODE_H_ +#endif // BALLISTICA_CLASSIC_SUPPORT_CLASSIC_APP_MODE_H_ diff --git a/src/ballistica/scene_v1/connection/connection_set.cc b/src/ballistica/scene_v1/connection/connection_set.cc index c487e323..a6336fd8 100644 --- a/src/ballistica/scene_v1/connection/connection_set.cc +++ b/src/ballistica/scene_v1/connection/connection_set.cc @@ -4,11 +4,11 @@ #include "ballistica/base/assets/assets.h" #include "ballistica/base/networking/network_writer.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/connection/connection_to_client_udp.h" #include "ballistica/scene_v1/connection/connection_to_host_udp.h" #include "ballistica/scene_v1/python/scene_v1_python.h" #include "ballistica/scene_v1/support/host_session.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" #include "ballistica/shared/foundation/event_loop.h" #include "ballistica/shared/python/python.h" @@ -88,14 +88,14 @@ void ConnectionSet::SendChatMessage(const std::string& message, "Can't send chat message with sender_override as a client."); } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); std::string our_spec_string; if (sender_override != nullptr) { std::string override_final = *sender_override; - if (override_final.size() > kMaxPartyNameCombinedSize) { - override_final.resize(kMaxPartyNameCombinedSize); + if (override_final.size() > classic::kMaxPartyNameCombinedSize) { + override_final.resize(classic::kMaxPartyNameCombinedSize); override_final += "..."; } our_spec_string = @@ -129,8 +129,8 @@ void ConnectionSet::SendChatMessage(const std::string& message, } } } - if (p_name_combined.size() > kMaxPartyNameCombinedSize) { - p_name_combined.resize(kMaxPartyNameCombinedSize); + if (p_name_combined.size() > classic::kMaxPartyNameCombinedSize) { + p_name_combined.resize(classic::kMaxPartyNameCombinedSize); p_name_combined += "..."; } if (!p_name_combined.empty()) { @@ -275,14 +275,14 @@ void ConnectionSet::PrepareForLaunchHostSession() { connection_to_host_->RequestDisconnect(); connection_to_host_.Clear(); has_connection_to_host_ = false; - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->UpdateGameRoster(); } } } void ConnectionSet::HandleClientDisconnected(int id) { - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); auto i = connections_to_clients_.find(id); if (i != connections_to_clients_.end()) { bool was_connected = i->second->can_communicate(); @@ -333,7 +333,7 @@ auto ConnectionSet::DisconnectClient(int client_id, int ban_seconds) -> bool { // If this is considered a kick, add an entry to our banned list so we // know not to let them back in for a while. if (ban_seconds > 0) { - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->BanPlayer(i->second->peer_spec(), 1000 * ban_seconds); } } @@ -363,7 +363,7 @@ void ConnectionSet::PushDisconnectedFromHostCall() { // Clear out our party roster. - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->UpdateGameRoster(); // Go back to main menu *if* the connection was fully connected. @@ -382,7 +382,7 @@ void ConnectionSet::PushHostConnectedUDPCall(const SockAddr& addr, g_base->logic->event_loop()->PushCall([this, addr, print_connect_progress] { // Attempt to disconnect any clients we have, turn off public-party // advertising, etc. - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->CleanUpBeforeConnectingToHost(); } print_udp_connect_progress_ = print_connect_progress; @@ -434,7 +434,7 @@ void ConnectionSet::ForceDisconnectClients() { void ConnectionSet::HandleIncomingUDPPacket(const std::vector& data_in, const SockAddr& addr) { assert(!data_in.empty()); - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); const uint8_t* data = &(data_in[0]); auto data_size = static_cast(data_in.size()); @@ -736,7 +736,7 @@ void ConnectionSet::SetClientInfoFromMasterServer( client->HandleMasterServerClientInfo(info_obj); // Roster will now include account-id... - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->MarkGameRosterDirty(); } break; diff --git a/src/ballistica/scene_v1/connection/connection_to_client.cc b/src/ballistica/scene_v1/connection/connection_to_client.cc index 367e8e16..4c011d8c 100644 --- a/src/ballistica/scene_v1/connection/connection_to_client.cc +++ b/src/ballistica/scene_v1/connection/connection_to_client.cc @@ -7,6 +7,7 @@ #include "ballistica/base/networking/networking.h" #include "ballistica/base/python/base_python.h" #include "ballistica/base/support/plus_soft.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/core/python/core_python.h" #include "ballistica/scene_v1/connection/connection_set.h" #include "ballistica/scene_v1/python/scene_v1_python.h" @@ -14,7 +15,6 @@ #include "ballistica/scene_v1/support/client_input_device.h" #include "ballistica/scene_v1/support/client_input_device_delegate.h" #include "ballistica/scene_v1/support/host_session.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/shared/generic/json.h" #include "ballistica/shared/generic/utils.h" #include "ballistica/shared/python/python_sys.h" @@ -27,7 +27,7 @@ const int kNewClientKickVoteDelay = 60000; ConnectionToClient::ConnectionToClient(int id) : id_(id), protocol_version_{ - SceneV1AppMode::GetSingleton()->host_protocol_version()} { + classic::ClassicAppMode::GetSingleton()->host_protocol_version()} { // We calc this once just in case it changes on our end // (the client uses it for their verification hash so we need to // ensure it stays consistent). @@ -76,7 +76,7 @@ ConnectionToClient::~ConnectionToClient() { // If they had been announced as connected, announce their departure. // It's also expected our app mode may no longer be active here; that's ok. - auto* appmode = SceneV1AppMode::GetActive(); + auto* appmode = classic::ClassicAppMode::GetActive(); if (appmode && can_communicate() && appmode->ShouldAnnouncePartyJoinsAndLeaves()) { std::string s = g_base->assets->GetResourceString("playerLeftPartyText"); @@ -144,7 +144,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { return; } - auto* appmode = SceneV1AppMode::GetActiveOrWarn(); + auto* appmode = classic::ClassicAppMode::GetActiveOrWarn(); if (!appmode) { return; } @@ -350,7 +350,7 @@ void ConnectionToClient::HandleMessagePacket( return; } - auto* appmode = SceneV1AppMode::GetActiveOrWarn(); + auto* appmode = classic::ClassicAppMode::GetActiveOrWarn(); if (!appmode) { return; } @@ -710,7 +710,7 @@ void ConnectionToClient::HandleMessagePacket( } auto ConnectionToClient::GetCombinedSpec() -> PlayerSpec { - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); // Look for players coming from this client-connection. // If we find any, make a spec out of their name(s). @@ -733,8 +733,8 @@ auto ConnectionToClient::GetCombinedSpec() -> PlayerSpec { } } } - if (p_name_combined.size() > kMaxPartyNameCombinedSize) { - p_name_combined.resize(kMaxPartyNameCombinedSize); + if (p_name_combined.size() > classic::kMaxPartyNameCombinedSize) { + p_name_combined.resize(classic::kMaxPartyNameCombinedSize); p_name_combined += "..."; } if (!p_name_combined.empty()) { @@ -765,7 +765,7 @@ auto ConnectionToClient::GetAsUDP() -> ConnectionToClientUDP* { } void ConnectionToClient::HandleMasterServerClientInfo(PyObject* info_obj) { - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); PyObject* profiles_obj = PyDict_GetItemString(info_obj, "p"); if (profiles_obj != nullptr) { @@ -802,7 +802,7 @@ void ConnectionToClient::HandleMasterServerClientInfo(PyObject* info_obj) { } auto ConnectionToClient::IsAdmin() const -> bool { - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); if (peer_public_account_id_.empty()) { return false; } diff --git a/src/ballistica/scene_v1/connection/connection_to_client_udp.cc b/src/ballistica/scene_v1/connection/connection_to_client_udp.cc index 424e09a9..fd07e855 100644 --- a/src/ballistica/scene_v1/connection/connection_to_client_udp.cc +++ b/src/ballistica/scene_v1/connection/connection_to_client_udp.cc @@ -4,8 +4,8 @@ #include "ballistica/base/logic/logic.h" #include "ballistica/base/networking/network_writer.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/connection/connection_set.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" namespace ballistica::scene_v1 { @@ -72,7 +72,7 @@ void ConnectionToClientUDP::Die() { return; } // this will actually clear the object.. - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->connections()->PushClientDisconnectedCall(id()); } did_die_ = true; diff --git a/src/ballistica/scene_v1/connection/connection_to_host.cc b/src/ballistica/scene_v1/connection/connection_to_host.cc index 9f9e1339..ee578fb9 100644 --- a/src/ballistica/scene_v1/connection/connection_to_host.cc +++ b/src/ballistica/scene_v1/connection/connection_to_host.cc @@ -9,9 +9,9 @@ #include "ballistica/base/platform/base_platform.h" #include "ballistica/base/python/base_python.h" #include "ballistica/base/support/plus_soft.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/core/python/core_python.h" #include "ballistica/scene_v1/support/client_session_net.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" #include "ballistica/shared/generic/json.h" #include "ballistica/shared/generic/utils.h" @@ -24,7 +24,7 @@ const int kPingSendInterval = 2000; ConnectionToHost::ConnectionToHost() : protocol_version_{ - SceneV1AppMode::GetSingleton()->host_protocol_version()} {} + classic::ClassicAppMode::GetSingleton()->host_protocol_version()} {} auto ConnectionToHost::GetAsUDP() -> ConnectionToHostUDP* { return nullptr; } @@ -93,7 +93,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { break; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); // We expect a > 3 byte handshake packet with protocol version as the // second and third bytes and name/info beyond that. @@ -354,7 +354,7 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { cJSON* new_roster = cJSON_Parse(reinterpret_cast(&(buffer[1]))); if (new_roster) { - if (auto* appmode = SceneV1AppMode::GetActive()) { + if (auto* appmode = classic::ClassicAppMode::GetActive()) { appmode->SetGameRoster(new_roster); } } @@ -496,7 +496,7 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { } case BA_MESSAGE_CHAT: { - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->LocalDisplayChatMessage(buffer); } break; diff --git a/src/ballistica/scene_v1/connection/connection_to_host_udp.cc b/src/ballistica/scene_v1/connection/connection_to_host_udp.cc index 3ae741eb..d737ed93 100644 --- a/src/ballistica/scene_v1/connection/connection_to_host_udp.cc +++ b/src/ballistica/scene_v1/connection/connection_to_host_udp.cc @@ -5,8 +5,8 @@ #include "ballistica/base/assets/assets.h" #include "ballistica/base/logic/logic.h" #include "ballistica/base/networking/network_writer.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/connection/connection_set.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/shared/math/vector3f.h" #include "ballistica/shared/networking/sockaddr.h" @@ -33,7 +33,7 @@ ConnectionToHostUDP::ConnectionToHostUDP(const SockAddr& addr) last_host_response_time_millisecs_( static_cast(g_base->logic->display_time() * 1000.0)) { GetRequestID_(); - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { if (appmode->connections()->GetPrintUDPConnectProgress()) { ScreenMessage(g_base->assets->GetResourceString("connectingToPartyText")); } @@ -58,7 +58,7 @@ void ConnectionToHostUDP::GetRequestID_() { void ConnectionToHostUDP::Update() { ConnectionToHost::Update(); - auto* appmode = SceneV1AppMode::GetActiveOrWarn(); + auto* appmode = classic::ClassicAppMode::GetActiveOrWarn(); if (!appmode) { return; } @@ -125,7 +125,7 @@ void ConnectionToHostUDP::Die() { Log(LogLevel::kError, "Posting multiple die messages; probably not good."); return; } - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { if (appmode->connections()->connection_to_host() == this) { appmode->connections()->PushDisconnectedFromHostCall(); did_die_ = true; diff --git a/src/ballistica/scene_v1/node/globals_node.cc b/src/ballistica/scene_v1/node/globals_node.cc index afa3a5dc..ecbb6bad 100644 --- a/src/ballistica/scene_v1/node/globals_node.cc +++ b/src/ballistica/scene_v1/node/globals_node.cc @@ -7,12 +7,12 @@ #include "ballistica/base/graphics/graphics.h" #include "ballistica/base/graphics/support/camera.h" #include "ballistica/base/support/classic_soft.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/core/core.h" #include "ballistica/scene_v1/node/node_attribute.h" #include "ballistica/scene_v1/node/node_type.h" #include "ballistica/scene_v1/support/host_activity.h" #include "ballistica/scene_v1/support/scene.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/shared/python/python.h" // FIXME: should not need this here. @@ -103,7 +103,7 @@ GlobalsNode::GlobalsNode(Scene* scene) : Node(scene, node_type) { // Set ourself as the current globals node for our scene. this->scene()->set_globals_node(this); - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); // If we're being made in a host-activity, also set ourself as the current // globals node for our activity. (there should only be one, so complain if @@ -191,7 +191,13 @@ void GlobalsNode::SetAsForeground() { auto GlobalsNode::IsCurrentGlobals() const -> bool { // We're current if our scene is the foreground one and we're the globals // node for our scene. - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActive(); + if (appmode == nullptr) { + BA_LOG_ERROR_NATIVE_TRACE( + "GlobalsNode::IsCurrentGlobals() called without ClassicAppMode " + "active."); + return false; + } Scene* scene = this->scene(); assert(scene); diff --git a/src/ballistica/scene_v1/python/methods/python_methods_networking.cc b/src/ballistica/scene_v1/python/methods/python_methods_networking.cc index c6605928..eb2a95b5 100644 --- a/src/ballistica/scene_v1/python/methods/python_methods_networking.cc +++ b/src/ballistica/scene_v1/python/methods/python_methods_networking.cc @@ -5,12 +5,12 @@ #include "ballistica/base/assets/assets.h" #include "ballistica/base/networking/network_reader.h" #include "ballistica/base/python/base_python.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/core/python/core_python.h" #include "ballistica/scene_v1/connection/connection_set.h" #include "ballistica/scene_v1/connection/connection_to_client.h" #include "ballistica/scene_v1/connection/connection_to_host_udp.h" #include "ballistica/scene_v1/python/scene_v1_python.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/shared/math/vector3f.h" #include "ballistica/shared/networking/sockaddr.h" #include "ballistica/shared/python/python.h" @@ -32,7 +32,7 @@ static auto PyGetPublicPartyEnabled(PyObject* self, PyObject* args, const_cast(kwlist))) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); if (appmode->public_party_enabled()) { Py_RETURN_TRUE; } else { @@ -62,7 +62,7 @@ static auto PySetPublicPartyEnabled(PyObject* self, PyObject* args, const_cast(kwlist), &enable)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->SetPublicPartyEnabled(static_cast(enable)); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -89,7 +89,7 @@ static auto PySetPublicPartyName(PyObject* self, PyObject* args, const_cast(kwlist), &name_obj)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); std::string name = g_base->python->GetPyLString(name_obj); appmode->SetPublicPartyName(name); Py_RETURN_NONE; @@ -117,7 +117,7 @@ static auto PySetPublicPartyStatsURL(PyObject* self, PyObject* args, const_cast(kwlist), &url_obj)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); // The call expects an empty string for the no-url option. std::string url = (url_obj == Py_None) ? "" : Python::GetPyString(url_obj); @@ -146,7 +146,7 @@ static auto PyGetPublicPartyMaxSize(PyObject* self, PyObject* args, const_cast(kwlist))) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); return PyLong_FromLong(appmode->public_party_max_size()); BA_PYTHON_CATCH; } @@ -172,7 +172,7 @@ static auto PySetPublicPartyMaxSize(PyObject* self, PyObject* args, const_cast(kwlist), &max_size)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->SetPublicPartyMaxSize(max_size); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -200,7 +200,7 @@ static auto PySetPublicPartyQueueEnabled(PyObject* self, PyObject* args, const_cast(kwlist), &enabled)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->SetPublicPartyQueueEnabled(enabled); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -227,7 +227,7 @@ static auto PySetPublicPartyPublicAddressIPV4(PyObject* self, PyObject* args, const_cast(kwlist), &address_obj)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); // The call expects an empty string for the no-url option. @@ -261,7 +261,7 @@ static auto PySetPublicPartyPublicAddressIPV6(PyObject* self, PyObject* args, const_cast(kwlist), &address_obj)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); // The call expects an empty string for the no-url option. @@ -295,7 +295,7 @@ static auto PySetAuthenticateClients(PyObject* self, PyObject* args, const_cast(kwlist), &enable)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->set_require_client_authentication(static_cast(enable)); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -322,7 +322,7 @@ static auto PySetAdmins(PyObject* self, PyObject* args, const_cast(kwlist), &admins_obj)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); auto admins = g_base->python->GetPyLStrings(admins_obj); std::set adminset; @@ -358,7 +358,7 @@ static auto PySetEnableDefaultKickVoting(PyObject* self, PyObject* args, } assert(g_base->logic); - if (auto* appmode{SceneV1AppMode::GetActiveOrWarn()}) { + if (auto* appmode{classic::ClassicAppMode::GetActiveOrWarn()}) { appmode->set_kick_voting_enabled(static_cast(enable)); } @@ -398,7 +398,7 @@ static auto PyConnectToParty(PyObject* self, PyObject* args, } // Error if we're not in our app-mode. - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); address = Python::GetPyString(address_obj); @@ -453,7 +453,7 @@ static auto PyClientInfoQueryResponse(PyObject* self, PyObject* args, return nullptr; } // Error if we're not in our app-mode. - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->connections()->SetClientInfoFromMasterServer(token, response_obj); Py_RETURN_NONE; @@ -484,7 +484,7 @@ static auto PyGetConnectionToHostInfo(PyObject* self, PyObject* args, "bascenev1.get_connection_to_host_info() is deprecated; use " "bascenev1.get_connection_to_host_info_2()."); BA_PRECONDITION(g_base->InLogicThread()); - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); ConnectionToHost* hc = appmode->connections()->connection_to_host(); if (hc) { @@ -518,7 +518,7 @@ static auto PyGetConnectionToHostInfo2(PyObject* self, PyObject* args, return nullptr; } BA_PRECONDITION(g_base->InLogicThread()); - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); ConnectionToHost* hc = appmode->connections()->connection_to_host(); if (hc) { @@ -569,7 +569,7 @@ static auto PyDisconnectFromHost(PyObject* self, PyObject* args, return nullptr; } // Error if we're not in our app-mode. - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->connections()->PushDisconnectFromHostCall(); Py_RETURN_NONE; @@ -602,7 +602,7 @@ static auto PyDisconnectClient(PyObject* self, PyObject* args, return nullptr; } // Error if we're not in our app-mode. - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); bool kickable = appmode->connections()->DisconnectClient(client_id, ban_time); if (kickable) { @@ -635,7 +635,7 @@ static auto PyGetClientPublicDeviceUUID(PyObject* self, PyObject* args, return nullptr; } // Error if we're not in our app-mode. - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); auto&& connection{ appmode->connections()->connections_to_clients().find(client_id)}; @@ -731,9 +731,9 @@ static PyMethodDef PySetMasterServerSourceDef = { static auto PyHostScanCycle(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->HostScanCycle(); - std::vector results = + std::vector results = appmode->GetScanResults(); PyObject* py_list = PyList_New(0); for (auto&& i : results) { @@ -760,7 +760,7 @@ static PyMethodDef PyHostScanCycleDef = { static auto PyEndHostScanning(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->EndHostScanning(); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -825,7 +825,7 @@ static auto PyChatMessage(PyObject* self, PyObject* args, return nullptr; } BA_PRECONDITION(g_base->InLogicThread()); - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); message = g_base->python->GetPyLString(message_obj); if (sender_override_obj != Py_None) { @@ -867,7 +867,7 @@ static auto PyGetChatMessages(PyObject* self, PyObject* args, const_cast(kwlist))) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); PyObject* py_list = PyList_New(0); for (auto&& i : appmode->chat_messages()) { PyList_Append(py_list, PyUnicode_FromString(i.c_str())); diff --git a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc index 09f15847..bbcf8f21 100644 --- a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc +++ b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc @@ -11,6 +11,7 @@ #include "ballistica/base/python/class/python_class_simple_sound.h" #include "ballistica/base/python/support/python_context_call_runnable.h" #include "ballistica/base/support/plus_soft.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/core/python/core_python.h" #include "ballistica/scene_v1/assets/scene_sound.h" #include "ballistica/scene_v1/assets/scene_texture.h" @@ -26,7 +27,6 @@ #include "ballistica/scene_v1/support/client_session_replay.h" #include "ballistica/scene_v1/support/host_activity.h" #include "ballistica/scene_v1/support/host_session.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" #include "ballistica/scene_v1/support/session_stream.h" #include "ballistica/shared/generic/json.h" @@ -316,7 +316,7 @@ static auto PyNewHostSession(PyObject* self, PyObject* args, &benchmark_type_str)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); base::BenchmarkType benchmark_type = base::BenchmarkType::kNone; if (benchmark_type_str != nullptr) { if (!strcmp(benchmark_type_str, "cpu")) { @@ -357,7 +357,7 @@ static auto PyNewReplaySession(PyObject* self, PyObject* args, args, keywds, "O", const_cast(kwlist), &file_name_obj)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); file_name = Python::GetPyString(file_name_obj); appmode->LaunchReplaySession(file_name); @@ -386,7 +386,7 @@ static auto PyIsInReplay(PyObject* self, PyObject* args, const_cast(kwlist))) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActive(); + auto* appmode = classic::ClassicAppMode::GetActive(); if (appmode && dynamic_cast(appmode->GetForegroundSession())) { Py_RETURN_TRUE; @@ -673,7 +673,7 @@ static auto PyBroadcastMessage(PyObject* self, PyObject* args, PyExcType::kValue); } std::vector client_ids; - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { if (clients_obj != Py_None) { std::vector client_ids2 = Python::GetPyInts(clients_obj); appmode->connections()->SendScreenMessageToSpecificClients( @@ -1050,7 +1050,8 @@ static auto PyCameraShake(PyObject* self, PyObject* args, if (Scene* scene = ContextRefSceneV1::FromCurrent().GetMutableScene()) { // Send to clients/replays (IF we're servering protocol 35+). - if (SceneV1AppMode::GetSingleton()->host_protocol_version() >= 35) { + if (classic::ClassicAppMode::GetSingleton()->host_protocol_version() + >= 35) { if (SessionStream* output_stream = scene->GetSceneStream()) { output_stream->EmitCameraShake(intensity); } @@ -1314,7 +1315,7 @@ static auto PyGetGameRoster(PyObject* self, PyObject* args, } PythonRef py_client_list(PyList_New(0), PythonRef::kSteal); - cJSON* party = SceneV1AppMode::GetSingleton()->game_roster(); + cJSON* party = classic::ClassicAppMode::GetSingleton()->game_roster(); assert(party); int len = cJSON_GetArraySize(party); for (int i = 0; i < len; i++) { @@ -1367,7 +1368,7 @@ static auto PyGetGameRoster(PyObject* self, PyObject* args, if (clientid == -1) { account_id = g_base->plus()->GetPublicV1AccountID(); } else { - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { auto client2 = appmode->connections()->connections_to_clients().find(clientid); if (client2 != appmode->connections()->connections_to_clients().end()) { @@ -1420,7 +1421,7 @@ static auto PySetDebugSpeedExponent(PyObject* self, if (!PyArg_ParseTuple(args, "i", &speed)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); HostActivity* host_activity = ContextRefSceneV1::FromCurrent().GetHostActivity(); @@ -1455,7 +1456,7 @@ static PyMethodDef PySetDebugSpeedExponentDef = { static auto PyGetReplaySpeedExponent(PyObject* self, PyObject* args) -> PyObject* { BA_PYTHON_TRY; - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); return PyLong_FromLong(appmode->replay_speed_exponent()); BA_PYTHON_CATCH; } @@ -1482,7 +1483,7 @@ static auto PySetReplaySpeedExponent(PyObject* self, if (!PyArg_ParseTuple(args, "i", &speed)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->SetReplaySpeedExponent(speed); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -1504,7 +1505,7 @@ static PyMethodDef PySetReplaySpeedExponentDef = { static auto PyIsReplayPaused(PyObject* self, PyObject* args) -> PyObject* { BA_PYTHON_TRY; - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); if (appmode->is_replay_paused()) { Py_RETURN_TRUE; } else { @@ -1528,7 +1529,7 @@ static PyMethodDef PyIsReplayPausedDef = { static auto PyPauseReplay(PyObject* self, PyObject* args) -> PyObject* { BA_PYTHON_TRY; - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->PauseReplay(); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -1550,7 +1551,7 @@ static PyMethodDef PyPauseReplayDef = { static auto PyResumeReplay(PyObject* self, PyObject* args) -> PyObject* { BA_PYTHON_TRY; - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->ResumeReplay(); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -1572,7 +1573,7 @@ static PyMethodDef PyResumeReplayDef = { static auto PySeekReplay(PyObject* self, PyObject* args) -> PyObject* { BA_PYTHON_TRY; - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); auto* session = dynamic_cast(appmode->GetForegroundSession()); if (session == nullptr) { @@ -1714,7 +1715,7 @@ static auto PySetInternalMusic(PyObject* self, PyObject* args, &volume, &loop)) { return nullptr; } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); if (music_obj == Py_None) { appmode->SetInternalMusic(nullptr); @@ -1737,117 +1738,13 @@ static PyMethodDef PySetInternalMusicDef = { "(internal).", }; -// -------------------------- on_app_mode_activate ----------------------------- - -static auto PyOnAppModeActivate(PyObject* self) -> PyObject* { - BA_PYTHON_TRY; - BA_PRECONDITION(g_base->InLogicThread()); - g_base->set_app_mode(SceneV1AppMode::GetSingleton()); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyOnAppModeActivateDef = { - "on_app_mode_activate", // name - (PyCFunction)PyOnAppModeActivate, // method - METH_NOARGS, // flags - - "on_app_mode_activate() -> None\n" - "\n" - "(internal)\n", -}; - -// ------------------------- on_app_mode_deactivate ---------------------------- - -static auto PyOnAppModeDeactivate(PyObject* self) -> PyObject* { - BA_PYTHON_TRY; - BA_PRECONDITION(g_base->InLogicThread()); - // Currently doing nothing. - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyOnAppModeDeactivateDef = { - "on_app_mode_deactivate", // name - (PyCFunction)PyOnAppModeDeactivate, // method - METH_NOARGS, // flags - - "on_app_mode_deactivate() -> None\n" - "\n" - "(internal)\n", -}; - -// ----------------------- handle_app_intent_default --------------------------- - -static auto PyHandleAppIntentDefault(PyObject* self) -> PyObject* { - BA_PYTHON_TRY; - BA_PRECONDITION(g_base->InLogicThread()); - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); - appmode->RunMainMenu(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyHandleAppIntentDefaultDef = { - "handle_app_intent_default", // name - (PyCFunction)PyHandleAppIntentDefault, // method - METH_NOARGS, // flags - - "handle_app_intent_default() -> None\n" - "\n" - "(internal)\n", -}; - -// ------------------------ handle_app_intent_exec ----------------------------- - -static auto PyHandleAppIntentExec(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - const char* command; - static const char* kwlist[] = {"command", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &command)) { - return nullptr; - } - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); - - // Run the command. - if (g_core->core_config().exec_command.has_value()) { - bool success = PythonCommand(*g_core->core_config().exec_command, - BA_BUILD_COMMAND_FILENAME) - .Exec(true, nullptr, nullptr); - if (!success) { - // TODO(ericf): what should we do in this case? - // Obviously if we add return/success values for intents we should set - // that here. - } - } - // If the stuff we just ran didn't result in a session, create a default - // one. - if (!appmode->GetForegroundSession()) { - appmode->RunMainMenu(); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyHandleAppIntentExecDef = { - "handle_app_intent_exec", // name - (PyCFunction)PyHandleAppIntentExec, // method - METH_VARARGS | METH_KEYWORDS, // flags - - "handle_app_intent_exec(command: str) -> None\n" - "\n" - "(internal)", -}; - // ---------------------------- protocol_version ------------------------------- static auto PyProtocolVersion(PyObject* self) -> PyObject* { BA_PYTHON_TRY; return PyLong_FromLong( - SceneV1AppMode::GetActiveOrThrow()->host_protocol_version()); + classic::ClassicAppMode::GetActiveOrThrow()->host_protocol_version()); BA_PYTHON_CATCH; } @@ -1900,10 +1797,6 @@ auto PythonMethodsScene::GetMethods() -> std::vector { PyBaseTimeDef, PyBaseTimerDef, PyLsInputDevicesDef, - PyOnAppModeActivateDef, - PyOnAppModeDeactivateDef, - PyHandleAppIntentDefaultDef, - PyHandleAppIntentExecDef, PyProtocolVersionDef, }; } diff --git a/src/ballistica/scene_v1/scene_v1.h b/src/ballistica/scene_v1/scene_v1.h index 2d41bbc5..e1ae0644 100644 --- a/src/ballistica/scene_v1/scene_v1.h +++ b/src/ballistica/scene_v1/scene_v1.h @@ -134,7 +134,6 @@ class ClientSessionReplay; class RigidBody; class SessionStream; class Scene; -class SceneV1AppMode; class SceneV1FeatureSet; class Session; class SceneSound; diff --git a/src/ballistica/scene_v1/support/client_session.cc b/src/ballistica/scene_v1/support/client_session.cc index f796c64c..d2e55997 100644 --- a/src/ballistica/scene_v1/support/client_session.cc +++ b/src/ballistica/scene_v1/support/client_session.cc @@ -7,6 +7,7 @@ #include "ballistica/base/graphics/graphics.h" #include "ballistica/base/graphics/support/screen_messages.h" #include "ballistica/base/networking/networking.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/assets/scene_collision_mesh.h" #include "ballistica/scene_v1/assets/scene_mesh.h" #include "ballistica/scene_v1/assets/scene_sound.h" @@ -18,7 +19,6 @@ #include "ballistica/scene_v1/node/node_type.h" #include "ballistica/scene_v1/python/scene_v1_python.h" #include "ballistica/scene_v1/support/scene.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/scene_v1/support/session_stream.h" namespace ballistica::scene_v1 { @@ -364,7 +364,7 @@ void ClientSession::Update(int time_advance_millisecs, double time_advance) { } case SessionCommand::kSetForegroundScene: { Scene* scene = GetScene(ReadInt32()); - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->SetForegroundScene(scene); } break; diff --git a/src/ballistica/scene_v1/support/client_session_net.cc b/src/ballistica/scene_v1/support/client_session_net.cc index 467735c1..58e68a9a 100644 --- a/src/ballistica/scene_v1/support/client_session_net.cc +++ b/src/ballistica/scene_v1/support/client_session_net.cc @@ -6,8 +6,8 @@ #include "ballistica/base/graphics/graphics.h" #include "ballistica/base/graphics/support/net_graph.h" #include "ballistica/base/logic/logic.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/connection/connection_to_host.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" namespace ballistica::scene_v1 { @@ -66,13 +66,13 @@ void ClientSessionNet::Update(int time_advance_millisecs, double time_advance) { } auto ClientSessionNet::GetBucketNum() -> int { - auto* appmode = scene_v1::SceneV1AppMode::GetSingleton(); + auto* appmode = classic::ClassicAppMode::GetSingleton(); return (delay_sample_counter_ / appmode->delay_bucket_samples()) % static_cast(buckets_.size()); } void ClientSessionNet::UpdateBuffering() { - auto* appmode = scene_v1::SceneV1AppMode::GetSingleton(); + auto* appmode = classic::ClassicAppMode::GetSingleton(); // Keep record of the most and least amount of time we've had buffered // recently, and slow down/speed up a bit based on that. { diff --git a/src/ballistica/scene_v1/support/client_session_replay.cc b/src/ballistica/scene_v1/support/client_session_replay.cc index cd60b10b..2d2f1fc5 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.cc +++ b/src/ballistica/scene_v1/support/client_session_replay.cc @@ -7,10 +7,10 @@ #include "ballistica/base/assets/assets.h" #include "ballistica/base/networking/networking.h" #include "ballistica/base/support/huffman.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/core/platform/core_platform.h" #include "ballistica/scene_v1/connection/connection_set.h" #include "ballistica/scene_v1/connection/connection_to_client.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/scene_v1/support/session_stream.h" #include "ballistica/shared/math/vector3f.h" @@ -28,7 +28,7 @@ auto ClientSessionReplay::GetActualTimeAdvanceMillisecs( } is_fast_forwarding_ = false; } - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); if (appmode->is_replay_paused()) { // FIXME: seeking a replay results in black screen here return 0; @@ -38,7 +38,7 @@ auto ClientSessionReplay::GetActualTimeAdvanceMillisecs( ClientSessionReplay::ClientSessionReplay(std::string filename) : file_name_(std::move(filename)) { - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); // take responsibility for feeding all clients to this device.. appmode->connections()->RegisterClientController(this); @@ -48,7 +48,7 @@ ClientSessionReplay::ClientSessionReplay(std::string filename) } ClientSessionReplay::~ClientSessionReplay() { - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); // we no longer are responsible for feeding clients to this device.. appmode->connections()->UnregisterClientController(this); diff --git a/src/ballistica/scene_v1/support/host_activity.cc b/src/ballistica/scene_v1/support/host_activity.cc index 17fe55bd..c74d338c 100644 --- a/src/ballistica/scene_v1/support/host_activity.cc +++ b/src/ballistica/scene_v1/support/host_activity.cc @@ -3,6 +3,7 @@ #include "ballistica/scene_v1/support/host_activity.h" #include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/assets/scene_collision_mesh.h" #include "ballistica/scene_v1/assets/scene_data_asset.h" #include "ballistica/scene_v1/assets/scene_mesh.h" @@ -13,7 +14,6 @@ #include "ballistica/scene_v1/node/node_type.h" #include "ballistica/scene_v1/support/host_session.h" #include "ballistica/scene_v1/support/player.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/scene_v1/support/session_stream.h" #include "ballistica/shared/generic/lambda_runnable.h" #include "ballistica/shared/generic/utils.h" @@ -259,7 +259,7 @@ void HostActivity::UpdateStepTimerLength() { if (!started_) { return; } - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); auto* host_session = host_session_.Get(); if (!host_session) { return; @@ -347,7 +347,7 @@ void HostActivity::SetIsForeground(bool val) { if (val && sg) { // Set it locally. - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->SetForegroundScene(sg); } diff --git a/src/ballistica/scene_v1/support/host_session.cc b/src/ballistica/scene_v1/support/host_session.cc index 9537d3ea..9a9406ab 100644 --- a/src/ballistica/scene_v1/support/host_session.cc +++ b/src/ballistica/scene_v1/support/host_session.cc @@ -5,12 +5,12 @@ #include "ballistica/base/graphics/graphics.h" #include "ballistica/base/python/base_python.h" #include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/assets/scene_data_asset.h" #include "ballistica/scene_v1/assets/scene_mesh.h" #include "ballistica/scene_v1/assets/scene_sound.h" #include "ballistica/scene_v1/assets/scene_texture.h" #include "ballistica/scene_v1/support/host_activity.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" #include "ballistica/scene_v1/support/session_stream.h" #include "ballistica/shared/generic/lambda_runnable.h" @@ -26,7 +26,7 @@ HostSession::HostSession(PyObject* session_type_obj) assert(g_base->InLogicThread()); assert(session_type_obj != nullptr); - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); base::ScopedSetContext ssc(this); // FIXME: Should be an attr of the session class, not hard-coded. @@ -61,7 +61,7 @@ HostSession::HostSession(PyObject* session_type_obj) } // Fade in from our current blackness. - g_base->graphics->FadeScreen(true, 250, nullptr); + // g_base->graphics->FadeScreen(true, 250, nullptr); // Start by showing the progress bar instead of hitching. g_base->graphics->EnableProgressBar(true); @@ -209,7 +209,7 @@ auto HostSession::GetForegroundContext() -> base::ContextRef { void HostSession::RequestPlayer(SceneV1InputDeviceDelegate* device) { assert(g_base->InLogicThread()); - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); // Ignore if we have no Python session Obj. if (!GetSessionPyObj()) { @@ -254,7 +254,7 @@ void HostSession::RequestPlayer(SceneV1InputDeviceDelegate* device) { void HostSession::RemovePlayer(Player* player) { assert(player); - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); // If we find the player amongst our ranks, remove them. // Note that it is expected to get redundant calls for this that @@ -330,7 +330,7 @@ void HostSession::SetForegroundHostActivity(HostActivity* a) { assert(a); assert(g_base->InLogicThread()); - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); if (shutting_down_) { Log(LogLevel::kWarning, diff --git a/src/ballistica/scene_v1/support/player.cc b/src/ballistica/scene_v1/support/player.cc index f9c855c9..8b79db19 100644 --- a/src/ballistica/scene_v1/support/player.cc +++ b/src/ballistica/scene_v1/support/player.cc @@ -4,10 +4,10 @@ #include "ballistica/base/input/device/joystick_input.h" #include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/python/class/python_class_session_player.h" #include "ballistica/scene_v1/support/host_activity.h" #include "ballistica/scene_v1/support/host_session.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" #include "ballistica/shared/generic/utils.h" @@ -374,7 +374,7 @@ void Player::SetName(const std::string& name, const std::string& full_name, // If we're already in the game and our name is changing, we need to update // the roster. if (accepted_) { - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->UpdateGameRoster(); } } diff --git a/src/ballistica/scene_v1/support/player_spec.cc b/src/ballistica/scene_v1/support/player_spec.cc index a589fdc2..3322464e 100644 --- a/src/ballistica/scene_v1/support/player_spec.cc +++ b/src/ballistica/scene_v1/support/player_spec.cc @@ -3,8 +3,8 @@ #include "ballistica/scene_v1/support/player_spec.h" #include "ballistica/base/support/classic_soft.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/core/platform/core_platform.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/shared/generic/json.h" #include "ballistica/shared/generic/utils.h" @@ -90,7 +90,7 @@ auto PlayerSpec::GetSpecString() const -> std::string { } auto PlayerSpec::GetAccountPlayerSpec() -> PlayerSpec { - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); PlayerSpec spec; if (g_base->HaveClassic() && g_base->classic()->IsV1AccountSignedIn()) { spec.v1_account_type_ = g_base->classic()->GetV1AccountType(); diff --git a/src/ballistica/scene_v1/support/scene.cc b/src/ballistica/scene_v1/support/scene.cc index f45b5de3..cef709e4 100644 --- a/src/ballistica/scene_v1/support/scene.cc +++ b/src/ballistica/scene_v1/support/scene.cc @@ -6,13 +6,13 @@ #include "ballistica/base/graphics/support/camera.h" #include "ballistica/base/networking/networking.h" #include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/assets/scene_sound.h" #include "ballistica/scene_v1/dynamics/dynamics.h" #include "ballistica/scene_v1/node/bomb_node.h" #include "ballistica/scene_v1/node/node_attribute_connection.h" #include "ballistica/scene_v1/node/player_node.h" #include "ballistica/scene_v1/node/text_node.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/scene_v1/support/session_stream.h" namespace ballistica::scene_v1 { @@ -133,7 +133,7 @@ auto Scene::GetPlayerNode(int id) -> PlayerNode* { void Scene::Step() { out_of_bounds_nodes_.clear(); - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); // Step all our nodes. { @@ -281,7 +281,7 @@ auto Scene::NewNode(const std::string& type_string, const std::string& name, void Scene::Dump(SessionStream* stream) { assert(g_base->InLogicThread()); - auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); stream->AddScene(this); // If we're the foreground one, communicate that fact as well. 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 0afe1ccf..7388ef7f 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 @@ -5,13 +5,13 @@ #include "ballistica/base/input/device/input_device.h" #include "ballistica/base/networking/networking.h" #include "ballistica/base/support/plus_soft.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/connection/connection_to_host.h" #include "ballistica/scene_v1/node/player_node.h" #include "ballistica/scene_v1/python/class/python_class_input_device.h" #include "ballistica/scene_v1/support/client_session_net.h" #include "ballistica/scene_v1/support/host_activity.h" #include "ballistica/scene_v1/support/host_session.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" #include "ballistica/shared/python/python.h" namespace ballistica::scene_v1 { @@ -37,7 +37,7 @@ std::optional SceneV1InputDeviceDelegate::GetPlayerPosition() { player_node = host_activity->scene()->GetPlayerNode(player->id()); } } else { - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { if (Scene* scene = appmode->GetForegroundScene()) { player_node = scene->GetPlayerNode(remote_player_id()); } @@ -57,7 +57,7 @@ auto SceneV1InputDeviceDelegate::AttachedToPlayer() const -> bool { void SceneV1InputDeviceDelegate::RequestPlayer() { assert(g_base->InLogicThread()); - auto* appmode = SceneV1AppMode::GetActive(); + auto* appmode = classic::ClassicAppMode::GetActive(); BA_PRECONDITION_FATAL(appmode); if (player_.Exists()) { @@ -192,7 +192,7 @@ void SceneV1InputDeviceDelegate::InputCommand(InputType type, float value) { void SceneV1InputDeviceDelegate::ShipBufferIfFull() { assert(remote_player_.Exists()); - auto* appmode = SceneV1AppMode::GetSingleton(); + auto* appmode = classic::ClassicAppMode::GetSingleton(); ConnectionToHost* hc = remote_player_.Get(); diff --git a/src/ballistica/scene_v1/support/session.cc b/src/ballistica/scene_v1/support/session.cc index 5b00bf0a..0371fda1 100644 --- a/src/ballistica/scene_v1/support/session.cc +++ b/src/ballistica/scene_v1/support/session.cc @@ -2,14 +2,14 @@ #include "ballistica/scene_v1/support/session.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/classic/support/classic_app_mode.h" namespace ballistica::scene_v1 { Session::Session() { g_scene_v1->session_count++; - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); // New sessions immediately become foreground. appmode->SetForegroundSession(this); diff --git a/src/ballistica/scene_v1/support/session_stream.cc b/src/ballistica/scene_v1/support/session_stream.cc index b2298514..ffd3094f 100644 --- a/src/ballistica/scene_v1/support/session_stream.cc +++ b/src/ballistica/scene_v1/support/session_stream.cc @@ -5,6 +5,7 @@ #include "ballistica/base/assets/assets_server.h" #include "ballistica/base/dynamics/bg/bg_dynamics.h" #include "ballistica/base/networking/networking.h" +#include "ballistica/classic/support/classic_app_mode.h" #include "ballistica/scene_v1/assets/scene_collision_mesh.h" #include "ballistica/scene_v1/assets/scene_data_asset.h" #include "ballistica/scene_v1/assets/scene_mesh.h" @@ -18,12 +19,11 @@ #include "ballistica/scene_v1/node/node_type.h" #include "ballistica/scene_v1/support/host_session.h" #include "ballistica/scene_v1/support/scene.h" -#include "ballistica/scene_v1/support/scene_v1_app_mode.h" namespace ballistica::scene_v1 { SessionStream::SessionStream(HostSession* host_session, bool save_replay) - : app_mode_{SceneV1AppMode::GetActiveOrThrow()}, + : app_mode_{classic::ClassicAppMode::GetActiveOrThrow()}, host_session_{host_session} { if (save_replay) { // Sanity check - we should only ever be writing one replay at once. @@ -42,7 +42,7 @@ SessionStream::SessionStream(HostSession* host_session, bool save_replay) // If we're the live output-stream from a host-session, // take responsibility for feeding all clients to this device. if (host_session_) { - auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); appmode->connections()->RegisterClientController(this); } } @@ -66,7 +66,7 @@ SessionStream::~SessionStream() { // If we're wired to the host-session, go ahead and release clients. if (host_session_) { - if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (auto* appmode = classic::ClassicAppMode::GetActiveOrWarn()) { appmode->connections()->UnregisterClientController(this); } @@ -431,7 +431,7 @@ void SessionStream::EndCommand(bool is_time_set) { // When attached to a host-session, send this message to clients if it's been // long enough. Also send off occasional correction packets. if (host_session_) { - auto* appmode = SceneV1AppMode::GetSingleton(); + 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 diff = real_time - last_send_time_; diff --git a/src/ballistica/scene_v1/support/session_stream.h b/src/ballistica/scene_v1/support/session_stream.h index 1bc3a831..4d97fe4f 100644 --- a/src/ballistica/scene_v1/support/session_stream.h +++ b/src/ballistica/scene_v1/support/session_stream.h @@ -7,6 +7,7 @@ #include #include "ballistica/base/base.h" +#include "ballistica/classic/classic.h" #include "ballistica/scene_v1/support/client_controller_interface.h" #include "ballistica/shared/foundation/object.h" @@ -138,7 +139,7 @@ class SessionStream : public Object, public ClientControllerInterface { std::vector out_message_; std::vector connections_to_clients_; std::vector connections_to_clients_ignored_; - SceneV1AppMode* app_mode_; + classic::ClassicAppMode* app_mode_; bool writing_replay_{}; millisecs_t last_physics_correction_time_{}; millisecs_t last_send_time_{}; diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index cb8d2beb..55bc92dd 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 = 21949; +const int kEngineBuildNumber = 21968; const char* kEngineVersion = "1.7.37"; const int kEngineApiVersion = 8; diff --git a/src/ballistica/shared/ballistica.h b/src/ballistica/shared/ballistica.h index 17831283..083ae9d7 100644 --- a/src/ballistica/shared/ballistica.h +++ b/src/ballistica/shared/ballistica.h @@ -41,10 +41,16 @@ const int kMaxPacketSize = 700; const int kMessagePacketHeaderSize = 6; // The screen, no matter what size/aspect, will always fit this virtual -// rectangle, so placing UI elements within these coords is always safe. (we -// currently match the screen ratio of an iPhone 5). -const int kBaseVirtualResX = 1207; -const int kBaseVirtualResY = 680; +// rectangle, so placing UI elements within these coords is always safe. + +// Our standard virtual res (16:9 aspect ratio). +const int kBaseVirtualResX = 1280; +const int kBaseVirtualResY = 720; + +// Our 'small' res which is used for 'small' ui mode only. This matches +// the 19.5:9 aspect ratio common on modern smartphones (as of 2024). +const int kBaseVirtualResSmallX = 1300; +const int kBaseVirtualResSmallY = 600; // Magic numbers at the start of our file types. const int kBrpFileID = 83749; 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 a04a1f00..765c719c 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 @@ -7,11 +7,11 @@ #include "ballistica/base/assets/sound_asset.h" #include "ballistica/base/platform/base_platform.h" #include "ballistica/base/python/base_python.h" +#include "ballistica/shared/foundation/macros.h" #include "ballistica/ui_v1/python/class/python_class_ui_mesh.h" #include "ballistica/ui_v1/python/class/python_class_ui_sound.h" #include "ballistica/ui_v1/python/class/python_class_ui_texture.h" #include "ballistica/ui_v1/python/ui_v1_python.h" -#include "ballistica/ui_v1/support/root_ui.h" #include "ballistica/ui_v1/widget/button_widget.h" #include "ballistica/ui_v1/widget/check_box_widget.h" #include "ballistica/ui_v1/widget/column_widget.h" @@ -1324,16 +1324,24 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, val = Widget::ToolbarVisibility::kMenuMinimal; } else if (sval == "menu_minimal_no_back") { val = Widget::ToolbarVisibility::kMenuMinimalNoBack; + } else if (sval == "menu_store") { + val = Widget::ToolbarVisibility::kMenuStore; + } else if (sval == "menu_store_no_back") { + val = Widget::ToolbarVisibility::kMenuStoreNoBack; + } else if (sval == "menu_in_game") { + val = Widget::ToolbarVisibility::kMenuInGame; + } else if (sval == "menu_tokens") { + val = Widget::ToolbarVisibility::kMenuTokens; } else if (sval == "menu_full") { val = Widget::ToolbarVisibility::kMenuFull; - } else if (sval == "menu_currency") { - val = Widget::ToolbarVisibility::kMenuCurrency; - } else if (sval == "menu_full_root") { - val = Widget::ToolbarVisibility::kMenuFullRoot; + } else if (sval == "menu_full_no_back") { + val = Widget::ToolbarVisibility::kMenuFullNoBack; } else if (sval == "in_game") { val = Widget::ToolbarVisibility::kInGame; } else if (sval == "inherit") { val = Widget::ToolbarVisibility::kInherit; + } else if (sval == "get_tokens") { + val = Widget::ToolbarVisibility::kGetTokens; } else { throw Exception("Invalid toolbar_visibility: '" + sval + "'.", PyExcType::kValue); @@ -1384,7 +1392,17 @@ static PyMethodDef PyContainerWidgetDef = { " always_highlight: bool | None = None,\n" " selectable: bool | None = None,\n" " scale_origin_stack_offset: Sequence[float] | None = None,\n" - " toolbar_visibility: str | None = None,\n" + " toolbar_visibility: Literal['menu_minimal',\n" + " 'menu_minimal_no_back',\n" + " 'menu_full',\n" + " 'menu_full_no_back',\n" + " 'menu_store',\n" + " 'menu_store_no_back',\n" + " 'menu_in_game',\n" + " 'menu_tokens',\n" + " 'get_tokens',\n" + " 'inherit',\n" + " ] | None = None,\n" " on_select_call: Callable[[], None] | None = None,\n" " claim_outside_clicks: bool | None = None,\n" " claims_up_down: bool | None = None) -> bauiv1.Widget\n" @@ -2339,9 +2357,10 @@ static auto PyWidgetCall(PyObject* self, PyObject* args, if (edit_obj != Py_None) { widget = UIV1Python::GetPyWidget(edit_obj); } - if (!widget) + if (!widget) { throw Exception("Invalid or nonexistent widget passed.", PyExcType::kWidgetNotFound); + } if (down_widget_obj != Py_None) { Widget* down_widget = UIV1Python::GetPyWidget(down_widget_obj); @@ -2399,7 +2418,7 @@ static PyMethodDef PyWidgetDef = { (PyCFunction)PyWidgetCall, // method METH_VARARGS | METH_KEYWORDS, // flags - "widget(edit: bauiv1.Widget | None = None,\n" + "widget(edit: bauiv1.Widget,\n" " up_widget: bauiv1.Widget | None = None,\n" " down_widget: bauiv1.Widget | None = None,\n" " left_widget: bauiv1.Widget | None = None,\n" @@ -2430,8 +2449,15 @@ auto PyUIBounds(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { assert(g_base->graphics); // Note: to be safe, we return our min guaranteed screen bounds; not our // current (which can be bigger). - float x = 0.5f * kBaseVirtualResX; - float virtual_res_y = kBaseVirtualResY; + float x, virtual_res_y; + + if (g_base->ui->scale() == UIScale::kSmall) { + x = 0.5f * kBaseVirtualResSmallX; + virtual_res_y = kBaseVirtualResSmallY; + } else { + x = 0.5f * kBaseVirtualResX; + virtual_res_y = kBaseVirtualResY; + } float y = 0.5f * virtual_res_y; return Py_BuildValue("(ffff)", -x, x, -y, y); BA_PYTHON_CATCH; @@ -2453,41 +2479,6 @@ static PyMethodDef PyUIBoundsDef = { "center remains onscreen.", }; -// --------------------- set_party_icon_always_visible ------------------------- - -static auto PySetPartyIconAlwaysVisible(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - - int value; - static const char* kwlist[] = {"value", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &value)) { - return nullptr; - } - BA_PRECONDITION(g_base->InLogicThread()); - assert(g_base); - assert(g_ui_v1); - auto* root_ui = g_ui_v1->root_ui(); - if (root_ui == nullptr) { - throw Exception("ui-v1 root ui not found."); - } - - root_ui->set_always_draw_party_icon(static_cast(value)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PySetPartyIconAlwaysVisibleDef = { - "set_party_icon_always_visible", // name - (PyCFunction)PySetPartyIconAlwaysVisible, // method - METH_VARARGS | METH_KEYWORDS, // flags - - "set_party_icon_always_visible(value: bool) -> None\n" - "\n" - "(internal)", -}; - // ------------------------ set_party_window_open ------------------------------ static auto PySetPartyWindowOpen(PyObject* self, PyObject* args, @@ -2502,12 +2493,13 @@ static auto PySetPartyWindowOpen(PyObject* self, PyObject* args, BA_PRECONDITION(g_base->InLogicThread()); assert(g_base->input); assert(g_ui_v1); - auto* root_ui = g_ui_v1->root_ui(); - if (root_ui == nullptr) { - throw Exception("ui-v1 root ui not found."); - } + printf("FIXME SET PARTY WINDOW OPEN\n"); + // auto* root_ui = g_ui_v1->root_ui(); + // if (root_ui == nullptr) { + // throw Exception("ui-v1 root ui not found."); + // } - root_ui->set_party_window_open(static_cast(value)); + // root_ui->set_party_window_open(static_cast(value)); Py_RETURN_NONE; BA_PYTHON_CATCH; } @@ -2552,33 +2544,44 @@ static PyMethodDef PyGetSpecialWidgetDef = { (PyCFunction)PyGetSpecialWidget, // method METH_VARARGS | METH_KEYWORDS, // flags - "get_special_widget(name: str) -> bauiv1.Widget\n" + "get_special_widget(name:\n" + " Literal[" + " 'squad_button'," + " 'back_button'," + " 'account_button'," + " 'achievements_button'," + " 'settings_button'," + " 'inbox_button'," + " 'store_button'," + " 'get_tokens_button'," + " 'inventory_button'," + " 'tickets_meter'," + " 'tokens_meter'," + " 'trophy_meter'," + " 'level_meter'," + " 'overlay_stack'," + " ]) -> bauiv1.Widget\n" "\n" "(internal)", }; -// ------------------------------ back_press ----------------------------------- +// -------------------------- root_ui_back_press ------------------------------- -static auto PyBackPress(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { +static auto PyRootUIBackPress(PyObject* self) -> PyObject* { BA_PYTHON_TRY; - - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - g_base->ui->PushBackButtonCall(nullptr); + BA_PRECONDITION(g_base->InLogicThread()); + RootWidget* root_widget = g_ui_v1->root_widget(); + BA_PRECONDITION(root_widget); + root_widget->BackPress(); Py_RETURN_NONE; BA_PYTHON_CATCH; } -static PyMethodDef PyBackPressDef = { - "back_press", // name - (PyCFunction)PyBackPress, // method - METH_VARARGS | METH_KEYWORDS, // flags - - "back_press() -> None\n" +static PyMethodDef PyRootUIBackPressDef = { + "root_ui_back_press", // name + (PyCFunction)PyRootUIBackPress, // method + METH_NOARGS, // flags + "root_ui_back_press() -> None\n" "\n" "(internal)", }; @@ -2589,8 +2592,11 @@ static auto PyIsPartyIconVisible(PyObject* self) -> PyObject* { BA_PYTHON_TRY; BA_PRECONDITION(g_base->InLogicThread()); bool party_button_active = (g_base->app_mode()->HasConnectionToClients() - || g_base->app_mode()->HasConnectionToHost() - || g_ui_v1->root_ui()->always_draw_party_icon()); + || g_base->app_mode()->HasConnectionToHost()); + // bool party_button_active = (g_base->app_mode()->HasConnectionToClients() + // || g_base->app_mode()->HasConnectionToHost() + // || + // g_ui_v1->root_ui()->always_draw_party_icon()); if (party_button_active) { Py_RETURN_TRUE; } else { @@ -2609,28 +2615,6 @@ static PyMethodDef PyIsPartyIconVisibleDef = { "(internal)", }; -// ----------------------------- toolbar_test ---------------------------------- - -static auto PyToolbarTest(PyObject* self) -> PyObject* { - BA_PYTHON_TRY; - if (BA_UI_V1_TOOLBAR_TEST) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -static PyMethodDef PyToolbarTestDef = { - "toolbar_test", // name - (PyCFunction)PyToolbarTest, // method - METH_NOARGS, // flags - - "toolbar_test() -> bool\n" - "\n" - "(internal)", -}; - // ----------------------------- is_available ---------------------------------- static auto PyIsAvailable(PyObject* self) -> PyObject* { @@ -2661,10 +2645,9 @@ static PyMethodDef PyIsAvailableDef = { auto PythonMethodsUIV1::GetMethods() -> std::vector { return { PyIsPartyIconVisibleDef, - PyBackPressDef, + PyRootUIBackPressDef, PyGetSpecialWidgetDef, PySetPartyWindowOpenDef, - PySetPartyIconAlwaysVisibleDef, PyButtonWidgetDef, PyCheckBoxWidgetDef, PyImageWidgetDef, @@ -2680,7 +2663,6 @@ auto PythonMethodsUIV1::GetMethods() -> std::vector { PyGetTextureDef, PyGetQRCodeTextureDef, PyGetMeshDef, - PyToolbarTestDef, PyIsAvailableDef, }; } diff --git a/src/ballistica/ui_v1/python/ui_v1_python.h b/src/ballistica/ui_v1/python/ui_v1_python.h index dadc4524..5a3f8c95 100644 --- a/src/ballistica/ui_v1/python/ui_v1_python.h +++ b/src/ballistica/ui_v1/python/ui_v1_python.h @@ -27,14 +27,25 @@ class UIV1Python { /// Specific Python objects we hold in objs_. enum class ObjID { kOnScreenKeyboardClass, - kTicketIconPressCall, - kTrophyIconPressCall, - kLevelIconPressCall, - kCoinIconPressCall, + kRootUITicketIconPressCall, + kRootUIGetTokensButtonPressCall, + kRootUIAccountButtonPressCall, + kRootUIInboxButtonPressCall, + kRootUISettingsButtonPressCall, + kRootUIAchievementsButtonPressCall, + kRootUIStoreButtonPressCall, + kRootUIChestSlot1PressCall, + kRootUIChestSlot2PressCall, + kRootUIChestSlot3PressCall, + kRootUIChestSlot4PressCall, + kRootUIInventoryButtonPressCall, + kRootUITrophyMeterPressCall, + kRootUILevelIconPressCall, + kRootUITokensMeterPressCall, kEmptyCall, - kBackButtonPressCall, - kFriendsButtonPressCall, - kPartyIconActivateCall, + kRootUIMenuButtonPressCall, + kRootUIBackButtonPressCall, + kRootUISquadButtonPressCall, kQuitWindowCall, kDeviceMenuPressCall, kShowURLWindowCall, diff --git a/src/ballistica/ui_v1/support/root_ui.cc b/src/ballistica/ui_v1/support/root_ui.cc deleted file mode 100644 index 255b56b2..00000000 --- a/src/ballistica/ui_v1/support/root_ui.cc +++ /dev/null @@ -1,407 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/ui_v1/support/root_ui.h" - -#include "ballistica/base/app_mode/app_mode.h" -#include "ballistica/base/graphics/component/simple_component.h" -#include "ballistica/base/input/device/keyboard_input.h" -#include "ballistica/base/input/device/touch_input.h" -#include "ballistica/base/input/input.h" -#include "ballistica/ui_v1/python/ui_v1_python.h" -#include "ballistica/ui_v1/widget/container_widget.h" - -namespace ballistica::ui_v1 { - -// Phasing these out; replaced by buttons in our rootwidget. -#define DO_OLD_MENU_PARTY_BUTTONS (!BA_UI_V1_TOOLBAR_TEST) - -const float kMenuButtonSize = 40.0f; -const float kMenuButtonDrawDepth = -0.07f; - -RootUI::RootUI() { - float base_scale; - switch (g_base->ui->scale()) { - case UIScale::kLarge: - base_scale = 1.0f; - break; - case UIScale::kMedium: - base_scale = 1.5f; - break; - case UIScale::kSmall: - base_scale = 2.0f; - break; - default: - base_scale = 1.0f; - break; - } - menu_button_size_ = kMenuButtonSize * base_scale; -} - -RootUI::~RootUI() = default; - -void RootUI::TogglePartyWindowKeyPress() { - assert(g_base->InLogicThread()); - if (g_base->app_mode()->GetPartySize() > 1 - || g_base->app_mode()->HasConnectionToHost() - || always_draw_party_icon()) { - ActivatePartyIcon(); - } -} - -void RootUI::ActivatePartyIcon() const { - assert(g_base->InLogicThread()); - base::ScopedSetContext ssc(nullptr); - - // Originate from center of party icon. If menu button is shown, it is to the - // left of that. - float icon_pos_h = g_base->graphics->screen_virtual_width() * 0.5f - - menu_button_size_ * 0.5f; - float icon_pos_v = g_base->graphics->screen_virtual_height() * 0.5f - - menu_button_size_ * 0.5f; - bool menu_active = !(g_ui_v1 && g_ui_v1->screen_root_widget() - && g_ui_v1->screen_root_widget()->HasChildren()); - if (menu_active) { - icon_pos_h -= menu_button_size_; - } - g_ui_v1->python->objs() - .Get(UIV1Python::ObjID::kPartyIconActivateCall) - .Call(Vector2f(icon_pos_h, icon_pos_v)); -} - -auto RootUI::HandleMouseButtonDown(float x, float y) -> bool { - // Whether the menu button is visible/active. - bool menu_active = !(g_ui_v1 && g_ui_v1->screen_root_widget() - && g_ui_v1->screen_root_widget()->HasChildren()); - - // Handle party button presses (need to do this before UI since it - // floats over the top). Party button is to the left of menu button. - if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) { - bool party_button_active = (!party_window_open_ - && (g_base->app_mode()->HasConnectionToClients() - || g_base->app_mode()->HasConnectionToHost() - || always_draw_party_icon())); - float party_button_left = - menu_active ? 2 * menu_button_size_ : menu_button_size_; - float party_button_right = menu_active ? menu_button_size_ : 0; - if (party_button_active - && (g_base->graphics->screen_virtual_width() - x < party_button_left) - && (g_base->graphics->screen_virtual_width() - x >= party_button_right) - && (g_base->graphics->screen_virtual_height() - y - < menu_button_size_)) { - ActivatePartyIcon(); - return true; - } - } - // Menu button. - if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) { - if (menu_active - && (g_base->graphics->screen_virtual_width() - x < menu_button_size_) - && (g_base->graphics->screen_virtual_height() - y - < menu_button_size_)) { - menu_button_pressed_ = true; - menu_button_hover_ = true; - return true; - } - } - - return false; -} - -void RootUI::HandleMouseButtonUp(float x, float y) { - if (menu_button_pressed_) { - menu_button_pressed_ = false; - menu_button_hover_ = false; - - // If we've got a touch input, bring the menu up in its name.. - // otherwise go with keyboard input. - base::InputDevice* input_device = nullptr; - auto* touch_input = g_base->input->touch_input(); - auto* keyboard_input = g_base->input->keyboard_input(); - if (touch_input) { - input_device = touch_input; - } else if (keyboard_input) { - input_device = keyboard_input; - } - - // Handle top right corner menu button. - if ((g_base->graphics->screen_virtual_width() - x < menu_button_size_) - && (g_base->graphics->screen_virtual_height() - y - < menu_button_size_)) { - g_base->ui->PushMainMenuPressCall(input_device); - last_menu_button_press_time_ = g_core->GetAppTimeMillisecs(); - } - } -} - -void RootUI::HandleMouseMotion(float x, float y) { - // Menu button hover. - if (menu_button_pressed_) { - menu_button_hover_ = - ((g_base->graphics->screen_virtual_width() - x < menu_button_size_) - && (g_base->graphics->screen_virtual_height() - y - < menu_button_size_)); - } -} - -void RootUI::Draw(base::FrameDef* frame_def) { - if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) { - millisecs_t real_time = frame_def->app_time_millisecs(); - - // Menu button. - // Update time-dependent stuff to this point. - bool active = !(g_ui_v1 && g_ui_v1->screen_root_widget() - && g_ui_v1->screen_root_widget()->HasChildren()); - if (real_time - menu_update_time_ > 500) { - menu_update_time_ = real_time - 500; - } - while (menu_update_time_ < real_time) { - menu_update_time_ += 10; - if (!active && (real_time - last_menu_button_press_time_ > 100)) { - menu_fade_ = std::max(0.0f, menu_fade_ - 0.05f); - } else { - menu_fade_ = std::min(1.0f, menu_fade_ + 0.05f); - } - } - - // Don't draw menu button on certain UIs such as TV or VR. - bool draw_menu_button = true; - - if (g_buildconfig.ostype_android()) { - // Draw if we have a touchscreen or are in desktop mode. - if (g_base->input->touch_input() == nullptr - && !g_core->platform->IsRunningOnDesktop()) { - draw_menu_button = false; - } - } else if (g_buildconfig.rift_build()) { - if (g_core->vr_mode()) { - draw_menu_button = false; - } - } - - if (draw_menu_button) { - base::SimpleComponent c(frame_def->overlay_pass()); - c.SetTransparent(true); - c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kMenuButton)); - - // Draw menu button. - float width = g_base->graphics->screen_virtual_width(); - float height = g_base->graphics->screen_virtual_height(); - if ((menu_button_pressed_ && menu_button_hover_) - || real_time - last_menu_button_press_time_ < 100) { - c.SetColor(1, 2, 0.5f, 1); - } else { - c.SetColor(0.3f, 0.3f + 0.2f * menu_fade_, 0.2f, menu_fade_); - } - { - auto xf = c.ScopedTransform(); - c.Translate(width - menu_button_size_ * 0.5f, - height - menu_button_size_ * 0.38f, kMenuButtonDrawDepth); - c.Scale(menu_button_size_ * 0.8f, menu_button_size_ * 0.8f); - c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); - } - c.Submit(); - } - - // To the left of the menu button, draw our connected-players indicator - // (this probably shouldn't live here). - bool draw_connected_players_icon = false; - int party_size = g_base->app_mode()->GetPartySize(); - bool is_host = (!g_base->app_mode()->HasConnectionToHost()); - millisecs_t last_connection_to_client_join_time = - g_base->app_mode()->LastClientJoinTime(); - - bool show_client_joined = - (is_host && last_connection_to_client_join_time != 0 - && real_time - last_connection_to_client_join_time < 5000); - - if (!party_window_open_ - && (party_size != 0 || g_base->app_mode()->HasConnectionToHost() - || always_draw_party_icon_)) { - draw_connected_players_icon = true; - } - - if (draw_connected_players_icon) { - // Flash and show a message if we're in the main menu instructing the - // player to start a game. - bool flash = false; - bool in_main_menu = g_base->app_mode()->InClassicMainMenuSession(); - - if (in_main_menu && party_size > 0 && show_client_joined) flash = true; - - base::SimpleComponent c(frame_def->overlay_pass()); - c.SetTransparent(true); - c.SetTexture( - g_base->assets->SysTexture(base::SysTextureID::kUsersButton)); - - // Draw button. - float width = g_base->graphics->screen_virtual_width(); - float height = g_base->graphics->screen_virtual_height(); - { - auto xf = c.ScopedTransform(); - - float extra_offset = - (draw_menu_button && menu_fade_ > 0.0f) ? -menu_button_size_ : 0.0f; - { - float smoothing = 0.8f; - connected_client_extra_offset_smoothed_ = - smoothing * connected_client_extra_offset_smoothed_ - + (1.0f - smoothing) * extra_offset; - } - c.Translate(width - menu_button_size_ * 0.4f - + connected_client_extra_offset_smoothed_, - height - menu_button_size_ * 0.35f, kMenuButtonDrawDepth); - c.Scale(menu_button_size_ * 0.8f, menu_button_size_ * 0.8f); - if (flash && frame_def->display_time_millisecs() % 250 < 125) { - c.SetColor(1.0f, 1.4f, 1.0f); - } - c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); - } - c.Submit(); - - // Based on who has menu control, we may show a key/button below the - // party icon. - if (!active) { - if (base::InputDevice* uiid = g_base->ui->GetUIInputDevice()) { - std::string party_button_name = uiid->GetPartyButtonName(); - if (!party_button_name.empty()) { - if (!party_button_text_group_.Exists()) { - party_button_text_group_ = Object::New(); - } - if (party_button_name != party_button_text_group_->text()) { - party_button_text_group_->SetText(party_button_name, - base::TextMesh::HAlign::kCenter, - base::TextMesh::VAlign::kTop); - } - int text_elem_count = party_button_text_group_->GetElementCount(); - for (int e = 0; e < text_elem_count; e++) { - c.SetTexture(party_button_text_group_->GetElementTexture(e)); - c.SetMaskUV2Texture( - party_button_text_group_->GetElementMaskUV2Texture(e)); - c.SetShadow( - -0.003f * party_button_text_group_->GetElementUScale(e), - -0.003f * party_button_text_group_->GetElementVScale(e), 0.0f, - 1.0f); - c.SetFlatness(1.0f); - c.SetColor(0.8f, 1, 0.8f, 0.9f); - { - auto xf = c.ScopedTransform(); - c.Translate(width - menu_button_size_ * 0.42f - + connected_client_extra_offset_smoothed_, - height - menu_button_size_ * 0.77f, - kMenuButtonDrawDepth); - c.Scale(menu_button_size_ * 0.015f, menu_button_size_ * 0.015f); - c.DrawMesh(party_button_text_group_->GetElementMesh(e)); - } - } - c.Submit(); - } - } - } - - { - // Update party count text if party size has changed. - if (party_size_text_group_num_ != party_size) { - party_size_text_group_num_ = party_size; - if (!party_size_text_group_.Exists()) { - party_size_text_group_ = Object::New(); - } - party_size_text_group_->SetText( - std::to_string(party_size_text_group_num_)); - - // ..we also may want to update our 'someone joined' message if - // we're host - if (is_host) { - if (!start_a_game_text_group_.Exists()) { - start_a_game_text_group_ = Object::New(); - } - if (party_size == 2) { // (includes us as host) - start_a_game_text_group_->SetText( - g_base->assets->GetResourceString( - "joinedPartyInstructionsText"), - base::TextMesh::HAlign::kRight, base::TextMesh::VAlign::kTop); - } else if (party_size > 2) { - start_a_game_text_group_->SetText( - std::to_string(party_size - 1) + - " friends have joined your party.\nGo to 'Play' to start " - "a game.", - base::TextMesh::HAlign::kRight, base::TextMesh::VAlign::kTop); - } - } - } - - // Draw party member count. - int text_elem_count = party_size_text_group_->GetElementCount(); - for (int e = 0; e < text_elem_count; e++) { - c.SetTexture(party_size_text_group_->GetElementTexture(e)); - c.SetMaskUV2Texture( - party_size_text_group_->GetElementMaskUV2Texture(e)); - c.SetShadow(-0.003f * party_size_text_group_->GetElementUScale(e), - -0.003f * party_size_text_group_->GetElementVScale(e), - 0.0f, 1.0f); - c.SetFlatness(1.0f); - if (flash && frame_def->display_time_millisecs() % 250 < 125) { - c.SetColor(1, 1, 0); - } else { - if (party_size > 0) { - c.SetColor(0.2f, 1.0f, 0.2f); - } else { - c.SetColor(0.5f, 0.65f, 0.5f); - } - } - { - auto xf = c.ScopedTransform(); - c.Translate(width - menu_button_size_ * 0.49f - + connected_client_extra_offset_smoothed_, - height - menu_button_size_ * 0.6f, - kMenuButtonDrawDepth); - c.Scale(menu_button_size_ * 0.01f, menu_button_size_ * 0.01f); - c.DrawMesh(party_size_text_group_->GetElementMesh(e)); - } - } - c.Submit(); - } - - // Draw 'someone joined' text if applicable. - if (is_host) { - if (flash) { - float blend = 0.8f; - start_a_game_text_scale_ = - blend * start_a_game_text_scale_ + (1.0f - blend) * 1.0f; - } else { - float blend = 0.8f; - start_a_game_text_scale_ = - blend * start_a_game_text_scale_ + (1.0f - blend) * 0.0f; - } - - if (start_a_game_text_scale_ > 0.001f) { - // 'start a game' notice - int text_elem_count = start_a_game_text_group_->GetElementCount(); - for (int e = 0; e < text_elem_count; e++) { - c.SetTexture(start_a_game_text_group_->GetElementTexture(e)); - c.SetMaskUV2Texture( - start_a_game_text_group_->GetElementMaskUV2Texture(e)); - c.SetShadow(-0.003f * start_a_game_text_group_->GetElementUScale(e), - -0.003f * start_a_game_text_group_->GetElementVScale(e), - 0.0f, 1.0f); - c.SetFlatness(1.0f); - if (flash && frame_def->display_time_millisecs() % 250 < 125) { - c.SetColor(1, 1, 0); - } else { - c.SetColor(0, 1, 0); - } - { - auto xf = c.ScopedTransform(); - c.Translate(width - 10, height - menu_button_size_ * 0.7f, - -0.07f); - c.Scale(start_a_game_text_scale_, start_a_game_text_scale_); - c.DrawMesh(start_a_game_text_group_->GetElementMesh(e)); - } - } - c.Submit(); - } - } - } - } -} - -} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/support/root_ui.h b/src/ballistica/ui_v1/support/root_ui.h deleted file mode 100644 index 548af0c4..00000000 --- a/src/ballistica/ui_v1/support/root_ui.h +++ /dev/null @@ -1,49 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_UI_V1_SUPPORT_ROOT_UI_H_ -#define BALLISTICA_UI_V1_SUPPORT_ROOT_UI_H_ - -#include "ballistica/base/graphics/support/frame_def.h" - -namespace ballistica::ui_v1 { - -/// Manages root level UI such as the menu button, party button, etc. -/// This is set to be replaced by RootWidget. -class RootUI { - public: - RootUI(); - virtual ~RootUI(); - void Draw(base::FrameDef* frame_def); - - auto HandleMouseButtonDown(float x, float y) -> bool; - void HandleMouseButtonUp(float x, float y); - void HandleMouseMotion(float x, float y); - void set_party_window_open(bool val) { party_window_open_ = val; } - auto party_window_open() const -> bool { return party_window_open_; } - void set_always_draw_party_icon(bool val) { always_draw_party_icon_ = val; } - auto always_draw_party_icon() const -> bool { - return always_draw_party_icon_; - } - void TogglePartyWindowKeyPress(); - void ActivatePartyIcon() const; - - private: - millisecs_t last_menu_button_press_time_{}; - millisecs_t menu_update_time_{}; - bool menu_button_pressed_{}; - float menu_button_size_{}; - bool menu_button_hover_{}; - float menu_fade_{}; - bool party_window_open_{}; - bool always_draw_party_icon_{}; - float connected_client_extra_offset_smoothed_{}; - Object::Ref party_button_text_group_; - Object::Ref party_size_text_group_; - int party_size_text_group_num_{-1}; - Object::Ref start_a_game_text_group_; - float start_a_game_text_scale_{}; -}; - -} // namespace ballistica::ui_v1 - -#endif // BALLISTICA_UI_V1_SUPPORT_ROOT_UI_H_ diff --git a/src/ballistica/ui_v1/ui_v1.cc b/src/ballistica/ui_v1/ui_v1.cc index c26c9ffc..0bc3846f 100644 --- a/src/ballistica/ui_v1/ui_v1.cc +++ b/src/ballistica/ui_v1/ui_v1.cc @@ -7,7 +7,6 @@ #include "ballistica/base/input/input.h" #include "ballistica/base/support/app_config.h" #include "ballistica/ui_v1/python/ui_v1_python.h" -#include "ballistica/ui_v1/support/root_ui.h" #include "ballistica/ui_v1/widget/root_widget.h" #include "ballistica/ui_v1/widget/stack_widget.h" @@ -81,46 +80,31 @@ bool UIV1FeatureSet::MainMenuVisible() { } bool UIV1FeatureSet::PartyIconVisible() { - int party_size = g_base->app_mode()->GetPartySize(); - if (party_size > 1 || g_base->app_mode()->HasConnectionToHost() - || root_ui()->always_draw_party_icon()) { - return true; - } + printf("FIXME HANDLE PARTY ICON VISIBLE\n"); return false; + // int party_size = g_base->app_mode()->GetPartySize(); + // if (party_size > 1 || g_base->app_mode()->HasConnectionToHost() + // || root_ui()->always_draw_party_icon()) { + // return true; + // } + // return false; } void UIV1FeatureSet::ActivatePartyIcon() { - if (auto* r = root_ui()) { - r->ActivatePartyIcon(); - } + printf("FIXME HANDLE ACTIVATE PARTY ICON\n"); + // if (auto* r = root_ui()) { + // r->ActivatePartyIcon(); + // } } bool UIV1FeatureSet::PartyWindowOpen() { - if (auto* r = root_ui()) { - return r->party_window_open(); - } + printf("FIXME HANDLE PARTY WINDOW OPEN\n"); + // if (auto* r = root_ui()) { + // return r->party_window_open(); + // } return false; } -void UIV1FeatureSet::HandleLegacyRootUIMouseMotion(float x, float y) { - if (auto* r = root_ui()) { - r->HandleMouseMotion(x, y); - } -} - -auto UIV1FeatureSet::HandleLegacyRootUIMouseDown(float x, float y) -> bool { - if (auto* r = root_ui()) { - return r->HandleMouseButtonDown(x, y); - } - return false; -} - -void UIV1FeatureSet::HandleLegacyRootUIMouseUp(float x, float y) { - if (auto* r = root_ui()) { - r->HandleMouseButtonUp(x, y); - } -} - void UIV1FeatureSet::Draw(base::FrameDef* frame_def) { base::RenderPass* overlay_flat_pass = frame_def->GetOverlayFlatPass(); @@ -170,18 +154,9 @@ void UIV1FeatureSet::Draw(base::FrameDef* frame_def) { g_base->graphics->set_drawing_transparent_only(false); } - - if (auto* r = root_ui()) { - r->Draw(frame_def); - } } -void UIV1FeatureSet::OnActivate() { - assert(g_base->InLogicThread()); - if (root_ui_ == nullptr) { - root_ui_ = new RootUI(); - } -} +void UIV1FeatureSet::OnActivate() { assert(g_base->InLogicThread()); } void UIV1FeatureSet::OnDeactivate() { assert(g_base->InLogicThread()); } void UIV1FeatureSet::Reset() { @@ -221,11 +196,11 @@ void UIV1FeatureSet::AddWidget(Widget* w, ContainerWidget* parent) { BA_PRECONDITION(parent != nullptr); - // If they're adding an initial window/dialog to our screen-stack - // or overlay stack, send a reset-local-input message so that characters - // who have lost focus will not get stuck running or whatnot. - // We should come up with a more generalized way to track this sort of - // focus as this is a bit hacky, but it works for now. + // If they're adding an initial window/dialog to our screen-stack or + // overlay stack, send a reset-local-input message so that characters who + // have lost focus will not get stuck running or whatnot. We should come + // up with a more generalized way to track this sort of focus as this is a + // bit hacky, but it works for now. auto* screen_root_widget = screen_root_widget_.Get(); auto* overlay_root_widget = overlay_root_widget_.Get(); if ((screen_root_widget && !screen_root_widget->HasChildren() diff --git a/src/ballistica/ui_v1/ui_v1.h b/src/ballistica/ui_v1/ui_v1.h index 79994412..d962d6ae 100644 --- a/src/ballistica/ui_v1/ui_v1.h +++ b/src/ballistica/ui_v1/ui_v1.h @@ -12,9 +12,6 @@ // It predeclares our feature-set's various types and globals and other // bits. -// BA 2.0 UI testing. -#define BA_UI_V1_TOOLBAR_TEST 0 - // UI-Locks: make sure widget-lists don't change under you. Use a read-lock // if you just need to ensure lists remain intact but won't be changing // anything. Use a write-lock whenever modifying a list. @@ -88,17 +85,17 @@ class UIV1FeatureSet : public FeatureSetNativeComponent, auto MainMenuVisible() -> bool override; auto PartyIconVisible() -> bool override; void ActivatePartyIcon() override; - void HandleLegacyRootUIMouseMotion(float x, float y) override; - auto HandleLegacyRootUIMouseDown(float x, float y) -> bool override; - void HandleLegacyRootUIMouseUp(float x, float y) override; + // void HandleLegacyRootUIMouseMotion(float x, float y) override; + // auto HandleLegacyRootUIMouseDown(float x, float y) -> bool override; + // void HandleLegacyRootUIMouseUp(float x, float y) override; void Draw(base::FrameDef* frame_def) override; UIV1Python* const python; - auto root_ui() const -> ui_v1::RootUI* { - assert(root_ui_); - return root_ui_; - } + // auto root_ui() const -> ui_v1::RootUI* { + // assert(root_ui_); + // return root_ui_; + // } // void OnAppStart() override; void OnActivate() override; void OnDeactivate() override; @@ -140,7 +137,7 @@ class UIV1FeatureSet : public FeatureSetNativeComponent, private: UIV1FeatureSet(); - RootUI* root_ui_{}; + // RootUI* root_ui_{}; Object::Ref screen_root_widget_; Object::Ref overlay_root_widget_; Object::Ref root_widget_; diff --git a/src/ballistica/ui_v1/widget/button_widget.cc b/src/ballistica/ui_v1/widget/button_widget.cc index a5906567..ed243ef9 100644 --- a/src/ballistica/ui_v1/widget/button_widget.cc +++ b/src/ballistica/ui_v1/widget/button_widget.cc @@ -471,6 +471,10 @@ auto ButtonWidget::HandleMessage(const base::WidgetMessage& m) -> bool { bottom_overlap = 7.0f * extra_touch_border_scale_; } + // Extra overlap that always applies. + right_overlap += target_extra_right_; + left_overlap += target_extra_left_; + switch (m.type) { case base::WidgetMessage::Type::kMouseMove: { float x = m.fval1; @@ -565,8 +569,17 @@ void ButtonWidget::DoActivate(bool is_repeat) { } } if (auto* call = on_activate_call_.Get()) { - // Schedule this to run immediately after any current UI traversal. - call->ScheduleInUIOperation(); + // If we're being activated as part of a ui-operation (a click or other + // such event) then run at the end of that operation to avoid mucking + // with volatile UI. + if (g_base->ui->InUIOperation()) { + call->ScheduleInUIOperation(); + } else { + // Ok, we're *not* in a ui-operation. This generally means we're + // being activated explicitly via a Python call or whatnot. Just + // run immediately in this case. + call->Run(); + } return; } } diff --git a/src/ballistica/ui_v1/widget/button_widget.h b/src/ballistica/ui_v1/widget/button_widget.h index 1928c41d..e6d1f510 100644 --- a/src/ballistica/ui_v1/widget/button_widget.h +++ b/src/ballistica/ui_v1/widget/button_widget.h @@ -86,6 +86,9 @@ class ButtonWidget : public Widget { auto is_color_set() const -> bool { return color_set_; } void OnLanguageChange() override; + auto set_target_extra_left(float val) { target_extra_left_ = val; } + auto set_target_extra_right(float val) { target_extra_right_ = val; } + private: bool text_width_dirty_ = true; bool color_set_ = false; @@ -129,6 +132,8 @@ class ButtonWidget : public Widget { float tint2_color_red_{1.0f}; float tint2_color_green_{1.0f}; float tint2_color_blue_{1.0f}; + float target_extra_right_{0.0f}; + float target_extra_left_{0.0f}; Object::Ref texture_; Object::Ref icon_; Object::Ref tint_texture_; diff --git a/src/ballistica/ui_v1/widget/container_widget.cc b/src/ballistica/ui_v1/widget/container_widget.cc index aaacb1e2..a2934beb 100644 --- a/src/ballistica/ui_v1/widget/container_widget.cc +++ b/src/ballistica/ui_v1/widget/container_widget.cc @@ -346,7 +346,8 @@ auto ContainerWidget::HandleMessage(const base::WidgetMessage& m) -> bool { } else if (auto* call = on_cancel_call_.Get()) { claimed = true; - // Schedule this to run immediately after any current UI traversal. + // Schedule this to run immediately after any current UI + // traversal. call->ScheduleInUIOperation(); } else { OnCancelCustom(); diff --git a/src/ballistica/ui_v1/widget/root_widget.cc b/src/ballistica/ui_v1/widget/root_widget.cc index 51c34b84..33bac53b 100644 --- a/src/ballistica/ui_v1/widget/root_widget.cc +++ b/src/ballistica/ui_v1/widget/root_widget.cc @@ -4,15 +4,16 @@ #include "ballistica/base/app_mode/app_mode.h" #include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/shared/foundation/inline.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" #include "ballistica/ui_v1/widget/stack_widget.h" namespace ballistica::ui_v1 { -// color we mult toolbars by in medium and large ui modes -// (in small mode we keep them more the normal window color since everything -// overlaps) +// Color we mult toolbars by in medium and large ui modes (in small mode we +// keep them more the normal window color since everything overlaps). #define TOOLBAR_COLOR_R 0.75f #define TOOLBAR_COLOR_G 0.85f #define TOOLBAR_COLOR_B 0.85f @@ -21,20 +22,28 @@ namespace ballistica::ui_v1 { #define TOOLBAR_BACK_COLOR_G 0.8f #define TOOLBAR_BACK_COLOR_B 0.8f -// opacity in med/large +// Opacity in med/large. #define TOOLBAR_OPACITY 1.0f -// opacity in small +// Opacity in small. #define TOOLBAR_OPACITY_2 1.0f #define BOT_LEFT_COLOR_R 0.6 #define BOT_LEFT_COLOR_G 0.6 #define BOT_LEFT_COLOR_B 0.8 -// for defining toolbar buttons. +// For defining toolbar buttons. struct RootWidget::ButtonDef { - float h_align{}; + std::string label; + std::string img; + std::string mesh_transparent; + std::string mesh_opaque; VAlign v_align{VAlign::kTop}; + UIV1Python::ObjID call{UIV1Python::ObjID::kEmptyCall}; + uint32_t visibility_mask{}; + bool selectable{true}; + bool enable_sound{true}; + float h_align{}; float x{}; float y{}; float width{100.0f}; @@ -42,17 +51,15 @@ struct RootWidget::ButtonDef { float scale{1.0f}; float depth_min{}; float depth_max{1.0f}; - std::string label; - std::string img; - std::string mesh_transparent; - std::string mesh_opaque; - UIV1Python::ObjID call{UIV1Python::ObjID::kEmptyCall}; float color_r{1.0f}; float color_g{1.0f}; float color_b{1.0f}; float opacity{1.0f}; - bool selectable{true}; - uint32_t visibility_mask{}; + float disable_offset_scale{1.0f}; + float target_extra_left{0.0f}; + float target_extra_right{0.0f}; + float pre_buffer{0.0f}; + float post_buffer{0.0f}; }; struct RootWidget::Button { @@ -68,25 +75,30 @@ struct RootWidget::Button { float width{100.0f}; float height{30.0f}; float scale{1.0f}; + float disable_offset_scale{1.0f}; + float pre_buffer{0.0f}; + float post_buffer{0.0f}; bool selectable{true}; + bool fully_offscreen{}; + bool enabled{}; uint32_t visibility_mask{}; }; -// for adding text label decorations to buttons +// For adding text label decorations to buttons. struct RootWidget::TextDef { - Button* button = nullptr; - float x = 0.0f; - float y = 0.0f; - float width = -1.0f; - float scale = 1.0f; - float depth_min = 0.0f; - float depth_max = 1.0f; - float color_r = 1.0f; - float color_g = 1.0f; - float color_b = 1.0f; - float color_a = 1.0f; - float flatness = 0.5f; - float shadow = 0.5f; + Button* button{}; + float x{}; + float y{}; + float width{-1.0f}; + float scale{1.0f}; + float depth_min{}; + float depth_max{1.0f}; + float color_r{1.0f}; + float color_g{1.0f}; + float color_b{1.0f}; + float color_a{1.0f}; + float flatness{0.5f}; + float shadow{0.5f}; std::string text; }; @@ -97,6 +109,24 @@ struct RootWidget::Text { float y{}; }; +struct RootWidget::ImageDef { + Button* button{}; + float x{}; + float y{}; + float width{32.0f}; + float height{32.0f}; + float depth_min{}; + float depth_max{1.0f}; + std::string img; +}; + +struct RootWidget::Image { + Button* button{}; + Object::Ref widget; + float x{}; + float y{}; +}; + RootWidget::RootWidget() { // We enable a special 'single-depth-root' mode in which we use most of // our depth range for our first child (our screen stack) and the small @@ -108,8 +138,8 @@ RootWidget::RootWidget() { RootWidget::~RootWidget() = default; -auto RootWidget::AddCover(float h_align, VAlign v_align, float x, float y, - float w, float h, float o) -> RootWidget::Button* { +auto RootWidget::AddCover_(float h_align, VAlign v_align, float x, float y, + float w, float h, float o) -> RootWidget::Button* { // Currently just not doing these in vr mode. if (g_core->vr_mode()) { return nullptr; @@ -132,58 +162,155 @@ auto RootWidget::AddCover(float h_align, VAlign v_align, float x, float y, bd.visibility_mask = static_cast(Widget::ToolbarVisibility::kMenuFullRoot); + // When the user specifies no backing it means they intend to cover the - // screen with a flat-ish window texture.. however this only applies to + // screen with a flat-ish window texture. However this only applies to // phone-size; for other sizes we always draw a backing. + // + // UPDATE: We no longer do backings, so ignore that. if (g_base->ui->scale() != UIScale::kSmall) { bd.visibility_mask |= static_cast(Widget::ToolbarVisibility::kMenuFull); } - Button* b = AddButton(bd); + Button* b = AddButton_(bd); return b; } -#pragma clang diagnostic push -#pragma ide diagnostic ignored "ConstantParameter" - -void RootWidget::AddMeter(float h_align, float x, int type, float r, float g, - float b, bool plus, const std::string& s) { +void RootWidget::AddMeter_(MeterType type, float h_align, float r, float g, + float b, bool plus, const std::string& s) { float yoffs = (g_base->ui->scale() == UIScale::kSmall) ? 0.0f : -7.0f; - float width = type == 1 ? 80.0f : 110.0f; - // bar + float width = (type == MeterType::kTrophy) ? 80.0f : 110.0f; + width = 110.0f; + + // Bar. { ButtonDef bd; bd.h_align = h_align; bd.v_align = VAlign::kTop; bd.width = width; bd.height = 36.0f; - bd.x = x; + // bd.x = x; bd.y = -36.0f + 10.0f + yoffs; bd.img = "uiAtlas2"; bd.mesh_transparent = "currencyMeter"; - bd.selectable = false; - bd.color_r = 0.32f; - bd.color_g = 0.30f; - bd.color_b = 0.4f; - if (g_base->ui->scale() != UIScale::kSmall) { - bd.color_r *= TOOLBAR_COLOR_R; - bd.color_g *= TOOLBAR_COLOR_G; - bd.color_b *= TOOLBAR_COLOR_B; - } + bd.selectable = true; + + // bd.color_r = 0.32f; + // bd.color_g = 0.30f; + // bd.color_b = 0.4f; + + bd.color_r = 0.4f; + bd.color_g = 0.38f; + bd.color_b = 0.5f; + + // if (g_base->ui->scale() != UIScale::kSmall) { + // bd.color_r *= TOOLBAR_COLOR_R; + // bd.color_g *= TOOLBAR_COLOR_G; + // bd.color_b *= TOOLBAR_COLOR_B; + // } + // if (g_base->ui->scale() != UIScale::kSmall) { + // bd.color_r *= 2.0f; + // bd.color_g *= 2.0f; + // bd.color_b *= 2.0f; + // } + bd.depth_min = 0.3f; - bd.call = UIV1Python::ObjID::kEmptyCall; bd.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - // show in currency mode - if (type == 2 || type == 3) { + // Show some in store mode. + if (type == MeterType::kLevel || type == MeterType::kTickets) { bd.visibility_mask |= - static_cast(Widget::ToolbarVisibility::kMenuCurrency); + static_cast(Widget::ToolbarVisibility::kMenuStore) + | static_cast(Widget::ToolbarVisibility::kMenuStoreNoBack); + } + // Show some in get-tokens/tokens mode + if (type == MeterType::kTokens) { + bd.visibility_mask |= + static_cast(Widget::ToolbarVisibility::kGetTokens) + | static_cast(Widget::ToolbarVisibility::kMenuTokens); + } + + // Adjust buffer between neighbors. + switch (type) { + case MeterType::kLevel: + bd.pre_buffer = 50.0f; + break; + case MeterType::kTrophy: + bd.pre_buffer = 50.0f; + break; + case MeterType::kTickets: + bd.pre_buffer = 50.0f; + break; + case MeterType::kTokens: + bd.pre_buffer = 50.0f; + break; + default: + break; + } + + // Extend button target areas to cover where icon will go. + switch (type) { + case MeterType::kLevel: + bd.target_extra_left = 40.0f; + break; + case MeterType::kTrophy: + bd.target_extra_left = 40.0f; + break; + case MeterType::kTickets: + bd.target_extra_right = 40.0f; + break; + case MeterType::kTokens: + bd.target_extra_right = 40.0f; + break; + default: + break; + } + + switch (type) { + case MeterType::kLevel: + bd.call = UIV1Python::ObjID::kRootUILevelIconPressCall; + break; + case MeterType::kTrophy: + bd.call = UIV1Python::ObjID::kRootUITrophyMeterPressCall; + break; + case MeterType::kTokens: + bd.call = UIV1Python::ObjID::kRootUITokensMeterPressCall; + break; + case MeterType::kTickets: + bd.call = UIV1Python::ObjID::kRootUITicketIconPressCall; + break; + default: + break; + } + + Button* btn = AddButton_(bd); + + // Store the bar button in some cases. + switch (type) { + case MeterType::kLevel: + level_meter_button_ = btn; + top_left_buttons_.push_back(btn); + break; + case MeterType::kTrophy: + trophy_meter_button_ = btn; + top_left_buttons_.push_back(btn); + break; + case MeterType::kTickets: + tickets_meter_button_ = btn; + top_right_buttons_.push_back(btn); + break; + case MeterType::kTokens: + tokens_meter_button_ = btn; + top_right_buttons_.push_back(btn); + break; + default: + break; } - Button* btn = AddButton(bd); // Bar value text. { @@ -195,92 +322,138 @@ void RootWidget::AddMeter(float h_align, float x, int type, float r, float g, td.flatness = 1.0f; td.shadow = 1.0f; td.depth_min = 0.3f; - AddText(td); + AddText_(td); + } + // Icon on side. + { + ImageDef imgd; + imgd.button = btn; + // id.x = 0.0f; + if (type == MeterType::kLevel || type == MeterType::kTrophy) { + imgd.x = -0.5 * width - 10.0f; + } else { + imgd.x = 0.5 * width + 10.0f; + } + + imgd.y = 0.0f; + imgd.width = 54.0f; + imgd.height = 54.0f; + switch (type) { + case MeterType::kLevel: + imgd.img = "levelIcon"; + break; + case MeterType::kTrophy: + imgd.img = "trophy"; + break; + case MeterType::kTokens: + imgd.img = "coin"; + break; + case MeterType::kTickets: + imgd.img = "tickets"; + break; + default: + break; + } + imgd.depth_min = 0.3f; + AddImage_(imgd); + + // Level num. + if (type == MeterType::kLevel) { + TextDef td; + td.button = btn; + td.width = imgd.width * 0.8f; + td.text = "12"; + td.x = imgd.x - 2.1f; + td.y = imgd.y + 1.0f; + td.scale = 0.9f; + td.flatness = 1.0f; + td.shadow = 1.0f; + td.depth_min = 0.3f; + td.color_r = 1.0f; + td.color_g = 1.0f; + td.color_b = 1.0f; + AddText_(td); + } } } - // Icon on left. + // Icon on side. { - ButtonDef bd; - bd.h_align = h_align; - bd.v_align = VAlign::kTop; - bd.width = bd.height = 50.0f; - if (type == 0 || type == 1) { - bd.x = x - width * 0.5f - 10.0f; - } else { - bd.x = x + width * 0.5f + 10.0f; - } - bd.y = -32.0f + 7.0f + yoffs; - bd.color_r = r; - bd.color_g = g; - bd.color_b = b; - bd.depth_min = 0.3f; - switch (type) { - case 0: - bd.img = "levelIcon"; - bd.call = UIV1Python::ObjID::kLevelIconPressCall; - break; - case 1: - bd.img = "trophy"; - bd.call = UIV1Python::ObjID::kTrophyIconPressCall; - break; - case 2: - bd.img = "coin"; - bd.call = UIV1Python::ObjID::kCoinIconPressCall; - break; - case 3: - bd.img = "tickets"; - bd.call = UIV1Python::ObjID::kTicketIconPressCall; - break; + // ButtonDef bd; + // bd.selectable = false; + // bd.h_align = h_align; + // bd.v_align = VAlign::kTop; + // bd.width = bd.height = 50.0f; + // if (type == MeterType::kLevel || type == MeterType::kTrophy) { + // bd.x = x - width * 0.5f - 10.0f; + // } else { + // bd.x = x + width * 0.5f + 10.0f; + // } + // bd.y = -32.0f + 7.0f + yoffs; + // bd.color_r = r; + // bd.color_g = g; + // bd.color_b = b; + // bd.depth_min = 0.3f; + // switch (type) { + // case MeterType::kLevel: + // bd.img = "levelIcon"; + // bd.call = UIV1Python::ObjID::kRootUILevelIconPressCall; + // break; + // case MeterType::kTrophy: + // bd.img = "trophy"; + // bd.call = UIV1Python::ObjID::kRootUITrophyMeterPressCall; + // break; + // case MeterType::kToken: + // bd.img = "coin"; + // bd.call = UIV1Python::ObjID::kRootUITokensMeterPressCall; + // break; + // case MeterType::kTicket: + // bd.img = "tickets"; + // bd.call = UIV1Python::ObjID::kRootUITicketIconPressCall; + // break; + // default: + // break; + // } + // bd.visibility_mask = + // (static_cast(Widget::ToolbarVisibility::kMenuFull) + // | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) + // | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); -#pragma clang diagnostic push -#pragma ide diagnostic ignored "UnreachableCode" - default: - break; -#pragma clang diagnostic pop - } - bd.visibility_mask = - (static_cast(Widget::ToolbarVisibility::kMenuFull) - | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - // show in currency mode - if (type == 2 || type == 3) { - bd.visibility_mask |= - static_cast(Widget::ToolbarVisibility::kMenuCurrency); - } - Button* btn = AddButton(bd); - switch (type) { // NOLINT - case 3: - tickets_info_button_ = btn; - break; - default: - break; - } + // // Show some in store mode. + // if (type == MeterType::kLevel || type == MeterType::kTicket + // || type == MeterType::kToken) { + // bd.visibility_mask |= + // static_cast(Widget::ToolbarVisibility::kMenuStore) + // | + // static_cast(Widget::ToolbarVisibility::kMenuStoreNoBack); + // } + // // Show some in get-tokens mode. + // if (type == MeterType::kToken) { + // bd.visibility_mask |= + // static_cast(Widget::ToolbarVisibility::kGetTokens); + // } + // Button* btn = AddButton_(bd); - // Level num. - if (type == 0) { - TextDef td; - td.button = btn; - td.width = bd.width * 0.8f; - td.text = "12"; - td.x = -1.6f; - td.y = 0.8f; - td.scale = 0.9f; - td.flatness = 1.0f; - td.shadow = 1.0f; - td.depth_min = 0.3f; - td.color_r = 1.0f; - td.color_g = 1.0f; - td.color_b = 1.0f; - AddText(td); - } + // // Store certain ones. + // switch (type) { + // case MeterType::kLevel: + // level_icon_ = btn; + // break; + // case MeterType::kTrophy: + // trophy_icon_ = btn; + // break; + // break; + // default: + // break; + // } } - // plus button + + // Plus button. if (plus) { ButtonDef bd; bd.h_align = h_align; bd.v_align = VAlign::kTop; bd.width = bd.height = 45.0f; - // bd.x = x + 72; - bd.x = x - 68; + // bd.x = x - 68; bd.y = -36.0f + 11.0f + yoffs; bd.img = "uiAtlas2"; bd.mesh_transparent = "currencyPlusButton"; @@ -293,31 +466,41 @@ void RootWidget::AddMeter(float h_align, float x, int type, float r, float g, bd.color_b *= TOOLBAR_COLOR_B; } bd.depth_min = 0.3f; - bd.call = UIV1Python::ObjID::kEmptyCall; + switch (type) { + case MeterType::kTokens: + bd.call = UIV1Python::ObjID::kRootUIGetTokensButtonPressCall; + break; + default: + break; + } bd.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - // Show in currency mode. - if (type == 2 || type == 3) { + // Show some in store mode. + if (type == MeterType::kLevel || type == MeterType::kTickets) { bd.visibility_mask |= - static_cast(Widget::ToolbarVisibility::kMenuCurrency); + static_cast(Widget::ToolbarVisibility::kMenuStore) + | static_cast(Widget::ToolbarVisibility::kMenuStoreNoBack); } - Button* btn = AddButton(bd); - if (type == 3) { - tickets_plus_button_ = btn; + // Show some in tokens mode. + if (type == MeterType::kTokens) { + bd.visibility_mask |= + static_cast(Widget::ToolbarVisibility::kMenuTokens); } + + bd.pre_buffer = -10.0f; + Button* btn = AddButton_(bd); + if (type == MeterType::kTokens) { + get_tokens_button_ = btn; + } + top_right_buttons_.push_back(btn); } } -#pragma clang diagnostic pop - void RootWidget::Setup() { - if (!explicit_bool(BA_UI_V1_TOOLBAR_TEST)) { - return; - } - - // back button + // Back button. { ButtonDef bd; bd.h_align = 0.0f; @@ -327,16 +510,20 @@ void RootWidget::Setup() { bd.color_g = 0.4f; bd.color_b = 0.35f; - bd.x = 40.0f; + // bd.x = 40.0f; bd.y = -40.0f; bd.img = "nub"; - bd.call = UIV1Python::ObjID::kBackButtonPressCall; + bd.call = UIV1Python::ObjID::kRootUIBackButtonPressCall; bd.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuMinimal) - | static_cast(Widget::ToolbarVisibility::kMenuFull)); - Button* b = back_button_ = AddButton(bd); + | static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuStore) + | static_cast(Widget::ToolbarVisibility::kGetTokens) + | static_cast(Widget::ToolbarVisibility::kMenuTokens)); + bd.pre_buffer = -30.0f; + Button* b = back_button_ = AddButton_(bd); + top_left_buttons_.push_back(b); - // clan { TextDef td; td.button = b; @@ -348,7 +535,7 @@ void RootWidget::Setup() { td.scale = 2.0f; td.flatness = 0.0f; td.shadow = 0.5f; - AddText(td); + AddText_(td); } } @@ -361,10 +548,10 @@ void RootWidget::Setup() { // } // if (c) { // c->visibility_mask |= - // static_cast(Widget::ToolbarVisibility::kMenuCurrency); + // static_cast(Widget::ToolbarVisibility::kMenuStore); // } - // top bar backing (currency only) + // Top bar backing (currency only). if (explicit_bool(false)) { ButtonDef bd; bd.h_align = 0.5f; @@ -404,11 +591,11 @@ void RootWidget::Setup() { // bd.visibility_mask |= // static_cast(Widget::ToolbarVisibility::kMenuFull); bd.visibility_mask |= - static_cast(Widget::ToolbarVisibility::kMenuCurrency); - AddButton(bd); + static_cast(Widget::ToolbarVisibility::kMenuStore); + AddButton_(bd); } - // top bar backing + // Top bar backing. if (explicit_bool(false)) { ButtonDef bd; bd.h_align = 0.5f; @@ -443,34 +630,36 @@ void RootWidget::Setup() { bd.visibility_mask |= static_cast(Widget::ToolbarVisibility::kMenuFull); // bd.visibility_mask |= - // static_cast(Widget::ToolbarVisibility::kMenuCurrency); - AddButton(bd); + // static_cast(Widget::ToolbarVisibility::kMenuStore); + AddButton_(bd); } + // float xoffs = (g_base->ui->scale() == UIScale::kSmall) ? 90.0f : 0.0f; float yoffs = (g_base->ui->scale() == UIScale::kSmall) ? 0.0f : -10.0f; - // account button + // Account Button { ButtonDef bd; - bd.h_align = 0.1f; + bd.h_align = 0.0f; bd.v_align = VAlign::kTop; bd.width = 160.0f; bd.height = 60.0f; bd.depth_min = 0.3f; - bd.x = (g_base->ui->scale() == UIScale::kSmall) ? 100.0f : -50.0f; + // bd.x = 110.0f + xoffs; bd.y = -24.0f + yoffs; bd.color_r = 0.56f; bd.color_g = 0.5f; bd.color_b = 0.73f; + bd.call = UIV1Python::ObjID::kRootUIAccountButtonPressCall; if (g_base->ui->scale() != UIScale::kSmall) { bd.color_r *= TOOLBAR_COLOR_R; bd.color_g *= TOOLBAR_COLOR_G; bd.color_b *= TOOLBAR_COLOR_B; } - // bd.call = ""; - bd.call = UIV1Python::ObjID::kEmptyCall; + bd.pre_buffer = 10.0f; bd.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); // on desktop, stick this in the top left corner @@ -479,9 +668,10 @@ void RootWidget::Setup() { // bd.x = 120.0f; // } - Button* b = account_button_ = AddButton(bd); + Button* b = account_button_ = AddButton_(bd); + top_left_buttons_.push_back(b); - // player name + // Player name. { TextDef td; td.button = b; @@ -494,9 +684,10 @@ void RootWidget::Setup() { td.color_g = 0.8f; td.color_b = 0.8f; td.shadow = 1.0f; - AddText(td); + AddText_(td); } - // clan + + // Clan. { TextDef td; td.button = b; @@ -508,114 +699,224 @@ void RootWidget::Setup() { td.scale = 0.6f; td.flatness = 1.0f; td.shadow = 0.0f; - AddText(td); + AddText_(td); } } - float anchorx = (g_base->ui->scale() == UIScale::kSmall) ? 0.3f : 0.25f; + // float anchorx = 0.0f; + // float anchorx = (g_base->ui->scale() == UIScale::kSmall) ? 0.3f : 0.25f; - AddMeter(anchorx, 200.0f - 148.0f, 0, 1.0f, 1.0f, 1.0f, false, "456/1000"); - AddMeter(anchorx, 200.0f, 1, 1.0f, 1.0f, 1.0f, false, "123"); + AddMeter_(MeterType::kLevel, 0.0f, 1.0f, 1.0f, 1.0f, false, "456/1000"); + AddMeter_(MeterType::kTrophy, 0.0f, 1.0f, 1.0f, 1.0f, false, "#123"); + // AddMeter(anchorx, 200.0f - 148.0f, 0, 1.0f, 1.0f, 1.0f, false, "456/1000"); + // AddMeter(anchorx, 200.0f, 1, 1.0f, 1.0f, 1.0f, false, "123"); - AddMeter(0.7f, -100.0f, 2, 1.0f, 1.0f, 1.0f, true, "12343"); - AddMeter(0.7f, -100.0f + 188.0f, 3, 1.0f, 1.0f, 1.0f, true, "123"); - - // party button - { - ButtonDef b; - b.h_align = 1.0f; - b.v_align = VAlign::kTop; - b.width = b.height = 70.0f; - b.x = -110.0f; - b.y = b.height * -0.41f; - b.img = "usersButton"; - b.call = UIV1Python::ObjID::kFriendsButtonPressCall; - b.visibility_mask = - (static_cast(Widget::ToolbarVisibility::kInGame) - | static_cast(Widget::ToolbarVisibility::kMenuMinimal) - | static_cast(Widget::ToolbarVisibility::kMenuMinimalNoBack) - | static_cast(Widget::ToolbarVisibility::kMenuCurrency) - | static_cast(Widget::ToolbarVisibility::kMenuFull) - | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - party_button_ = AddButton(b); - } - - // menu button (only shows up when we're not in a menu) + // Menu button (only shows up when we're not in a menu) // FIXME - this should never be visible on TV or VR UI modes { ButtonDef b; b.h_align = 1.0f; b.v_align = VAlign::kTop; b.width = b.height = 65.0f; - b.x = -36.0f; + // b.x = -36.0f; b.y = b.height * -0.48f; b.img = "menuButton"; - b.call = UIV1Python::ObjID::kBackButtonPressCall; + b.call = UIV1Python::ObjID::kRootUIMenuButtonPressCall; b.color_r = 0.3f; b.color_g = 0.5f; b.color_b = 0.2f; b.visibility_mask = (static_cast(Widget::ToolbarVisibility::kInGame) + | static_cast(Widget::ToolbarVisibility::kMenuInGame) | static_cast(Widget::ToolbarVisibility::kMenuMinimal) | static_cast(Widget::ToolbarVisibility::kMenuMinimalNoBack) - | static_cast(Widget::ToolbarVisibility::kMenuCurrency) + | static_cast(Widget::ToolbarVisibility::kMenuStore) + | static_cast(Widget::ToolbarVisibility::kMenuStoreNoBack) | static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - menu_button_ = AddButton(b); + b.pre_buffer = 5.0f; + b.enable_sound = false; + menu_button_ = AddButton_(b); + top_right_buttons_.push_back(menu_button_); } - // bot-left cover + // Party button. + { + ButtonDef b; + b.h_align = 1.0f; + b.v_align = VAlign::kTop; + b.width = b.height = 70.0f; + // b.x = -110.0f; + b.y = b.height * -0.41f; + b.img = "usersButton"; + b.call = UIV1Python::ObjID::kRootUISquadButtonPressCall; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kInGame) + | static_cast(Widget::ToolbarVisibility::kMenuInGame) + | static_cast(Widget::ToolbarVisibility::kMenuMinimal) + | static_cast(Widget::ToolbarVisibility::kMenuMinimalNoBack) + | static_cast(Widget::ToolbarVisibility::kMenuStore) + | static_cast(Widget::ToolbarVisibility::kMenuStoreNoBack) + | static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot) + | static_cast(Widget::ToolbarVisibility::kGetTokens) + | static_cast(Widget::ToolbarVisibility::kMenuTokens)); + b.pre_buffer = 5.0f; + b.enable_sound = false; + squad_button_ = AddButton_(b); + top_right_buttons_.push_back(squad_button_); + } + + AddMeter_(MeterType::kTokens, 1.0f, 1.0f, 1.0f, 1.0f, true, "123"); + AddMeter_(MeterType::kTickets, 1.0f, 1.0f, 1.0f, 1.0f, false, "12345"); + + // Bot-left cover // AddCover(0.0f, VAlign::kBottom, 0.0f, -210.0f, 600.0f, 600.0f, 0.25f); - float bx = 45.0f; + float bx = 55.0f; - // log button + // Inbox button. { ButtonDef b; b.h_align = 0.0f; b.v_align = VAlign::kBottom; - b.width = b.height = 50.0f; - b.x = bx; + b.width = b.height = 55.0f; + // b.x = bx; b.y = b.height * 0.5f + 5; b.color_r = BOT_LEFT_COLOR_R; b.color_g = BOT_LEFT_COLOR_G; b.color_b = BOT_LEFT_COLOR_B; b.img = "logIcon"; + b.call = UIV1Python::ObjID::kRootUIInboxButtonPressCall; b.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - AddButton(b); + b.pre_buffer = 20.0f; + inbox_button_ = AddButton_(b); + bottom_left_buttons_.push_back(inbox_button_); } - bx += 70.0f; + bx += 80.0f; - // achievements button - { + // Achievements button. + if (explicit_bool(true)) { ButtonDef b; b.h_align = 0.0f; b.v_align = VAlign::kBottom; - b.width = b.height = 50.0f; - b.x = bx; + b.width = b.height = 55.0f; + // b.x = bx; b.y = b.height * 0.5f + 5; b.color_r = BOT_LEFT_COLOR_R; b.color_g = BOT_LEFT_COLOR_G; b.color_b = BOT_LEFT_COLOR_B; b.img = "achievementsIcon"; + b.call = UIV1Python::ObjID::kRootUIAchievementsButtonPressCall; b.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - AddButton(b); + b.pre_buffer = 20.0f; + achievements_button_ = AddButton_(b); + bottom_left_buttons_.push_back(achievements_button_); + bx += 80.0f; + + // Achievement count. + // { + // TextDef td; + // td.button = achievements_button_; + // td.width = 26.0f; + // td.text = "12/39"; + // td.x = -1.5f; + // td.y = 8.0f; + // td.scale = 0.9f; + // td.flatness = 1.0f; + // td.shadow = 1.0f; + // td.depth_min = 0.3f; + // td.color_r = 1.0f; + // td.color_g = 1.0f; + // td.color_b = 1.0f; + // AddText_(td); + // } + auto centerx = -1.5f; + auto centery = 8.0f; + auto offsx = 5.5f; + auto offsy = 5.5f; + // { + // TextDef td; + // td.button = achievements_button_; + // td.width = 26.0f; + // td.text = "/"; + // td.x = centerx; + // td.y = centery; + // td.scale = 0.3f; + // td.flatness = 1.0f; + // td.shadow = 1.0f; + // td.depth_min = 0.3f; + // td.color_r = 1.0f; + // td.color_g = 1.0f; + // td.color_b = 1.0f; + // AddText_(td); + // } + // { + // TextDef td; + // td.button = achievements_button_; + // td.width = 26.0f; + // td.text = "12"; + // td.x = centerx - offsx; + // td.y = centery + offsy; + // td.scale = 0.4f; + // td.flatness = 1.0f; + // td.shadow = 1.0f; + // td.depth_min = 0.3f; + // td.color_r = 1.0f; + // td.color_g = 1.0f; + // td.color_b = 1.0f; + // AddText_(td); + // } + // { + // TextDef td; + // td.button = achievements_button_; + // td.width = 26.0f; + // td.text = "34"; + // td.x = centerx + offsx; + // td.y = centery - offsy; + // td.scale = 0.4f; + // td.flatness = 1.0f; + // td.shadow = 1.0f; + // td.depth_min = 0.3f; + // td.color_r = 1.0f; + // td.color_g = 1.0f; + // td.color_b = 1.0f; + // AddText_(td); + // } + { + TextDef td; + td.button = achievements_button_; + td.width = 26.0f; + td.text = "34%"; + td.x = centerx; + td.y = centery; + td.scale = 0.6f; + td.flatness = 1.0f; + td.shadow = 0.0f; + td.depth_min = 0.3f; + td.color_r = 0.8f; + td.color_g = 0.75f; + td.color_b = 0.9f; + AddText_(td); + } } - bx += 70.0f; - - // leaderboards button - { + // Leaderboards button. + if (explicit_bool(false)) { ButtonDef b; b.h_align = 0.0f; b.v_align = VAlign::kBottom; b.width = b.height = 50.0f; - b.x = bx; + // b.x = bx; b.y = b.height * 0.5f + 5; b.color_r = BOT_LEFT_COLOR_R; b.color_g = BOT_LEFT_COLOR_G; @@ -623,33 +924,39 @@ void RootWidget::Setup() { b.img = "leaderboardsIcon"; b.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - AddButton(b); + AddButton_(b); + bx += 70.0f; } - bx += 70.0f; - - // settings button + // Settings button. { ButtonDef b; b.h_align = 0.0f; b.v_align = VAlign::kBottom; - b.width = b.height = 50.0f; - b.x = bx; + b.width = b.height = 55.0f; + // b.x = bx; b.y = b.height * 0.58f; b.color_r = BOT_LEFT_COLOR_R; b.color_g = BOT_LEFT_COLOR_G; b.color_b = BOT_LEFT_COLOR_B; b.img = "settingsIcon"; + b.call = UIV1Python::ObjID::kRootUISettingsButtonPressCall; b.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) - | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - settings_button_ = AddButton(b); + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot) + | static_cast(Widget::ToolbarVisibility::kMenuInGame)); + b.pre_buffer = 20.0f; + settings_button_ = AddButton_(b); + bottom_left_buttons_.push_back(settings_button_); } - // chests + // Chests. { - // AddCover(0.5f, VAlign::kBottom, 0.0f, -180.0f, 600.0f, 550.0f, 0.35f); + // AddCover(0.5f, VAlign::kBottom, 0.0f, -180.0f, 600.0f, 550.0f, + // 0.35f); float backingR = 0.44f; float backingG = 0.41f; @@ -676,7 +983,7 @@ void RootWidget::Setup() { backingA *= TOOLBAR_OPACITY_2; } - // bar backing + // Bar backing. { ButtonDef bd; bd.h_align = 0.5f; @@ -697,50 +1004,55 @@ void RootWidget::Setup() { // bd.call = ""; bd.call = UIV1Python::ObjID::kEmptyCall; bd.visibility_mask = - static_cast(Widget::ToolbarVisibility::kMenuFullRoot); - bd.visibility_mask |= - static_cast(Widget::ToolbarVisibility::kMenuFull); + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - AddButton(bd); + AddButton_(bd); } ButtonDef b; b.h_align = 0.5f; b.v_align = VAlign::kBottom; - b.width = b.height = 110.0f; + b.width = b.height = 120.0f; b.x = 0.0f; b.y = b.height * 0.4f; b.img = "chestIcon"; b.depth_min = 0.3f; b.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - float spacing = 110.0f; - b.x = -2.0f * spacing; - AddButton(b); + float spacing = 130.0f; + b.x = -1.5f * spacing; + b.call = UIV1Python::ObjID::kRootUIChestSlot1PressCall; + AddButton_(b); - b.x = -1.0f * spacing; + b.x = -0.5f * spacing; b.img = "chestOpenIcon"; b.y = b.height * 0.5f; - AddButton(b); + b.call = UIV1Python::ObjID::kRootUIChestSlot2PressCall; + AddButton_(b); // test - empty icons b.y = b.height * 0.4f; - b.x = 0.0f; + b.x = 0.5f * spacing; b.img = "chestIconEmpty"; b.width = b.height = 80.0f; b.color_r = backingCoverR; b.color_g = backingCoverG; b.color_b = backingCoverB; b.opacity = 1.0f; - AddButton(b); - b.x = 1.0f * spacing; - AddButton(b); - b.x = 2.0f * spacing; + b.call = UIV1Python::ObjID::kRootUIChestSlot3PressCall; + AddButton_(b); + b.x = 1.5f * spacing; + b.call = UIV1Python::ObjID::kRootUIChestSlot4PressCall; + AddButton_(b); + // b.x = 2.0f * spacing; // test - multi-icon tile - b.img = "chestIconMulti"; - AddButton(b); + // b.img = "chestIconMulti"; + // AddButton(b); } // bot-right cover @@ -762,59 +1074,73 @@ void RootWidget::Setup() { // AddButton(b); // } - // store button - { - ButtonDef b; - b.h_align = 1.0f; - b.v_align = VAlign::kBottom; - b.width = b.height = 85.0f; - b.x = -206.0f; - b.y = b.height * 0.5f; - b.img = "storeIcon"; - b.visibility_mask = - (static_cast(Widget::ToolbarVisibility::kMenuFull) - | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - AddButton(b); - } - - // inventory button + // Inventory button. { ButtonDef b; b.h_align = 1.0f; b.v_align = VAlign::kBottom; b.width = b.height = 135.0f; - b.x = -80.0f; + // b.x = -80.0f; b.y = b.height * 0.45f; b.img = "inventoryIcon"; + b.call = UIV1Python::ObjID::kRootUIInventoryButtonPressCall; b.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); - AddButton(b); + b.disable_offset_scale = 1.5f; + b.pre_buffer = 20.0f; + inventory_button_ = AddButton_(b); + bottom_right_buttons_.push_back(inventory_button_); } - UpdateForFocusedWindow(nullptr); + // Store button. + { + ButtonDef b; + b.h_align = 1.0f; + b.v_align = VAlign::kBottom; + b.width = b.height = 85.0f; + // b.x = -206.0f; + b.y = b.height * 0.5f; + b.img = "storeIcon"; + b.call = UIV1Python::ObjID::kRootUIStoreButtonPressCall; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullNoBack) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + b.pre_buffer = 20.0f; + store_button_ = AddButton_(b); + bottom_right_buttons_.push_back(store_button_); + } + + UpdateForFocusedWindow_(nullptr); } 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_); - StepPositions(static_cast(time_diff)); + + StepPositions_(static_cast(time_diff)); update_time_ = current_time; } ContainerWidget::Draw(pass, transparent); } -auto RootWidget::AddButton(const ButtonDef& def) -> RootWidget::Button* { +auto RootWidget::AddButton_(const ButtonDef& def) -> RootWidget::Button* { base::ScopedSetContext ssc(nullptr); buttons_.emplace_back(); Button& b(buttons_.back()); b.x = b.x_smoothed = b.x_target = def.x; b.y = b.y_smoothed = b.y_target = def.y; b.visibility_mask = def.visibility_mask; + b.disable_offset_scale = def.disable_offset_scale; + b.pre_buffer = def.pre_buffer; + b.post_buffer = def.post_buffer; b.scale = def.scale; b.width = def.width; b.height = def.height; @@ -828,7 +1154,11 @@ auto RootWidget::AddButton(const ButtonDef& def) -> RootWidget::Button* { b.widget->set_text(def.label); b.widget->set_enabled(def.selectable); b.widget->set_selectable(def.selectable); - b.widget->SetDepthRange(def.depth_min, def.depth_max); + b.widget->set_depth_range(def.depth_min, def.depth_max); + b.widget->set_target_extra_left(def.target_extra_left); + b.widget->set_target_extra_right(def.target_extra_right); + + b.widget->set_enable_sound(def.enable_sound); // Make sure up/down moves focus into the main stack. assert(screen_stack_widget_ != nullptr); @@ -843,13 +1173,16 @@ auto RootWidget::AddButton(const ButtonDef& def) -> RootWidget::Button* { b.widget->set_neighbors_locked(true); if (!def.img.empty()) { + base::Assets::AssetListLock lock; b.widget->SetTexture(g_base->assets->GetTexture(def.img).Get()); } if (!def.mesh_transparent.empty()) { + base::Assets::AssetListLock lock; b.widget->SetMeshTransparent( g_base->assets->GetMesh(def.mesh_transparent).Get()); } if (!def.mesh_opaque.empty()) { + base::Assets::AssetListLock lock; b.widget->SetMeshOpaque(g_base->assets->GetMesh(def.mesh_opaque).Get()); } if (def.call != UIV1Python::ObjID::kEmptyCall) { @@ -859,7 +1192,7 @@ auto RootWidget::AddButton(const ButtonDef& def) -> RootWidget::Button* { return &b; } -auto RootWidget::AddText(const TextDef& def) -> RootWidget::Text* { +auto RootWidget::AddText_(const TextDef& def) -> RootWidget::Text* { base::ScopedSetContext ssc(nullptr); texts_.emplace_back(); Text& t(texts_.back()); @@ -875,7 +1208,7 @@ auto RootWidget::AddText(const TextDef& def) -> RootWidget::Text* { t.widget->set_color(def.color_r, def.color_g, def.color_b, def.color_a); t.widget->set_shadow(def.shadow); t.widget->set_flatness(def.flatness); - t.widget->SetDepthRange(def.depth_min, def.depth_max); + t.widget->set_depth_range(def.depth_min, def.depth_max); assert(def.button->widget.Exists()); t.widget->set_draw_control_parent(def.button->widget.Get()); t.x = def.x; @@ -884,16 +1217,39 @@ auto RootWidget::AddText(const TextDef& def) -> RootWidget::Text* { return &t; } +auto RootWidget::AddImage_(const ImageDef& def) -> RootWidget::Image* { + base::ScopedSetContext ssc(nullptr); + images_.emplace_back(); + Image& img(images_.back()); + img.button = def.button; + img.widget = Object::New(); + img.widget->set_width(def.width); + img.widget->set_height(def.height); + img.widget->set_depth_range(def.depth_min, def.depth_max); + if (!def.img.empty()) { + base::Assets::AssetListLock lock; + img.widget->SetTexture(g_base->assets->GetTexture(def.img).Get()); + } + assert(def.button->widget.Exists()); + img.widget->set_draw_control_parent(def.button->widget.Get()); + img.x = def.x - def.width * 0.5f; + img.y = def.y - def.height * 0.5f; + AddWidget(img.widget.Get()); + return &img; +} + void RootWidget::UpdateForFocusedWindow() { - UpdateForFocusedWindow( + UpdateForFocusedWindow_( screen_stack_widget_ != nullptr ? screen_stack_widget_->GetTopmostToolbarInfluencingWidget() : nullptr); } -void RootWidget::UpdateForFocusedWindow(Widget* widget) { +void RootWidget::UpdateForFocusedWindow_(Widget* widget) { // Take note if the current session is the main menu; we do a few things // differently there. + // + // FIXME - need a more generalized way to determine this. in_main_menu_ = g_base->app_mode()->InClassicMainMenuSession(); if (widget == nullptr) { @@ -904,11 +1260,88 @@ void RootWidget::UpdateForFocusedWindow(Widget* widget) { MarkForUpdate(); } -void RootWidget::StepPositions(float dt) { +void RootWidget::StepPositions_(float 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); + if (!positions_dirty_) { return; } + // Update enabled-state for all buttons. + for (Button& b : buttons_) { + bool enable_button = + static_cast(static_cast(toolbar_visibility_) + & static_cast(b.visibility_mask)); + // When we're in the main menu, always disable the menu button and + // shift the party button a bit to the right + if (in_main_menu_) { + if (&b == menu_button_) { + enable_button = false; + } + } + if (&b == back_button_) { + // Back button is always disabled in medium/large UI. + if (g_base->ui->scale() != UIScale::kSmall) { + enable_button = false; + } + } + b.enabled = enable_button; + } + + // Go through our corner button lists updating positions based on + // what is visible. + float xpos = 0.0f; + for (auto* btn : top_left_buttons_) { + auto enabled = btn->enabled; + float bwidthhalf = btn->width * 0.5; + if (enabled) { + xpos += bwidthhalf + btn->pre_buffer; + } + btn->x = xpos; + if (enabled) { + xpos += bwidthhalf + btn->post_buffer; + } + } + xpos = 0.0f; + for (auto* btn : top_right_buttons_) { + auto enabled = btn->enabled; + float bwidthhalf = btn->width * 0.5; + if (enabled) { + xpos -= bwidthhalf + btn->pre_buffer; + } + btn->x = xpos; + if (enabled) { + xpos -= bwidthhalf + btn->post_buffer; + } + } + xpos = 0.0f; + for (auto* btn : bottom_left_buttons_) { + auto enabled = btn->enabled; + float bwidthhalf = btn->width * 0.5; + if (enabled) { + xpos += bwidthhalf + btn->pre_buffer; + } + btn->x = xpos; + if (enabled) { + xpos += bwidthhalf + btn->post_buffer; + } + } + xpos = 0.0f; + for (auto* btn : bottom_right_buttons_) { + auto enabled = btn->enabled; + float bwidthhalf = btn->width * 0.5; + if (enabled) { + xpos -= bwidthhalf + btn->pre_buffer; + } + btn->x = xpos; + if (enabled) { + xpos -= bwidthhalf + btn->post_buffer; + } + } + // Go through our buttons updating their target points and smooth values. // If everything has arrived at its target point, mark us as not dirty. bool have_dirty = false; @@ -916,44 +1349,59 @@ void RootWidget::StepPositions(float dt) { // Update our target position. b.x_target = b.x; b.y_target = b.y; - float disable_offset = - 110.0f * ((b.v_align == VAlign::kTop) ? 1.0f : -1.0f); + float disable_offset = b.disable_offset_scale * 110.0f + * ((b.v_align == VAlign::kTop) ? 1.0f : -1.0f); // float top_right_offset = 100.0f; // Can turn this down to debug visibility. if (explicit_bool(false)) { - disable_offset *= 0.5f; + disable_offset *= 0.1f; // top_right_offset *= 0.5f; } - bool enable_button = - static_cast(static_cast(toolbar_visibility_) - & static_cast(b.visibility_mask)); + // bool enable_button = + // static_cast(static_cast(toolbar_visibility_) + // & static_cast(b.visibility_mask)); - // When we're in the main menu, always disable the menu button and shift - // the party button a bit to the right + // When the back button is hidden, move the account button to the left. + // bool move_left_for_back{}; + // if (g_base->ui->scale() == UIScale::kSmall + // && !static_cast( + // static_cast(toolbar_visibility_) + // & static_cast(back_button_->visibility_mask))) { + // move_left_for_back = true; + // } + // if (move_left_for_back + // && (&b == account_button_ || &b == level_icon_ + // || &b == level_meter_button_ || &b == trophy_icon_ + // || &b == trophy_meter_button_)) { + // b.x_target -= 100.0f; + // } + + // When we're in the main menu, always disable the menu button and + // shift the party button a bit to the right if (in_main_menu_) { - if (&b == menu_button_) { - enable_button = false; - } - if (&b == party_button_) { - b.x_target += 70.0f; - } + // if (&b == menu_button_) { + // enable_button = false; + // } + // if (&b == squad_button_) { + // b.x_target += 70.0f; + // } } if (&b == back_button_) { // Back button is always disabled in medium/large UI. - if (g_base->ui->scale() != UIScale::kSmall) { - enable_button = false; - } + // if (g_base->ui->scale() != UIScale::kSmall) { + // enable_button = false; + // } // Whenever back button is enabled, left on account button should go // to it; otherwise it goes nowhere. Widget* ab = account_button_->widget.Get(); ab->set_neighbors_locked(false); - ab->set_left_widget(enable_button ? back_button_->widget.Get() : ab); + ab->set_left_widget(b.enabled ? back_button_->widget.Get() : ab); account_button_->widget->set_neighbors_locked(true); } - if (!enable_button) { + if (!b.enabled) { b.y_target += disable_offset; } @@ -968,7 +1416,7 @@ void RootWidget::StepPositions(float dt) { // b.x_target += top_right_offset; // } - // Now push our smooth value towards our target value... + // 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; @@ -979,14 +1427,16 @@ void RootWidget::StepPositions(float dt) { b.x_smoothed = b.x_target; b.y_smoothed = b.y_target; - // Also flip off visibility if we're moving offscreen and have reached - // our target. - if (!enable_button) { + // Also flip off visibility if we're moving offscreen and have + // reached our target. + if (!b.enabled) { + b.fully_offscreen = true; b.widget->set_visible_in_container(false); } } else { have_dirty = true; // Always remain visible while still moving. + b.fully_offscreen = false; b.widget->set_visible_in_container(true); } @@ -1007,8 +1457,8 @@ void RootWidget::StepPositions(float dt) { y = base_scale_ * (b.y_smoothed - b.height * b.scale * 0.5f); break; } - b.widget->set_selectable(enable_button && b.selectable); - b.widget->set_enabled(enable_button && b.selectable); + b.widget->set_selectable(b.enabled && b.selectable); + b.widget->set_enabled(b.enabled && b.selectable); b.widget->set_translate(x, y); b.widget->set_width(b.width); b.widget->set_height(b.height); @@ -1024,6 +1474,19 @@ void RootWidget::StepPositions(float dt) { b->widget->ty() + base_scale_ * b->scale * (b->height * 0.5f + t.y); t.widget->set_translate(x, y); t.widget->set_scale(base_scale_ * b->scale); + t.widget->set_visible_in_container(!b->fully_offscreen); + } + + for (Image& img : images_) { + // Move the image widget to wherever its target button is (plus offset). + Button* b = img.button; + float x = + b->widget->tx() + base_scale_ * b->scale * (b->width * 0.5f + img.x); + float y = + b->widget->ty() + base_scale_ * b->scale * (b->height * 0.5f + img.y); + img.widget->set_translate(x, y); + img.widget->set_scale(base_scale_ * b->scale); + img.widget->set_visible_in_container(!b->fully_offscreen); } positions_dirty_ = have_dirty; @@ -1072,13 +1535,12 @@ void RootWidget::UpdateLayout() { // Run an immediate step to update things; (avoids jumpy positions if // resizing game window)) - StepPositions(0.0f); + StepPositions_(0.0f); } auto RootWidget::HandleMessage(const base::WidgetMessage& m) -> bool { - // If a cancel message comes through and our back button is active, fire our - // back button. - // ..in all other cases just do the default. + // If a cancel message comes through and our back button is enabled, fire + // our back button. In all other cases just do the default. if (m.type == base::WidgetMessage::Type::kCancel && back_button_ != nullptr && back_button_->widget->enabled() && !overlay_stack_widget_->HasChildren()) { @@ -1088,9 +1550,14 @@ auto RootWidget::HandleMessage(const base::WidgetMessage& m) -> bool { return ContainerWidget::HandleMessage(m); } } +void RootWidget::BackPress() { + assert(g_base->InLogicThread()); + screen_stack_widget_->HandleMessage( + base::WidgetMessage(base::WidgetMessage::Type::kCancel)); +} void RootWidget::SetScreenWidget(StackWidget* w) { - // this needs to happen before any buttons get added.. + // this needs to happen before any buttons get added. assert(buttons_.empty()); AddWidget(w); screen_stack_widget_ = w; @@ -1098,9 +1565,8 @@ void RootWidget::SetScreenWidget(StackWidget* w) { void RootWidget::SetOverlayWidget(StackWidget* w) { // this needs to happen after our buttons and things get added.. - if (explicit_bool(BA_UI_V1_TOOLBAR_TEST)) { - assert(!buttons_.empty()); - } + assert(!buttons_.empty()); + AddWidget(w); overlay_stack_widget_ = w; } @@ -1112,22 +1578,41 @@ void RootWidget::OnCancelCustom() { // then it gets them all. Current repro case is Sign-in-with-BombSquad-Account // window - press escape a few times while that is up and then click cancel; // This code is only used for toolbar mode so should be safe to leave it - // disabled for now. g_ui->PushBackButtonCall(nullptr); + // disabled for now. + + // Is there a reason for this to exist? If so, what is it? + // printf("GOT OnCancelCustom\n"); + // g_base->ui->PushBackButtonCall(nullptr); } auto RootWidget::GetSpecialWidget(const std::string& s) const -> Widget* { - if (s == "party_button") { - return party_button_ ? party_button_->widget.Get() : nullptr; - } else if (s == "tickets_plus_button") { - return tickets_plus_button_ ? tickets_plus_button_->widget.Get() : nullptr; + if (s == "squad_button") { + return squad_button_ ? squad_button_->widget.Get() : nullptr; } else if (s == "back_button") { return back_button_ ? back_button_->widget.Get() : nullptr; } else if (s == "account_button") { return account_button_ ? account_button_->widget.Get() : nullptr; + } else if (s == "achievements_button") { + return achievements_button_ ? achievements_button_->widget.Get() : nullptr; + } else if (s == "inbox_button") { + return inbox_button_ ? inbox_button_->widget.Get() : nullptr; } else if (s == "settings_button") { return settings_button_ ? settings_button_->widget.Get() : nullptr; - } else if (s == "tickets_info_button") { - return tickets_info_button_ ? tickets_info_button_->widget.Get() : nullptr; + } else if (s == "store_button") { + return store_button_ ? store_button_->widget.Get() : nullptr; + } else if (s == "get_tokens_button") { + return get_tokens_button_ ? get_tokens_button_->widget.Get() : nullptr; + } else if (s == "inventory_button") { + return inventory_button_ ? inventory_button_->widget.Get() : nullptr; + } else if (s == "tickets_meter") { + return tickets_meter_button_ ? tickets_meter_button_->widget.Get() + : nullptr; + } else if (s == "tokens_meter") { + return tokens_meter_button_ ? tokens_meter_button_->widget.Get() : nullptr; + } else if (s == "trophy_meter") { + return trophy_meter_button_ ? trophy_meter_button_->widget.Get() : nullptr; + } else if (s == "level_meter") { + return level_meter_button_ ? level_meter_button_->widget.Get() : nullptr; } else if (s == "overlay_stack") { return overlay_stack_widget_; } diff --git a/src/ballistica/ui_v1/widget/root_widget.h b/src/ballistica/ui_v1/widget/root_widget.h index fb9fa692..272fa71e 100644 --- a/src/ballistica/ui_v1/widget/root_widget.h +++ b/src/ballistica/ui_v1/widget/root_widget.h @@ -10,8 +10,8 @@ namespace ballistica::ui_v1 { -// Root-level widget; contains a top-bar, screen-stack, bottom-bar, menu-button, -// etc. This is intended to replace RootUI. +// Root-level widget; contains a top-bar, screen-stack, bottom-bar, +// menu-button, etc. This is intended to replace RootUI. class RootWidget : public ContainerWidget { public: RootWidget(); @@ -22,6 +22,7 @@ class RootWidget : public ContainerWidget { void UpdateForFocusedWindow(); void Setup(); auto HandleMessage(const base::WidgetMessage& m) -> bool override; + void BackPress(); void Draw(base::RenderPass* pass, bool transparent) override; auto GetSpecialWidget(const std::string& s) const -> Widget*; auto base_scale() const -> float { return base_scale_; } @@ -29,38 +30,57 @@ class RootWidget : public ContainerWidget { return overlay_stack_widget_; } + void OnCancelCustom() override; + void UpdateLayout() override; + private: struct ButtonDef; struct Button; struct TextDef; + struct ImageDef; struct Text; + struct Image; + enum class MeterType { kLevel, kTrophy, kTickets, kTokens }; enum class VAlign { kTop, kCenter, kBottom }; - void UpdateForFocusedWindow(Widget* widget); - void OnCancelCustom() override; - void UpdateLayout() override; - auto AddButton(const ButtonDef& def) -> Button*; - auto AddText(const TextDef& def) -> Text*; - void StepPositions(float dt); - void AddMeter(float h_align, float x, int type, float r, float g, float b, - bool plus, const std::string& s); - auto AddCover(float h_align, VAlign v_align, float x, float y, float w, - float h, float o) -> Button*; + void UpdateForFocusedWindow_(Widget* widget); + auto AddButton_(const ButtonDef& def) -> Button*; + auto AddText_(const TextDef& def) -> Text*; + auto AddImage_(const ImageDef& def) -> Image*; + void StepPositions_(float dt); + void AddMeter_(MeterType type, float h_align, float r, float g, float b, + bool plus, const std::string& s); + auto AddCover_(float h_align, VAlign v_align, float x, float y, float w, + float h, float o) -> Button*; + ToolbarVisibility toolbar_visibility_{ToolbarVisibility::kInGame}; StackWidget* screen_stack_widget_{}; StackWidget* overlay_stack_widget_{}; float base_scale_{1.0f}; + millisecs_t update_time_{}; std::list