diff --git a/.efrocachemap b/.efrocachemap
index 71686050..81cfcf3b 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -53,6 +53,7 @@
"build/assets/ba_data/audio/assassinFall.ogg": "201d192debe8bda9e9dead28e9cc6939",
"build/assets/ba_data/audio/assassinHit1.ogg": "caaab755b159e399b121be1aec8f61b9",
"build/assets/ba_data/audio/assassinHit2.ogg": "a08ac94f02040af67bc46eff6a691a84",
+ "build/assets/ba_data/audio/aww.ogg": "80cf7a35a58ec6633d3c5440764de3f5",
"build/assets/ba_data/audio/bear1.ogg": "acddcf643e9fbf8d92eacf50992c81d0",
"build/assets/ba_data/audio/bear2.ogg": "74f7ce4f64e0fb943ab4ec34fdc83779",
"build/assets/ba_data/audio/bear3.ogg": "142d1f3d021c8639fbbc1a2ed0f3dc93",
@@ -95,6 +96,7 @@
"build/assets/ba_data/audio/cheer.ogg": "b9f1cce825ca00295b61b088f353715b",
"build/assets/ba_data/audio/click01.ogg": "204f93a3eb7d82cf8ca9172ee5f01c11",
"build/assets/ba_data/audio/corkPop.ogg": "7b0bdbb0cdaf40ec5adf3311ecfe620a",
+ "build/assets/ba_data/audio/corkPop2.ogg": "1ad5aff84af244acb2f3eaf95ddb7f6a",
"build/assets/ba_data/audio/cowboy1.ogg": "e12671599d7f6fa7e246b3ad4d83ad7d",
"build/assets/ba_data/audio/cowboy2.ogg": "0cd227e165a76bf829ac0e3ac129929c",
"build/assets/ba_data/audio/cowboy3.ogg": "9ce2c947a9ddcf9c6e793721d5b1eb66",
@@ -121,6 +123,7 @@
"build/assets/ba_data/audio/dingSmallHigh.ogg": "73ce9e68ef59847dc7621d38ed019c42",
"build/assets/ba_data/audio/dripity.ogg": "db450cee4e5241fbaa80a2874cb3cf7f",
"build/assets/ba_data/audio/drumRoll.ogg": "8d8234c10e7b9dee277a4e26aec3c9e1",
+ "build/assets/ba_data/audio/drumRollShort.ogg": "b4273cdd69c0b09bd1da5146a6b1bc76",
"build/assets/ba_data/audio/error.ogg": "a39731636b92282052e15eb5b9413816",
"build/assets/ba_data/audio/explosion01.ogg": "51bba8fc738410a61da8d535d8485a20",
"build/assets/ba_data/audio/explosion02.ogg": "b4ce0ea7abd1c5b52ad7a868f1029a2d",
@@ -147,6 +150,7 @@
"build/assets/ba_data/audio/frostyHit02.ogg": "c608e2ce52c872d701110367fa447656",
"build/assets/ba_data/audio/frostyHit03.ogg": "20860a3e0acd4104d2b62ac4a624749f",
"build/assets/ba_data/audio/fuse01.ogg": "f24744d8b4590b5898015dd98b2b9374",
+ "build/assets/ba_data/audio/gasp.ogg": "91b267abf7015cf36d263e961f83856b",
"build/assets/ba_data/audio/gladiator1.ogg": "528596df10aec80417a4df83325fc09d",
"build/assets/ba_data/audio/gladiator2.ogg": "a0f42843c7a73523e90ed32d766959f1",
"build/assets/ba_data/audio/gladiator3.ogg": "b3fa702291a4da6f439e72481177a5c2",
@@ -218,6 +222,7 @@
"build/assets/ba_data/audio/menuMusic.ogg": "b25ee0041baf71b08c7650ae9f4daab0",
"build/assets/ba_data/audio/metalHit.ogg": "ced0188b46245cd60b248fc7bc13b706",
"build/assets/ba_data/audio/metalSkid.ogg": "a069f5022be74229c008a19b3e00e64c",
+ "build/assets/ba_data/audio/nice.ogg": "fb362c97a244ed3a2c2f98c38691fb00",
"build/assets/ba_data/audio/ninjaAttack1.ogg": "82c98bc81f3e3e224ddd3151be918dd6",
"build/assets/ba_data/audio/ninjaAttack2.ogg": "1f0952bb9df1877fbb9e05b8649b63da",
"build/assets/ba_data/audio/ninjaAttack3.ogg": "e5edefe727e4fe2bcb16ab90edc36c39",
@@ -287,6 +292,7 @@
"build/assets/ba_data/audio/raceBeep1.ogg": "2d271c487cefbd67f15a21a0904b3e1f",
"build/assets/ba_data/audio/raceBeep2.ogg": "47cf9d039e19a3446444bfeb82b394b6",
"build/assets/ba_data/audio/refWhistle.ogg": "8ebbde5488b834e73f1d8ee4dad8b1f3",
+ "build/assets/ba_data/audio/revUp.ogg": "19862620186556029821e41b403f3a11",
"build/assets/ba_data/audio/robot1.ogg": "feed97d9abb23097d659b10c6276bab8",
"build/assets/ba_data/audio/robot2.ogg": "d9fc996427452f76b0527d5ca84983af",
"build/assets/ba_data/audio/robot3.ogg": "bf766b82d438323f89fe4688dd78f5ef",
@@ -395,7 +401,11 @@
"build/assets/ba_data/audio/wizardFall.ogg": "e422bd05ae30a28b02b1c55fb0c1fa00",
"build/assets/ba_data/audio/wizardHit1.ogg": "7686120c658c811064efda94ac3e90ca",
"build/assets/ba_data/audio/wizardHit2.ogg": "d245e07803be7158da49d4962ccf483a",
+ "build/assets/ba_data/audio/woo.ogg": "e84ef95cb1d97b65ff529687d6076da9",
+ "build/assets/ba_data/audio/woo2.ogg": "5dfb2e489f406d11772f96ded5611c4f",
+ "build/assets/ba_data/audio/woo3.ogg": "637878bb31decb36e6db7a5295eea933",
"build/assets/ba_data/audio/woodDebrisFall.ogg": "e163c84d87821e3e19ec8b0bf1fef9a7",
+ "build/assets/ba_data/audio/wow.ogg": "581d224d771195bde72548509cc1cc32",
"build/assets/ba_data/audio/wrestler1.ogg": "7486e02349206e8082b44105d0a1195c",
"build/assets/ba_data/audio/wrestler2.ogg": "0f197b1d7e6c2e0cc86e57d1b53581aa",
"build/assets/ba_data/audio/wrestler3.ogg": "911ad7c64018e8d5fa5f722a04de8837",
@@ -404,6 +414,7 @@
"build/assets/ba_data/audio/wrestlerFall.ogg": "3c6bb84fb09a0829fd60066b1807a16c",
"build/assets/ba_data/audio/wrestlerHit1.ogg": "1950d463514448069f0d3c0f00108eaa",
"build/assets/ba_data/audio/wrestlerHit2.ogg": "5b549fb2406fd72d1d0947fc8173cc08",
+ "build/assets/ba_data/audio/yeah.ogg": "2c55f21c39cf5f41a81317dec3f5d7fa",
"build/assets/ba_data/audio/zoeAttack01.ogg": "0b0536b8afba7cb773beffeaa2e4bb90",
"build/assets/ba_data/audio/zoeAttack02.ogg": "931a5b3d78e2322443fe1e51e6c25b99",
"build/assets/ba_data/audio/zoeAttack03.ogg": "e1d1f58f038bedda8c22fc518aa37c7e",
@@ -421,21 +432,21 @@
"build/assets/ba_data/audio/zoeOw.ogg": "b2d705c31c9dcc1efdc71394764c3beb",
"build/assets/ba_data/audio/zoePickup01.ogg": "e9366dc2d2b8ab8b0c4e2c14c02d0789",
"build/assets/ba_data/audio/zoeScream01.ogg": "903e0e45ee9b3373e9d9ce20c814374e",
- "build/assets/ba_data/data/langdata.json": "ce2f76ab5f36cbc0212d1b3c424eb954",
+ "build/assets/ba_data/data/langdata.json": "dbbd8f26d2f85c0b649d461e991b80cb",
"build/assets/ba_data/data/languages/arabic.json": "3c22e7b6d7b09a812a2e28b35c9e9241",
"build/assets/ba_data/data/languages/belarussian.json": "0b60a9d4496d1213c2d0b647d346ce30",
"build/assets/ba_data/data/languages/chinese.json": "fc45d2838b834889c06920ae7c2102fa",
"build/assets/ba_data/data/languages/chinesetraditional.json": "904b35b656c53f9830e406565edd5120",
- "build/assets/ba_data/data/languages/croatian.json": "1e541070309ff6be95b0c39940aa7e99",
+ "build/assets/ba_data/data/languages/croatian.json": "e131a87cf5783e0fbb3d211a927efe1a",
"build/assets/ba_data/data/languages/czech.json": "d18b7d1c6bf51fc81af4084ef0e69e3e",
"build/assets/ba_data/data/languages/danish.json": "8e57db30c5250df2abff14a822f83ea7",
- "build/assets/ba_data/data/languages/dutch.json": "f4e1e8e9231cda9d1bcc7e87a7f8821e",
- "build/assets/ba_data/data/languages/english.json": "b5917c3b975155e35fedb655dbd7568c",
+ "build/assets/ba_data/data/languages/dutch.json": "4085dec5af362cf068b494524ced3872",
+ "build/assets/ba_data/data/languages/english.json": "527d106870b0690cc39a80b88e60ab7a",
"build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880",
"build/assets/ba_data/data/languages/filipino.json": "3d9269a90a2fee164d0a7513c4f130a3",
"build/assets/ba_data/data/languages/french.json": "6d20655730b1017ef187fd828b91d43c",
- "build/assets/ba_data/data/languages/german.json": "a150dbb5c0f43984757f7db295d96203",
- "build/assets/ba_data/data/languages/gibberish.json": "df76e851aee59657b69e34efd54fee06",
+ "build/assets/ba_data/data/languages/german.json": "b92ec951b5a0ce4f73677051ca59a06b",
+ "build/assets/ba_data/data/languages/gibberish.json": "2569fe1b2f686670f825e2faaa8c5dc3",
"build/assets/ba_data/data/languages/greek.json": "d28d1092fbb00ed857cbd53124c0dc78",
"build/assets/ba_data/data/languages/hindi.json": "567e6976b3c72f891431ad7fcc62ab16",
"build/assets/ba_data/data/languages/hungarian.json": "9d88004a98f0fbe2ea72edd5e0b3002e",
@@ -451,7 +462,7 @@
"build/assets/ba_data/data/languages/russian.json": "fc64ed6b6356ea11385ee5c20748425a",
"build/assets/ba_data/data/languages/serbian.json": "623fa4129a1154c2f32ed7867e56ff6a",
"build/assets/ba_data/data/languages/slovak.json": "c11c29708b3742cdc2a92b4fa0d6d29f",
- "build/assets/ba_data/data/languages/spanish.json": "499b464318a8c9d1fb271cf480862b57",
+ "build/assets/ba_data/data/languages/spanish.json": "f8ab976d219e579546bb98b6d7fd12ce",
"build/assets/ba_data/data/languages/swedish.json": "3b179e7333183c70adb0811246b09959",
"build/assets/ba_data/data/languages/tamil.json": "ead39b864228696a9b0d19344bc4b5ec",
"build/assets/ba_data/data/languages/thai.json": "383540a1e9c7c131ac579f51afc87471",
@@ -1362,10 +1373,18 @@
"build/assets/ba_data/textures/chestIconMulti.ktx": "c026aa573aab141044b503437ffdaeea",
"build/assets/ba_data/textures/chestIconMulti.pvr": "3f41eaa108068751ba40e08964c45ec0",
"build/assets/ba_data/textures/chestIconMulti_preview.png": "47b6839bdc19ad9e3d3d6282ce0138dd",
+ "build/assets/ba_data/textures/chestIconTint.dds": "04c079c1a79d548ca02969e9934d591b",
+ "build/assets/ba_data/textures/chestIconTint.ktx": "ff39e97211bb71f59091aac2f51ebbed",
+ "build/assets/ba_data/textures/chestIconTint.pvr": "129b44259f8b62c0e1d4c08612b9c691",
+ "build/assets/ba_data/textures/chestIconTint_preview.png": "022f3f8bb8bc9c082dbb21958ec8722f",
"build/assets/ba_data/textures/chestIcon_preview.png": "d3046ccf2fedefa9ad766054e350a5d6",
"build/assets/ba_data/textures/chestOpenIcon.dds": "325e93c7147c3bf098857d0e3eff4a73",
"build/assets/ba_data/textures/chestOpenIcon.ktx": "562afa582aa621b946c22460e0caca61",
"build/assets/ba_data/textures/chestOpenIcon.pvr": "703c88d07b9ab22cbab647677eb05370",
+ "build/assets/ba_data/textures/chestOpenIconTint.dds": "c3003ed402d46da9d5c8351f028c5d95",
+ "build/assets/ba_data/textures/chestOpenIconTint.ktx": "d32fe2152a1bf6feff1f2416627b2023",
+ "build/assets/ba_data/textures/chestOpenIconTint.pvr": "bd665e8289bfd5c369c473e973dd2d65",
+ "build/assets/ba_data/textures/chestOpenIconTint_preview.png": "e5a2377c127998faaef83e06d4cc2584",
"build/assets/ba_data/textures/chestOpenIcon_preview.png": "40ae165451f052265d07ea62f7d08713",
"build/assets/ba_data/textures/circle.dds": "0f4c08ab481dcadce164227a146fdb62",
"build/assets/ba_data/textures/circle.ktx": "a6ff1b802324d1874aaa9c24050cfcf3",
@@ -2359,6 +2378,10 @@
"build/assets/ba_data/textures/sparks.ktx": "919af9b0b1b2e756c232c34bc15d506a",
"build/assets/ba_data/textures/sparks.pvr": "249e98bbadad09b59f0e8bdd46b1c9b1",
"build/assets/ba_data/textures/sparks_preview.png": "9b6b74000a0f56899dd09a956a864ec9",
+ "build/assets/ba_data/textures/spinner.dds": "54b0a3a695689974defcb7d0ddc40101",
+ "build/assets/ba_data/textures/spinner.ktx": "9e47f8b7dcca8f061bff6a04a30a2833",
+ "build/assets/ba_data/textures/spinner.pvr": "312c7ffa74c39d262d00971881ecd93b",
+ "build/assets/ba_data/textures/spinner_preview.png": "680af969ab856865098dfcf4fb8b6845",
"build/assets/ba_data/textures/star.dds": "e799668a604f3e680f7676994e894c1d",
"build/assets/ba_data/textures/star.ktx": "725da9d09a93c636bd825788f859c62d",
"build/assets/ba_data/textures/star.pvr": "bb68b74f455c36dcd7a40f9d38b5c74f",
@@ -4103,47 +4126,47 @@
"build/assets/windows/Win32/ucrtbased.dll": "bfd1180c269d3950b76f35a63655e9e1",
"build/assets/windows/Win32/vc_redist.x86.exe": "15a5f1f876503885adbdf5b3989b3718",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
- "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "390472d8d44b0a650796bfd6022d0549",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "23f27d6f139c653f23a15a0af7f7e02c",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "097b082f8f5abb18d824b2044da1de78",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "c027478f038af2faedcc4103e75bc39d",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "5c7f18d5ff9eb14421fe3743550ddf57",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "cef0bc0d149a4c433c2e4bee5b47414a",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "25546af5cf64a0e1e1d1f46b38298820",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "7f172d6c531c371837302f370f77db08",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "0d79db47846defaffaa0bf97ec6b23fc",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "45373e10bb60989787e415ae938756fe",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "0cc3834e81761efdc50babed9e5c7151",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "4e28c4abb0e7128d08f2b84a87d8a765",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "96b856d3db5d061527383b42e588a333",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "124afb5eec04365e42b059b0c2ec98e7",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "0271c53794bdb4a762ae7210d9828fb7",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "f8b3744f90503502618130c8d28f1aa4",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "33a0ae6f1ea5a0b0c60055ce01478488",
- "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "aad882eaf2230b89973e2cf4f13c9759",
- "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "33a0ae6f1ea5a0b0c60055ce01478488",
- "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "aad882eaf2230b89973e2cf4f13c9759",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "c20929c73caa78445525c5788b6963e0",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "0f21a43d99552df99e0d21c646e6e698",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "c20929c73caa78445525c5788b6963e0",
- "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "0f21a43d99552df99e0d21c646e6e698",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "01dab862a43d9e7c4ee4e49212442d42",
- "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "ae4e3f563892f6b9311c4b7284f28c11",
- "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "01dab862a43d9e7c4ee4e49212442d42",
- "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "ae4e3f563892f6b9311c4b7284f28c11",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "2f581e3dead7038e5b94bc096a7b8c80",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "4538ad0d6b4794de96fb78742ebcdc84",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "0d1a2f2066ae412549034e981ae39e2c",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "55180e66455f91d3af8f15c0c81607a4",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "bb2c15187840ef373ae79cdf1623d3b5",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "8ee1bb4440c364f9fa91791276d64b87",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "863f70d88ab9b3c7c6910d94e26b6f35",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "bb928df1aa05c84889b45f2a00544e64",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "2444899f2f36667dc27fcf5537fc76b6",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "efebe5318abe421031108d9908b4cb84",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "9b4928361ff7e3aa85ea6cca5f3903d8",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "9fad073f6fd49702ceb1ee5057c4f9de",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "ac52ed0994f33ea4997da21d0a7f6877",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "43f429f37c145fc2bf80c01dfd095a7e",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "ea87a5838b465c40b60ff911cccbe189",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "3cafa3287637bba5e56b363bb4fc8f9a",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "1fbe4ab96f25d20b421c8206c9230948",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "0a4dbc635f8e9b3f42e89dd2027bd9d3",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "6114856f2ab42ef6b20a1affdf53032d",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "bfa75071d993efc3853fa350c1de2bcb",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "3971d8deb2c33cfd18c4e3b338d48ec3",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "bd0c352337d72f020b431feca7974c3a",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "073b3690cfb03847ac3b3fe9c017e520",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "c7f0d9c4db7a67b4d72f3c406c585897",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "1c375e8003442dd3d059bc0baa260e61",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "40daac4bbc8990d5140f97e792bc4fb1",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "1c375e8003442dd3d059bc0baa260e61",
+ "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "40daac4bbc8990d5140f97e792bc4fb1",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "19af08729bed7249eaf9acd697966f3f",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "97d2486072bf3a83edb0250ea2d3e69e",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "19af08729bed7249eaf9acd697966f3f",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "97d2486072bf3a83edb0250ea2d3e69e",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "51884d81e2d7bdeb6b59a72f0247c8e1",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "36bb6f32ab12e2a46b82155a93b2e527",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "51884d81e2d7bdeb6b59a72f0247c8e1",
+ "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "36bb6f32ab12e2a46b82155a93b2e527",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "f629e97c70198ab3688f11ab4a2d1e97",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "a868c0116adf9f48f503b50b50c70cd8",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "04216607232edde485d139e9e96c6612",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "bdc158a2dfd592cd8dcb235a162a82fe",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "4a0340814c6034b39b2c08287935e26a",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "546ccf01f342463d46302b05c0171044",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "67bb2ff991e00433c1abba2044a0d21e",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "8653a52a8fd75c527c976abb9d223e90",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "794d258d59fd17a61752843a9a0551ad",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "06042d31df0ff9af96b99477162e2a91",
"src/ballistica/base/mgen/pyembed/binding_base_app.inc": "2d228e7c5578261d394f9c407f4becb1",
- "src/ballistica/classic/mgen/pyembed/binding_classic.inc": "fc09126750304b2761049aa9d93a951e",
+ "src/ballistica/classic/mgen/pyembed/binding_classic.inc": "8ab156122cde23d9718923abe1b4ae5b",
"src/ballistica/core/mgen/pyembed/binding_core.inc": "217c84a30f866aaca3a4373e82af7db2",
"src/ballistica/core/mgen/pyembed/env.inc": "f015d726b44d2922112fc14d9f146d8b",
"src/ballistica/core/mgen/python_modules_monolithic.h": "fb967ed1c7db0c77d8deb4f00a7103c5",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 705afdda..9ae37e12 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-### 1.7.37 (build 22155, api 9, 2024-12-31)
+### 1.7.37 (build 22178, api 9, 2025-01-11)
- Bumping api version to 9. As you'll see below, there's some UI changes that
will require a bit of work for any UI mods to adapt to. If your mods don't
touch UI stuff at all you can simply bump your api version and call it a day.
@@ -176,6 +176,21 @@
should use arrow keys for navigation. To update any old UI code, search for
and remove any 'claims_tab' arguments to UI calls since that argument no
longer exists.
+- Added a `get_unknown_type_fallback()` method to `dataclassio.IOMultiType`.
+ This be defined to allow multi-type data to be loadable even in the presence
+ of new types it doesn't recognize.
+- Added a `lossy` arg to `dataclassio.dataclass_from_dict()` and
+ `dataclassio.dataclass_from_json()`. Enum value fallbacks and the new
+ multitype fallbacks are now only applied when `lossy` is True. This also flags
+ the returned dataclass to prevent it from being serialized back out. Fallbacks
+ are useful for forward compatibility, but they are also dangerous in that they
+ can silently modify/destroy data, so this mechanism will hopefully help keep
+ them used safely.
+- Added a spinner widget (creatable via `bauiv1.spinnerwidget()`). This should
+ help things look more alive than the static 'loading...' text I've been using
+ in various places.
+- Tournament now award chests instead of tickets.
+- Tournaments are now free to enter if you are running this build or newer.
### 1.7.36 (build 21944, api 8, 2024-07-26)
- Wired up Tokens, BombSquad's new purchasable currency. The first thing these
diff --git a/ballisticakit-cmake/CMakeLists.txt b/ballisticakit-cmake/CMakeLists.txt
index ef00fe33..67ff5d14 100644
--- a/ballisticakit-cmake/CMakeLists.txt
+++ b/ballisticakit-cmake/CMakeLists.txt
@@ -772,6 +772,8 @@ set(BALLISTICA_SOURCES
${BA_SRC_ROOT}/ballistica/ui_v1/widget/row_widget.h
${BA_SRC_ROOT}/ballistica/ui_v1/widget/scroll_widget.cc
${BA_SRC_ROOT}/ballistica/ui_v1/widget/scroll_widget.h
+ ${BA_SRC_ROOT}/ballistica/ui_v1/widget/spinner_widget.cc
+ ${BA_SRC_ROOT}/ballistica/ui_v1/widget/spinner_widget.h
${BA_SRC_ROOT}/ballistica/ui_v1/widget/stack_widget.cc
${BA_SRC_ROOT}/ballistica/ui_v1/widget/stack_widget.h
${BA_SRC_ROOT}/ballistica/ui_v1/widget/text_widget.cc
diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
index 4041abab..55276809 100644
--- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
+++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
@@ -764,6 +764,8 @@
+
+
diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
index 578def22..43c15ad7 100644
--- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
+++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
@@ -1726,6 +1726,12 @@
ballistica\ui_v1\widget
+
+ ballistica\ui_v1\widget
+
+
+ ballistica\ui_v1\widget
+
ballistica\ui_v1\widget
diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
index 978f56bb..eb86bf1e 100644
--- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
+++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
@@ -759,6 +759,8 @@
+
+
diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
index 578def22..43c15ad7 100644
--- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
+++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
@@ -1726,6 +1726,12 @@
ballistica\ui_v1\widget
+
+ ballistica\ui_v1\widget
+
+
+ ballistica\ui_v1\widget
+
ballistica\ui_v1\widget
diff --git a/config/requirements.txt b/config/requirements.txt
index e4c0d992..5ea4a2a6 100644
--- a/config/requirements.txt
+++ b/config/requirements.txt
@@ -1,5 +1,5 @@
cpplint==2.0.0
-dmgbuild==1.6.2
+dmgbuild==1.6.4
filelock==3.16.1
furo==2024.8.6
mypy==1.14.1
diff --git a/src/assets/.asset_manifest_private.json b/src/assets/.asset_manifest_private.json
index c404d695..0c1da6e1 100644
--- a/src/assets/.asset_manifest_private.json
+++ b/src/assets/.asset_manifest_private.json
@@ -52,6 +52,7 @@
"ba_data/audio/assassinFall.ogg",
"ba_data/audio/assassinHit1.ogg",
"ba_data/audio/assassinHit2.ogg",
+ "ba_data/audio/aww.ogg",
"ba_data/audio/bear1.ogg",
"ba_data/audio/bear2.ogg",
"ba_data/audio/bear3.ogg",
@@ -94,6 +95,7 @@
"ba_data/audio/cheer.ogg",
"ba_data/audio/click01.ogg",
"ba_data/audio/corkPop.ogg",
+ "ba_data/audio/corkPop2.ogg",
"ba_data/audio/cowboy1.ogg",
"ba_data/audio/cowboy2.ogg",
"ba_data/audio/cowboy3.ogg",
@@ -120,6 +122,7 @@
"ba_data/audio/dingSmallHigh.ogg",
"ba_data/audio/dripity.ogg",
"ba_data/audio/drumRoll.ogg",
+ "ba_data/audio/drumRollShort.ogg",
"ba_data/audio/error.ogg",
"ba_data/audio/explosion01.ogg",
"ba_data/audio/explosion02.ogg",
@@ -146,6 +149,7 @@
"ba_data/audio/frostyHit02.ogg",
"ba_data/audio/frostyHit03.ogg",
"ba_data/audio/fuse01.ogg",
+ "ba_data/audio/gasp.ogg",
"ba_data/audio/gladiator1.ogg",
"ba_data/audio/gladiator2.ogg",
"ba_data/audio/gladiator3.ogg",
@@ -217,6 +221,7 @@
"ba_data/audio/menuMusic.ogg",
"ba_data/audio/metalHit.ogg",
"ba_data/audio/metalSkid.ogg",
+ "ba_data/audio/nice.ogg",
"ba_data/audio/ninjaAttack1.ogg",
"ba_data/audio/ninjaAttack2.ogg",
"ba_data/audio/ninjaAttack3.ogg",
@@ -286,6 +291,7 @@
"ba_data/audio/raceBeep1.ogg",
"ba_data/audio/raceBeep2.ogg",
"ba_data/audio/refWhistle.ogg",
+ "ba_data/audio/revUp.ogg",
"ba_data/audio/robot1.ogg",
"ba_data/audio/robot2.ogg",
"ba_data/audio/robot3.ogg",
@@ -394,7 +400,11 @@
"ba_data/audio/wizardFall.ogg",
"ba_data/audio/wizardHit1.ogg",
"ba_data/audio/wizardHit2.ogg",
+ "ba_data/audio/woo.ogg",
+ "ba_data/audio/woo2.ogg",
+ "ba_data/audio/woo3.ogg",
"ba_data/audio/woodDebrisFall.ogg",
+ "ba_data/audio/wow.ogg",
"ba_data/audio/wrestler1.ogg",
"ba_data/audio/wrestler2.ogg",
"ba_data/audio/wrestler3.ogg",
@@ -403,6 +413,7 @@
"ba_data/audio/wrestlerFall.ogg",
"ba_data/audio/wrestlerHit1.ogg",
"ba_data/audio/wrestlerHit2.ogg",
+ "ba_data/audio/yeah.ogg",
"ba_data/audio/zoeAttack01.ogg",
"ba_data/audio/zoeAttack02.ogg",
"ba_data/audio/zoeAttack03.ogg",
@@ -1401,10 +1412,18 @@
"ba_data/textures/chestIconMulti.ktx",
"ba_data/textures/chestIconMulti.pvr",
"ba_data/textures/chestIconMulti_preview.png",
+ "ba_data/textures/chestIconTint.dds",
+ "ba_data/textures/chestIconTint.ktx",
+ "ba_data/textures/chestIconTint.pvr",
+ "ba_data/textures/chestIconTint_preview.png",
"ba_data/textures/chestIcon_preview.png",
"ba_data/textures/chestOpenIcon.dds",
"ba_data/textures/chestOpenIcon.ktx",
"ba_data/textures/chestOpenIcon.pvr",
+ "ba_data/textures/chestOpenIconTint.dds",
+ "ba_data/textures/chestOpenIconTint.ktx",
+ "ba_data/textures/chestOpenIconTint.pvr",
+ "ba_data/textures/chestOpenIconTint_preview.png",
"ba_data/textures/chestOpenIcon_preview.png",
"ba_data/textures/circle.dds",
"ba_data/textures/circle.ktx",
@@ -2398,6 +2417,10 @@
"ba_data/textures/sparks.ktx",
"ba_data/textures/sparks.pvr",
"ba_data/textures/sparks_preview.png",
+ "ba_data/textures/spinner.dds",
+ "ba_data/textures/spinner.ktx",
+ "ba_data/textures/spinner.pvr",
+ "ba_data/textures/spinner_preview.png",
"ba_data/textures/star.dds",
"ba_data/textures/star.ktx",
"ba_data/textures/star.pvr",
diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json
index 84ab582b..bd9cbca3 100644
--- a/src/assets/.asset_manifest_public.json
+++ b/src/assets/.asset_manifest_public.json
@@ -77,6 +77,8 @@
"ba_data/python/baclassic/__pycache__/_appmode.cpython-312.opt-1.pyc",
"ba_data/python/baclassic/__pycache__/_appsubsystem.cpython-312.opt-1.pyc",
"ba_data/python/baclassic/__pycache__/_benchmark.cpython-312.opt-1.pyc",
+ "ba_data/python/baclassic/__pycache__/_chest.cpython-312.opt-1.pyc",
+ "ba_data/python/baclassic/__pycache__/_clienteffect.cpython-312.opt-1.pyc",
"ba_data/python/baclassic/__pycache__/_input.cpython-312.opt-1.pyc",
"ba_data/python/baclassic/__pycache__/_music.cpython-312.opt-1.pyc",
"ba_data/python/baclassic/__pycache__/_net.cpython-312.opt-1.pyc",
@@ -93,6 +95,8 @@
"ba_data/python/baclassic/_appmode.py",
"ba_data/python/baclassic/_appsubsystem.py",
"ba_data/python/baclassic/_benchmark.py",
+ "ba_data/python/baclassic/_chest.py",
+ "ba_data/python/baclassic/_clienteffect.py",
"ba_data/python/baclassic/_input.py",
"ba_data/python/baclassic/_music.py",
"ba_data/python/baclassic/_net.py",
@@ -107,6 +111,7 @@
"ba_data/python/bacommon/__pycache__/app.cpython-312.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/assets.cpython-312.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/bacloud.cpython-312.opt-1.pyc",
+ "ba_data/python/bacommon/__pycache__/bs.cpython-312.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/build.cpython-312.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/cloud.cpython-312.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/loggercontrol.cpython-312.opt-1.pyc",
@@ -118,6 +123,7 @@
"ba_data/python/bacommon/app.py",
"ba_data/python/bacommon/assets.py",
"ba_data/python/bacommon/bacloud.py",
+ "ba_data/python/bacommon/bs.py",
"ba_data/python/bacommon/build.py",
"ba_data/python/bacommon/cloud.py",
"ba_data/python/bacommon/loggercontrol.py",
@@ -603,6 +609,7 @@
"ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-312.opt-1.pyc",
"ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-312.opt-1.pyc",
"ba_data/python/efro/dataclassio/__pycache__/extras.cpython-312.opt-1.pyc",
+ "ba_data/python/efro/dataclassio/__pycache__/templatemultitype.cpython-312.opt-1.pyc",
"ba_data/python/efro/dataclassio/_api.py",
"ba_data/python/efro/dataclassio/_base.py",
"ba_data/python/efro/dataclassio/_inputter.py",
@@ -610,6 +617,7 @@
"ba_data/python/efro/dataclassio/_pathcapture.py",
"ba_data/python/efro/dataclassio/_prep.py",
"ba_data/python/efro/dataclassio/extras.py",
+ "ba_data/python/efro/dataclassio/templatemultitype.py",
"ba_data/python/efro/debug.py",
"ba_data/python/efro/error.py",
"ba_data/python/efro/logging.py",
diff --git a/src/assets/Makefile b/src/assets/Makefile
index 414ce46a..80815e96 100644
--- a/src/assets/Makefile
+++ b/src/assets/Makefile
@@ -206,6 +206,8 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/baclassic/_appmode.py \
$(BUILD_DIR)/ba_data/python/baclassic/_appsubsystem.py \
$(BUILD_DIR)/ba_data/python/baclassic/_benchmark.py \
+ $(BUILD_DIR)/ba_data/python/baclassic/_chest.py \
+ $(BUILD_DIR)/ba_data/python/baclassic/_clienteffect.py \
$(BUILD_DIR)/ba_data/python/baclassic/_input.py \
$(BUILD_DIR)/ba_data/python/baclassic/_music.py \
$(BUILD_DIR)/ba_data/python/baclassic/_net.py \
@@ -486,6 +488,8 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_appmode.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_appsubsystem.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_benchmark.cpython-312.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_chest.cpython-312.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_clienteffect.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_input.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_music.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_net.cpython-312.opt-1.pyc \
@@ -738,6 +742,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
$(BUILD_DIR)/ba_data/python/bacommon/app.py \
$(BUILD_DIR)/ba_data/python/bacommon/assets.py \
$(BUILD_DIR)/ba_data/python/bacommon/bacloud.py \
+ $(BUILD_DIR)/ba_data/python/bacommon/bs.py \
$(BUILD_DIR)/ba_data/python/bacommon/build.py \
$(BUILD_DIR)/ba_data/python/bacommon/cloud.py \
$(BUILD_DIR)/ba_data/python/bacommon/loggercontrol.py \
@@ -759,6 +764,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
$(BUILD_DIR)/ba_data/python/efro/dataclassio/_pathcapture.py \
$(BUILD_DIR)/ba_data/python/efro/dataclassio/_prep.py \
$(BUILD_DIR)/ba_data/python/efro/dataclassio/extras.py \
+ $(BUILD_DIR)/ba_data/python/efro/dataclassio/templatemultitype.py \
$(BUILD_DIR)/ba_data/python/efro/debug.py \
$(BUILD_DIR)/ba_data/python/efro/error.py \
$(BUILD_DIR)/ba_data/python/efro/logging.py \
@@ -778,6 +784,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
$(BUILD_DIR)/ba_data/python/bacommon/__pycache__/app.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bacommon/__pycache__/assets.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bacommon/__pycache__/bacloud.cpython-312.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/bs.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bacommon/__pycache__/build.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bacommon/__pycache__/cloud.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bacommon/__pycache__/loggercontrol.cpython-312.opt-1.pyc \
@@ -799,6 +806,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
$(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/extras.cpython-312.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/templatemultitype.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/efro/__pycache__/debug.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/efro/__pycache__/error.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/efro/__pycache__/logging.cpython-312.opt-1.pyc \
@@ -5259,6 +5267,7 @@ AUDIO_TARGETS = \
$(BUILD_DIR)/ba_data/audio/assassinFall.ogg \
$(BUILD_DIR)/ba_data/audio/assassinHit1.ogg \
$(BUILD_DIR)/ba_data/audio/assassinHit2.ogg \
+ $(BUILD_DIR)/ba_data/audio/aww.ogg \
$(BUILD_DIR)/ba_data/audio/bear1.ogg \
$(BUILD_DIR)/ba_data/audio/bear2.ogg \
$(BUILD_DIR)/ba_data/audio/bear3.ogg \
@@ -5301,6 +5310,7 @@ AUDIO_TARGETS = \
$(BUILD_DIR)/ba_data/audio/cheer.ogg \
$(BUILD_DIR)/ba_data/audio/click01.ogg \
$(BUILD_DIR)/ba_data/audio/corkPop.ogg \
+ $(BUILD_DIR)/ba_data/audio/corkPop2.ogg \
$(BUILD_DIR)/ba_data/audio/cowboy1.ogg \
$(BUILD_DIR)/ba_data/audio/cowboy2.ogg \
$(BUILD_DIR)/ba_data/audio/cowboy3.ogg \
@@ -5327,6 +5337,7 @@ AUDIO_TARGETS = \
$(BUILD_DIR)/ba_data/audio/dingSmallHigh.ogg \
$(BUILD_DIR)/ba_data/audio/dripity.ogg \
$(BUILD_DIR)/ba_data/audio/drumRoll.ogg \
+ $(BUILD_DIR)/ba_data/audio/drumRollShort.ogg \
$(BUILD_DIR)/ba_data/audio/error.ogg \
$(BUILD_DIR)/ba_data/audio/explosion01.ogg \
$(BUILD_DIR)/ba_data/audio/explosion02.ogg \
@@ -5353,6 +5364,7 @@ AUDIO_TARGETS = \
$(BUILD_DIR)/ba_data/audio/frostyHit02.ogg \
$(BUILD_DIR)/ba_data/audio/frostyHit03.ogg \
$(BUILD_DIR)/ba_data/audio/fuse01.ogg \
+ $(BUILD_DIR)/ba_data/audio/gasp.ogg \
$(BUILD_DIR)/ba_data/audio/gladiator1.ogg \
$(BUILD_DIR)/ba_data/audio/gladiator2.ogg \
$(BUILD_DIR)/ba_data/audio/gladiator3.ogg \
@@ -5424,6 +5436,7 @@ AUDIO_TARGETS = \
$(BUILD_DIR)/ba_data/audio/menuMusic.ogg \
$(BUILD_DIR)/ba_data/audio/metalHit.ogg \
$(BUILD_DIR)/ba_data/audio/metalSkid.ogg \
+ $(BUILD_DIR)/ba_data/audio/nice.ogg \
$(BUILD_DIR)/ba_data/audio/ninjaAttack1.ogg \
$(BUILD_DIR)/ba_data/audio/ninjaAttack2.ogg \
$(BUILD_DIR)/ba_data/audio/ninjaAttack3.ogg \
@@ -5493,6 +5506,7 @@ AUDIO_TARGETS = \
$(BUILD_DIR)/ba_data/audio/raceBeep1.ogg \
$(BUILD_DIR)/ba_data/audio/raceBeep2.ogg \
$(BUILD_DIR)/ba_data/audio/refWhistle.ogg \
+ $(BUILD_DIR)/ba_data/audio/revUp.ogg \
$(BUILD_DIR)/ba_data/audio/robot1.ogg \
$(BUILD_DIR)/ba_data/audio/robot2.ogg \
$(BUILD_DIR)/ba_data/audio/robot3.ogg \
@@ -5601,7 +5615,11 @@ AUDIO_TARGETS = \
$(BUILD_DIR)/ba_data/audio/wizardFall.ogg \
$(BUILD_DIR)/ba_data/audio/wizardHit1.ogg \
$(BUILD_DIR)/ba_data/audio/wizardHit2.ogg \
+ $(BUILD_DIR)/ba_data/audio/woo.ogg \
+ $(BUILD_DIR)/ba_data/audio/woo2.ogg \
+ $(BUILD_DIR)/ba_data/audio/woo3.ogg \
$(BUILD_DIR)/ba_data/audio/woodDebrisFall.ogg \
+ $(BUILD_DIR)/ba_data/audio/wow.ogg \
$(BUILD_DIR)/ba_data/audio/wrestler1.ogg \
$(BUILD_DIR)/ba_data/audio/wrestler2.ogg \
$(BUILD_DIR)/ba_data/audio/wrestler3.ogg \
@@ -5610,6 +5628,7 @@ AUDIO_TARGETS = \
$(BUILD_DIR)/ba_data/audio/wrestlerFall.ogg \
$(BUILD_DIR)/ba_data/audio/wrestlerHit1.ogg \
$(BUILD_DIR)/ba_data/audio/wrestlerHit2.ogg \
+ $(BUILD_DIR)/ba_data/audio/yeah.ogg \
$(BUILD_DIR)/ba_data/audio/zoeAttack01.ogg \
$(BUILD_DIR)/ba_data/audio/zoeAttack02.ogg \
$(BUILD_DIR)/ba_data/audio/zoeAttack03.ogg \
@@ -5723,7 +5742,9 @@ TEX2D_DDS_TARGETS = \
$(BUILD_DIR)/ba_data/textures/chestIcon.dds \
$(BUILD_DIR)/ba_data/textures/chestIconEmpty.dds \
$(BUILD_DIR)/ba_data/textures/chestIconMulti.dds \
+ $(BUILD_DIR)/ba_data/textures/chestIconTint.dds \
$(BUILD_DIR)/ba_data/textures/chestOpenIcon.dds \
+ $(BUILD_DIR)/ba_data/textures/chestOpenIconTint.dds \
$(BUILD_DIR)/ba_data/textures/circle.dds \
$(BUILD_DIR)/ba_data/textures/circleNoAlpha.dds \
$(BUILD_DIR)/ba_data/textures/circleOutline.dds \
@@ -5972,6 +5993,7 @@ TEX2D_DDS_TARGETS = \
$(BUILD_DIR)/ba_data/textures/softRect2.dds \
$(BUILD_DIR)/ba_data/textures/softRectVertical.dds \
$(BUILD_DIR)/ba_data/textures/sparks.dds \
+ $(BUILD_DIR)/ba_data/textures/spinner.dds \
$(BUILD_DIR)/ba_data/textures/star.dds \
$(BUILD_DIR)/ba_data/textures/startButton.dds \
$(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor.dds \
@@ -6135,7 +6157,9 @@ TEX2D_PVR_TARGETS = \
$(BUILD_DIR)/ba_data/textures/chestIcon.pvr \
$(BUILD_DIR)/ba_data/textures/chestIconEmpty.pvr \
$(BUILD_DIR)/ba_data/textures/chestIconMulti.pvr \
+ $(BUILD_DIR)/ba_data/textures/chestIconTint.pvr \
$(BUILD_DIR)/ba_data/textures/chestOpenIcon.pvr \
+ $(BUILD_DIR)/ba_data/textures/chestOpenIconTint.pvr \
$(BUILD_DIR)/ba_data/textures/circle.pvr \
$(BUILD_DIR)/ba_data/textures/circleNoAlpha.pvr \
$(BUILD_DIR)/ba_data/textures/circleOutline.pvr \
@@ -6384,6 +6408,7 @@ TEX2D_PVR_TARGETS = \
$(BUILD_DIR)/ba_data/textures/softRect2.pvr \
$(BUILD_DIR)/ba_data/textures/softRectVertical.pvr \
$(BUILD_DIR)/ba_data/textures/sparks.pvr \
+ $(BUILD_DIR)/ba_data/textures/spinner.pvr \
$(BUILD_DIR)/ba_data/textures/star.pvr \
$(BUILD_DIR)/ba_data/textures/startButton.pvr \
$(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor.pvr \
@@ -6547,7 +6572,9 @@ TEX2D_KTX_TARGETS = \
$(BUILD_DIR)/ba_data/textures/chestIcon.ktx \
$(BUILD_DIR)/ba_data/textures/chestIconEmpty.ktx \
$(BUILD_DIR)/ba_data/textures/chestIconMulti.ktx \
+ $(BUILD_DIR)/ba_data/textures/chestIconTint.ktx \
$(BUILD_DIR)/ba_data/textures/chestOpenIcon.ktx \
+ $(BUILD_DIR)/ba_data/textures/chestOpenIconTint.ktx \
$(BUILD_DIR)/ba_data/textures/circle.ktx \
$(BUILD_DIR)/ba_data/textures/circleNoAlpha.ktx \
$(BUILD_DIR)/ba_data/textures/circleOutline.ktx \
@@ -6796,6 +6823,7 @@ TEX2D_KTX_TARGETS = \
$(BUILD_DIR)/ba_data/textures/softRect2.ktx \
$(BUILD_DIR)/ba_data/textures/softRectVertical.ktx \
$(BUILD_DIR)/ba_data/textures/sparks.ktx \
+ $(BUILD_DIR)/ba_data/textures/spinner.ktx \
$(BUILD_DIR)/ba_data/textures/star.ktx \
$(BUILD_DIR)/ba_data/textures/startButton.ktx \
$(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor.ktx \
@@ -6958,7 +6986,9 @@ TEX2D_PREVIEW_PNG_TARGETS = \
$(BUILD_DIR)/ba_data/textures/characterIconMask_preview.png \
$(BUILD_DIR)/ba_data/textures/chestIconEmpty_preview.png \
$(BUILD_DIR)/ba_data/textures/chestIconMulti_preview.png \
+ $(BUILD_DIR)/ba_data/textures/chestIconTint_preview.png \
$(BUILD_DIR)/ba_data/textures/chestIcon_preview.png \
+ $(BUILD_DIR)/ba_data/textures/chestOpenIconTint_preview.png \
$(BUILD_DIR)/ba_data/textures/chestOpenIcon_preview.png \
$(BUILD_DIR)/ba_data/textures/circleNoAlpha_preview.png \
$(BUILD_DIR)/ba_data/textures/circleOutlineNoAlpha_preview.png \
@@ -7208,6 +7238,7 @@ TEX2D_PREVIEW_PNG_TARGETS = \
$(BUILD_DIR)/ba_data/textures/softRectVertical_preview.png \
$(BUILD_DIR)/ba_data/textures/softRect_preview.png \
$(BUILD_DIR)/ba_data/textures/sparks_preview.png \
+ $(BUILD_DIR)/ba_data/textures/spinner_preview.png \
$(BUILD_DIR)/ba_data/textures/star_preview.png \
$(BUILD_DIR)/ba_data/textures/startButton_preview.png \
$(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor_preview.png \
diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py
index cd5d5744..b2d08ac4 100644
--- a/src/assets/ba_data/python/babase/_app.py
+++ b/src/assets/ba_data/python/babase/_app.py
@@ -68,9 +68,10 @@ class App:
health_monitor: AppHealthMonitor
# How long we allow shutdown tasks to run before killing them.
- # Currently the entire app hard-exits if shutdown takes 10 seconds,
- # so we need to keep it under that.
- SHUTDOWN_TASK_TIMEOUT_SECONDS = 5
+ # Currently the entire app hard-exits if shutdown takes 15 seconds,
+ # so we need to keep it under that. Staying above 10 should allow
+ # 10 second network timeouts to happen though.
+ SHUTDOWN_TASK_TIMEOUT_SECONDS = 12
class State(Enum):
"""High level state the app can be in."""
diff --git a/src/assets/ba_data/python/babase/_apputils.py b/src/assets/ba_data/python/babase/_apputils.py
index 29545f40..9d58da48 100644
--- a/src/assets/ba_data/python/babase/_apputils.py
+++ b/src/assets/ba_data/python/babase/_apputils.py
@@ -30,7 +30,9 @@ def utc_now_cloud() -> datetime.datetime:
Applies offsets pulled from server communication/etc.
"""
- # FIXME - do something smart here.
+ # TODO: wire this up. Just using local time for now. Make sure that
+ # BaseFeatureSet::TimeSinceEpochCloudSeconds() and this are synced
+ # up.
return utc_now()
diff --git a/src/assets/ba_data/python/baclassic/__init__.py b/src/assets/ba_data/python/baclassic/__init__.py
index 3367c4b7..1c428fb2 100644
--- a/src/assets/ba_data/python/baclassic/__init__.py
+++ b/src/assets/ba_data/python/baclassic/__init__.py
@@ -2,18 +2,11 @@
#
"""Components for the classic BombSquad experience.
-This package is used as a dumping ground for functionality that is
-necessary to keep classic BombSquad working, but which may no longer be
-the best way to do things going forward.
-
-New code should try to avoid using code from here when possible.
-
-Functionality in this package should be exposed through the
-ClassicAppSubsystem. This allows type-checked code to go through the
-babase.app.classic singleton which forces it to explicitly handle the
-possibility of babase.app.classic being None. When code instead imports
-classic submodules directly, it is much harder to make it cleanly handle
-classic not being present.
+This package/feature-set contains functionality related to the classic
+BombSquad experience. Note that much legacy BombSquad code is still a
+bit tangled and thus this feature-set is largely inseperable from
+scenev1 and uiv1. Future feature-sets will be designed in a more modular
+way.
"""
# ba_meta require api 9
@@ -29,8 +22,16 @@ from efro.util import set_canonical_module_names
from baclassic._appmode import ClassicAppMode
from baclassic._appsubsystem import ClassicAppSubsystem
from baclassic._achievement import Achievement, AchievementSubsystem
+from baclassic._chest import (
+ ChestAppearanceDisplayInfo,
+ CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
+ CHEST_APPEARANCE_DISPLAY_INFOS,
+)
__all__ = [
+ 'ChestAppearanceDisplayInfo',
+ 'CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT',
+ 'CHEST_APPEARANCE_DISPLAY_INFOS',
'ClassicAppMode',
'ClassicAppSubsystem',
'Achievement',
diff --git a/src/assets/ba_data/python/baclassic/_appmode.py b/src/assets/ba_data/python/baclassic/_appmode.py
index e6cb68a1..c4118572 100644
--- a/src/assets/ba_data/python/baclassic/_appmode.py
+++ b/src/assets/ba_data/python/baclassic/_appmode.py
@@ -197,9 +197,9 @@ class ClassicAppMode(babase.AppMode):
if account is None:
self._account_data_sub = None
_baclassic.set_root_ui_account_values(
- tickets_text='',
- tokens_text='',
- league_rank_text='',
+ tickets=-1,
+ tokens=-1,
+ league_rank=-1,
league_type='',
achievements_percent_text='',
level_text='',
@@ -250,7 +250,7 @@ class ClassicAppMode(babase.AppMode):
print(f'GOT SUB TEST UPDATE: {val}')
def _on_classic_account_data_change(
- self, val: bacommon.cloud.BSClassicAccountLiveData
+ self, val: bacommon.bs.ClassicAccountLiveData
) -> None:
# print('ACCOUNT CHANGED:', val)
achp = round(val.achievements / max(val.achievements_total, 1) * 100.0)
@@ -264,11 +264,9 @@ class ClassicAppMode(babase.AppMode):
chest3 = val.chests.get('3')
_baclassic.set_root_ui_account_values(
- tickets_text=str(val.tickets),
- tokens_text=str(val.tokens),
- league_rank_text=(
- '-' if val.league_rank is None else f'#{val.league_rank}'
- ),
+ tickets=val.tickets,
+ tokens=val.tokens,
+ league_rank=(-1 if val.league_rank is None else val.league_rank),
league_type=(
'' if val.league_type is None else val.league_type.value
),
@@ -292,17 +290,35 @@ class ClassicAppMode(babase.AppMode):
chest_0_unlock_time=(
-1.0 if chest0 is None else chest0.unlock_time.timestamp()
),
- chest_1_unlock_time=-1.0,
- chest_2_unlock_time=-1.0,
- chest_3_unlock_time=-1.0,
+ chest_1_unlock_time=(
+ -1.0 if chest1 is None else chest1.unlock_time.timestamp()
+ ),
+ chest_2_unlock_time=(
+ -1.0 if chest2 is None else chest2.unlock_time.timestamp()
+ ),
+ chest_3_unlock_time=(
+ -1.0 if chest3 is None else chest3.unlock_time.timestamp()
+ ),
chest_0_ad_allow_time=(
-1.0
if chest0 is None or chest0.ad_allow_time is None
else chest0.ad_allow_time.timestamp()
),
- chest_1_ad_allow_time=-1.0,
- chest_2_ad_allow_time=-1.0,
- chest_3_ad_allow_time=-1.0,
+ chest_1_ad_allow_time=(
+ -1.0
+ if chest1 is None or chest1.ad_allow_time is None
+ else chest1.ad_allow_time.timestamp()
+ ),
+ chest_2_ad_allow_time=(
+ -1.0
+ if chest2 is None or chest2.ad_allow_time is None
+ else chest2.ad_allow_time.timestamp()
+ ),
+ chest_3_ad_allow_time=(
+ -1.0
+ if chest3 is None or chest3.ad_allow_time is None
+ else chest3.ad_allow_time.timestamp()
+ ),
)
# Note that we have values and updated faded state accordingly.
diff --git a/src/assets/ba_data/python/baclassic/_appsubsystem.py b/src/assets/ba_data/python/baclassic/_appsubsystem.py
index 99702dc2..f2d0527f 100644
--- a/src/assets/ba_data/python/baclassic/_appsubsystem.py
+++ b/src/assets/ba_data/python/baclassic/_appsubsystem.py
@@ -3,10 +3,10 @@
"""Provides classic app subsystem."""
from __future__ import annotations
-from typing import TYPE_CHECKING, override
import random
import logging
import weakref
+from typing import TYPE_CHECKING, override, assert_never
from efro.dataclassio import dataclass_from_dict
import babase
@@ -26,6 +26,7 @@ from baclassic import _input
if TYPE_CHECKING:
from typing import Callable, Any, Sequence
+ import bacommon.bs
from bascenev1lib.actor import spazappearance
from bauiv1lib.party import PartyWindow
@@ -509,11 +510,36 @@ class ClassicAppSubsystem(babase.AppSubsystem):
request, 'post', data, callback, response_type
).start()
- def get_tournament_prize_strings(self, entry: dict[str, Any]) -> list[str]:
+ def set_tournament_prize_image(
+ self, entry: dict[str, Any], index: int, image: bauiv1.Widget
+ ) -> None:
"""Given a tournament entry, return strings for its prize levels."""
from baclassic import _tournament
- return _tournament.get_tournament_prize_strings(entry)
+ return _tournament.set_tournament_prize_chest_image(entry, index, image)
+
+ def create_in_game_tournament_prize_image(
+ self,
+ entry: dict[str, Any],
+ index: int,
+ position: tuple[float, float],
+ ) -> None:
+ """Given a tournament entry, return strings for its prize levels."""
+ from baclassic import _tournament
+
+ _tournament.create_in_game_tournament_prize_image(
+ entry, index, position
+ )
+
+ def get_tournament_prize_strings(
+ self, entry: dict[str, Any], include_tickets: bool
+ ) -> list[str]:
+ """Given a tournament entry, return strings for its prize levels."""
+ from baclassic import _tournament
+
+ return _tournament.get_tournament_prize_strings(
+ entry, include_tickets=include_tickets
+ )
def getcampaign(self, name: str) -> bascenev1.Campaign:
"""Return a campaign by name."""
@@ -852,3 +878,48 @@ class ClassicAppSubsystem(babase.AppSubsystem):
is_top_level=True,
suppress_warning=True,
)
+
+ @staticmethod
+ def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
+ """Run client effects sent from the master server."""
+ from baclassic._clienteffect import run_bs_client_effects
+
+ run_bs_client_effects(effects)
+
+ @staticmethod
+ def basic_client_ui_button_label_str(
+ label: bacommon.bs.BasicClientUI.ButtonLabel,
+ ) -> babase.Lstr:
+ """Given a client-ui label, return an Lstr."""
+ import bacommon.bs
+
+ cls = bacommon.bs.BasicClientUI.ButtonLabel
+ if label is cls.UNKNOWN:
+ # Server should not be sending us unknown stuff; make noise
+ # if they do.
+ logging.error(
+ 'Got BasicClientUI.ButtonLabel.UNKNOWN; should not happen.'
+ )
+ return babase.Lstr(value='')
+
+ rsrc: str | None = None
+ if label is cls.OK:
+ rsrc = 'okText'
+ elif label is cls.APPLY:
+ rsrc = 'applyText'
+ elif label is cls.CANCEL:
+ rsrc = 'cancelText'
+ elif label is cls.ACCEPT:
+ rsrc = 'gatherWindow.partyInviteAcceptText'
+ elif label is cls.DECLINE:
+ rsrc = 'gatherWindow.partyInviteDeclineText'
+ elif label is cls.IGNORE:
+ rsrc = 'gatherWindow.partyInviteIgnoreText'
+ elif label is cls.CLAIM:
+ rsrc = 'claimText'
+ elif label is cls.DISCARD:
+ rsrc = 'discardText'
+ else:
+ assert_never(label)
+
+ return babase.Lstr(resource=rsrc)
diff --git a/src/assets/ba_data/python/baclassic/_chest.py b/src/assets/ba_data/python/baclassic/_chest.py
new file mode 100644
index 00000000..b5e18ceb
--- /dev/null
+++ b/src/assets/ba_data/python/baclassic/_chest.py
@@ -0,0 +1,91 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Chest related functionality."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
+
+from bacommon.bs import ClassicChestAppearance
+
+if TYPE_CHECKING:
+ pass
+
+
+@dataclass
+class ChestAppearanceDisplayInfo:
+ """Info about how to locally display chest appearances."""
+
+ # NOTE TO SELF: Don't rename these attrs; the C++ layer is hard
+ # coded to look for them.
+
+ texclosed: str
+ texclosedtint: str
+ texopen: str
+ texopentint: str
+ color: tuple[float, float, float]
+ tint: tuple[float, float, float]
+ tint2: tuple[float, float, float]
+
+
+# Info for chest types we know how to draw. Anything not found in here
+# should fall back to the DEFAULT entry.
+CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT = ChestAppearanceDisplayInfo(
+ texclosed='chestIcon',
+ texclosedtint='chestIconTint',
+ texopen='chestOpenIcon',
+ texopentint='chestOpenIconTint',
+ color=(1, 1, 1),
+ tint=(1, 1, 1),
+ tint2=(1, 1, 1),
+)
+
+CHEST_APPEARANCE_DISPLAY_INFOS: dict[
+ ClassicChestAppearance, ChestAppearanceDisplayInfo
+] = {
+ ClassicChestAppearance.L2: ChestAppearanceDisplayInfo(
+ texclosed='chestIcon',
+ texclosedtint='chestIconTint',
+ texopen='chestOpenIcon',
+ texopentint='chestOpenIconTint',
+ color=(0.8, 1.0, 0.93),
+ tint=(0.65, 1.0, 0.8),
+ tint2=(0.65, 1.0, 0.8),
+ ),
+ ClassicChestAppearance.L3: ChestAppearanceDisplayInfo(
+ texclosed='chestIcon',
+ texclosedtint='chestIconTint',
+ texopen='chestOpenIcon',
+ texopentint='chestOpenIconTint',
+ color=(0.75, 0.9, 1.3),
+ tint=(0.7, 1, 1.9),
+ tint2=(0.7, 1, 1.9),
+ ),
+ ClassicChestAppearance.L4: ChestAppearanceDisplayInfo(
+ texclosed='chestIcon',
+ texclosedtint='chestIconTint',
+ texopen='chestOpenIcon',
+ texopentint='chestOpenIconTint',
+ color=(0.7, 1.0, 1.4),
+ tint=(1.4, 1.6, 2.0),
+ tint2=(1.4, 1.6, 2.0),
+ ),
+ ClassicChestAppearance.L5: ChestAppearanceDisplayInfo(
+ texclosed='chestIcon',
+ texclosedtint='chestIconTint',
+ texopen='chestOpenIcon',
+ texopentint='chestOpenIconTint',
+ color=(0.75, 0.5, 2.4),
+ tint=(1.0, 0.8, 0.0),
+ tint2=(1.0, 0.8, 0.0),
+ ),
+ ClassicChestAppearance.L6: ChestAppearanceDisplayInfo(
+ texclosed='chestIcon',
+ texclosedtint='chestIconTint',
+ texopen='chestOpenIcon',
+ texopentint='chestOpenIconTint',
+ color=(1.1, 0.8, 0.0),
+ tint=(2, 2, 2),
+ tint2=(2, 2, 2),
+ ),
+}
diff --git a/src/assets/ba_data/python/baclassic/_clienteffect.py b/src/assets/ba_data/python/baclassic/_clienteffect.py
new file mode 100644
index 00000000..38b0d9cc
--- /dev/null
+++ b/src/assets/ba_data/python/baclassic/_clienteffect.py
@@ -0,0 +1,77 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Functionality related to running client-effects from the master server."""
+
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING, assert_never
+
+from efro.util import strict_partial
+
+import bacommon.bs
+import bauiv1
+
+if TYPE_CHECKING:
+ pass
+
+
+def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
+ """Run effects."""
+ # pylint: disable=too-many-branches
+
+ delay = 0.0
+ for effect in effects:
+ if isinstance(effect, bacommon.bs.ClientEffectScreenMessage):
+ textfin = bauiv1.Lstr(
+ translate=('serverResponses', effect.message)
+ ).evaluate()
+ if effect.subs is not None:
+ # Should always be even.
+ assert len(effect.subs) % 2 == 0
+ for j in range(0, len(effect.subs) - 1, 2):
+ textfin = textfin.replace(
+ effect.subs[j],
+ effect.subs[j + 1],
+ )
+ bauiv1.apptimer(
+ delay,
+ strict_partial(
+ bauiv1.screenmessage, textfin, color=effect.color
+ ),
+ )
+
+ elif isinstance(effect, bacommon.bs.ClientEffectSound):
+ smcls = bacommon.bs.ClientEffectSound.Sound
+ soundfile: str | None = None
+ if effect.sound is smcls.UNKNOWN:
+ # Server should avoid sending us sounds we don't
+ # support. Make some noise if it happens.
+ logging.error('Got unrecognized bacommon.bs.ClientEffectSound.')
+ elif effect.sound is smcls.CASH_REGISTER:
+ soundfile = 'cashRegister'
+ elif effect.sound is smcls.ERROR:
+ soundfile = 'error'
+ elif effect.sound is smcls.POWER_DOWN:
+ soundfile = 'powerdown01'
+ elif effect.sound is smcls.GUN_COCKING:
+ soundfile = 'gunCocking'
+ else:
+ assert_never(effect.sound)
+ if soundfile is not None:
+ bauiv1.apptimer(
+ delay,
+ strict_partial(
+ bauiv1.getsound(soundfile).play, volume=effect.volume
+ ),
+ )
+
+ elif isinstance(effect, bacommon.bs.ClientEffectDelay):
+ delay += effect.seconds
+ else:
+ # Server should not send us stuff we can't digest. Make
+ # some noise if it happens.
+ logging.error(
+ 'Got unrecognized bacommon.bs.ClientEffect;'
+ ' should not happen.'
+ )
diff --git a/src/assets/ba_data/python/baclassic/_tournament.py b/src/assets/ba_data/python/baclassic/_tournament.py
index 242ba527..e71d9608 100644
--- a/src/assets/ba_data/python/baclassic/_tournament.py
+++ b/src/assets/ba_data/python/baclassic/_tournament.py
@@ -6,13 +6,23 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from bacommon.bs import ClassicChestAppearance
import babase
+import bauiv1
+import bascenev1
+
+from baclassic._chest import (
+ CHEST_APPEARANCE_DISPLAY_INFOS,
+ CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
+)
if TYPE_CHECKING:
from typing import Any
-def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
+def get_tournament_prize_strings(
+ entry: dict[str, Any], include_tickets: bool
+) -> list[str]:
"""Given a tournament entry, return strings for its prize levels."""
# pylint: disable=too-many-locals
from bascenev1 import get_trophy_string
@@ -27,7 +37,7 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
trophy_type_2 = entry.get('prizeTrophy2')
trophy_type_3 = entry.get('prizeTrophy3')
out_vals = []
- for rng, prize, trophy_type in (
+ for rng, ticket_prize, trophy_type in (
(range1, prize1, trophy_type_1),
(range2, prize2, trophy_type_2),
(range3, prize3, trophy_type_3),
@@ -45,14 +55,100 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
if trophy_type is not None:
pvval += get_trophy_string(trophy_type)
- # If we've got trophies but not for this entry, throw some space
- # in to compensate so the ticket counts line up.
- if prize is not None:
+ if ticket_prize is not None and include_tickets:
pvval = (
babase.charstr(babase.SpecialChar.TICKET_BACKING)
- + str(prize)
+ + str(ticket_prize)
+ pvval
)
out_vals.append(prval)
out_vals.append(pvval)
return out_vals
+
+
+def set_tournament_prize_chest_image(
+ entry: dict[str, Any], index: int, image: bauiv1.Widget
+) -> None:
+ """Set image attrs representing a tourney prize chest."""
+ ranges = [
+ entry.get('prizeRange1'),
+ entry.get('prizeRange2'),
+ entry.get('prizeRange3'),
+ ]
+ chests = [
+ entry.get('prizeChest1'),
+ entry.get('prizeChest2'),
+ entry.get('prizeChest3'),
+ ]
+
+ assert 0 <= index < 3
+
+ # If tourney doesn't include this prize, just hide the image.
+ if ranges[index] is None:
+ bauiv1.imagewidget(edit=image, opacity=0.0)
+ return
+
+ try:
+ appearance = ClassicChestAppearance(chests[index])
+ except ValueError:
+ appearance = ClassicChestAppearance.DEFAULT
+ chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
+ appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
+ )
+ bauiv1.imagewidget(
+ edit=image,
+ opacity=1.0,
+ color=chestdisplayinfo.color,
+ texture=bauiv1.gettexture(chestdisplayinfo.texclosed),
+ tint_texture=bauiv1.gettexture(chestdisplayinfo.texclosedtint),
+ tint_color=chestdisplayinfo.tint,
+ tint2_color=chestdisplayinfo.tint2,
+ )
+
+
+def create_in_game_tournament_prize_image(
+ entry: dict[str, Any], index: int, position: tuple[float, float]
+) -> None:
+ """Create a display for the prize chest (if any) in-game."""
+ from bascenev1lib.actor.image import Image
+
+ ranges = [
+ entry.get('prizeRange1'),
+ entry.get('prizeRange2'),
+ entry.get('prizeRange3'),
+ ]
+ chests = [
+ entry.get('prizeChest1'),
+ entry.get('prizeChest2'),
+ entry.get('prizeChest3'),
+ ]
+
+ # If tourney doesn't include this prize, no-op.
+ if ranges[index] is None:
+ return
+
+ try:
+ appearance = ClassicChestAppearance(chests[index])
+ except ValueError:
+ appearance = ClassicChestAppearance.DEFAULT
+ chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
+ appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
+ )
+ Image(
+ # Provide magical extended dict version of texture that Image
+ # actor supports.
+ texture={
+ 'texture': bascenev1.gettexture(chestdisplayinfo.texclosed),
+ 'tint_texture': bascenev1.gettexture(
+ chestdisplayinfo.texclosedtint
+ ),
+ 'tint_color': chestdisplayinfo.tint,
+ 'tint2_color': chestdisplayinfo.tint2,
+ 'mask_texture': None,
+ },
+ color=chestdisplayinfo.color + (1.0,),
+ position=position,
+ scale=(48.0, 48.0),
+ transition=Image.Transition.FADE_IN,
+ transition_delay=2.0,
+ ).autoretain()
diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py
index 370155c1..cf4a7be8 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -53,7 +53,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 22155
+TARGET_BALLISTICA_BUILD = 22178
TARGET_BALLISTICA_VERSION = '1.7.37'
diff --git a/src/assets/ba_data/python/baplus/_appsubsystem.py b/src/assets/ba_data/python/baplus/_appsubsystem.py
index fd5b5281..809f387b 100644
--- a/src/assets/ba_data/python/baplus/_appsubsystem.py
+++ b/src/assets/ba_data/python/baplus/_appsubsystem.py
@@ -12,6 +12,7 @@ import _baplus
if TYPE_CHECKING:
from typing import Callable, Any
+ import bacommon.bs
from babase import AccountV2Subsystem
from baplus._cloud import CloudSubsystem
diff --git a/src/assets/ba_data/python/baplus/_cloud.py b/src/assets/ba_data/python/baplus/_cloud.py
index fdfcdf56..8be76293 100644
--- a/src/assets/ba_data/python/baplus/_cloud.py
+++ b/src/assets/ba_data/python/baplus/_cloud.py
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
from efro.message import Message, Response
import bacommon.cloud
+ import bacommon.bs
# TODO: Should make it possible to define a protocol in bacommon.cloud and
@@ -120,45 +121,45 @@ class CloudSubsystem(babase.AppSubsystem):
@overload
def send_message_cb(
self,
- msg: bacommon.cloud.BSPrivatePartyMessage,
+ msg: bacommon.bs.PrivatePartyMessage,
on_response: Callable[
- [bacommon.cloud.BSPrivatePartyResponse | Exception], None
+ [bacommon.bs.PrivatePartyResponse | Exception], None
],
) -> None: ...
@overload
def send_message_cb(
self,
- msg: bacommon.cloud.BSInboxRequestMessage,
+ msg: bacommon.bs.InboxRequestMessage,
on_response: Callable[
- [bacommon.cloud.BSInboxRequestResponse | Exception], None
+ [bacommon.bs.InboxRequestResponse | Exception], None
],
) -> None: ...
@overload
def send_message_cb(
self,
- msg: bacommon.cloud.BSInboxEntryProcessMessage,
+ msg: bacommon.bs.ClientUIActionMessage,
on_response: Callable[
- [bacommon.cloud.BSInboxEntryProcessResponse | Exception], None
+ [bacommon.bs.ClientUIActionResponse | Exception], None
],
) -> None: ...
@overload
def send_message_cb(
self,
- msg: bacommon.cloud.BSChestInfoMessage,
+ msg: bacommon.bs.ChestInfoMessage,
on_response: Callable[
- [bacommon.cloud.BSChestInfoResponse | Exception], None
+ [bacommon.bs.ChestInfoResponse | Exception], None
],
) -> None: ...
@overload
def send_message_cb(
self,
- msg: bacommon.cloud.BSChestActionMessage,
+ msg: bacommon.bs.ChestActionMessage,
on_response: Callable[
- [bacommon.cloud.BSChestActionResponse | Exception], None
+ [bacommon.bs.ChestActionResponse | Exception], None
],
) -> None: ...
@@ -229,7 +230,7 @@ class CloudSubsystem(babase.AppSubsystem):
def subscribe_classic_account_data(
self,
- updatecall: Callable[[bacommon.cloud.BSClassicAccountLiveData], None],
+ updatecall: Callable[[bacommon.bs.ClassicAccountLiveData], None],
) -> babase.CloudSubscription:
"""Subscribe to classic account data."""
raise NotImplementedError(
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
index 085e9111..ab1f5d05 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
@@ -357,6 +357,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
h_offs = 7.0
v_offs = -280.0
+ v_offs2 = -236.0
# We wanna prevent controllers users from popping up browsers
# or game-center widgets in cases where they can't easily get back
@@ -384,7 +385,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
bui.buttonwidget(
parent=rootc,
color=(0.45, 0.4, 0.5),
- position=(160, v_offs + 439),
+ position=(240, v_offs2 + 439),
size=(350, 62),
label=(
bui.Lstr(resource='tournamentStandingsText')
@@ -406,7 +407,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
show_next_button = self._is_more_levels and not (env.demo or env.arcade)
if not show_next_button:
- h_offs += 70
+ h_offs += 60
# Due to virtual-bounds changes, have to squish buttons a bit to
# avoid overlapping with tips at bottom. Could look nicer to
@@ -614,7 +615,6 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
@override
def on_begin(self) -> None:
- # FIXME: Clean this up.
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
@@ -882,7 +882,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
# If we're not doing the world's-best button, just show a title
# instead.
ts_height = 300
- ts_h_offs = 210
+ ts_h_offs = 290
v_offs = 40
txt = Text(
(
@@ -956,7 +956,6 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
if display_scores[i][1] is None:
name_str = '-'
else:
- # noinspection PyUnresolvedReferences
name_str = ', '.join(
[p['name'] for p in display_scores[i][1]['players']]
)
@@ -1025,9 +1024,8 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
ts_h_offs = -480
v_offs = 40
- # Only make this if we don't have the button
- # (never want clients to see it so no need for client-only
- # version, etc).
+ # Only make this if we don't have the button (never want clients
+ # to see it so no need for client-only version, etc).
if self._have_achievements:
if not self._account_has_achievements:
Text(
@@ -1069,7 +1067,6 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
).autoretain()
def _got_friend_score_results(self, results: list[Any] | None) -> None:
- # FIXME: tidy this up
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
@@ -1205,7 +1202,6 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
).autoretain()
def _got_score_results(self, results: dict[str, Any] | None) -> None:
- # FIXME: tidy this up
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
@@ -1222,11 +1218,12 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
# Delay a bit if results come in too fast.
assert self._begin_time is not None
base_delay = max(0, 2.7 - (bs.time() - self._begin_time))
- v_offs = 20
+ # v_offs = 20
+ v_offs = 64
if results is None:
self._score_loading_status = Text(
bs.Lstr(resource='worldScoresUnavailableText'),
- position=(230, 150 + v_offs),
+ position=(280, 130 + v_offs),
color=(1, 1, 1, 0.4),
transition=Text.Transition.FADE_IN,
transition_delay=base_delay + 0.3,
@@ -1271,7 +1268,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
(1.5 + base_delay),
bs.WeakCall(self._show_world_rank, offs_x),
)
- ts_h_offs = 200
+ ts_h_offs = 280
ts_height = 300
# Show world tops.
@@ -1299,7 +1296,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
transition_delay=base_delay + 0.3,
).autoretain()
else:
- v_offs += 20
+ v_offs += 40
h_offs_extra = 0
v_offs_names = 0
@@ -1326,6 +1323,37 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
random.randrange(0, len(times) + 1),
(base_delay + i * 0.05, base_delay + 0.4 + i * 0.05),
)
+
+ # Conundrum: We want to place line numbers to the
+ # left of our score column based on the largest
+ # score width. However scores may use Lstrs and thus
+ # may have different widths in different languages.
+ # We don't want to bake down the Lstrs we display
+ # because then clients can't view scores in their
+ # own language. So as a compromise lets measure
+ # max-width based on baked down Lstrs but then
+ # display regular Lstrs with max-width set based on
+ # that. Hopefully that'll look reasonable for most
+ # languages.
+ max_score_width = 10.0
+ for tval in self._show_info['tops']:
+ score = int(tval[0])
+ name_str = tval[1]
+ if name_str != '-':
+ max_score_width = max(
+ max_score_width,
+ bui.get_string_width(
+ (
+ str(score)
+ if self._score_type == 'points'
+ else bs.timestring(
+ (score * 10) / 1000.0
+ ).evaluate()
+ ),
+ suppress_warning=True,
+ ),
+ )
+
for i, tval in enumerate(self._show_info['tops']):
score = int(tval[0])
name_str = tval[1]
@@ -1347,12 +1375,37 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
tdelay2 = times[i][1]
if name_str != '-':
+ sstr = (
+ str(score)
+ if self._score_type == 'points'
+ else bs.timestring((score * 10) / 1000.0)
+ )
+
+ # Line number.
Text(
- (
- str(score)
- if self._score_type == 'points'
- else bs.timestring((score * 10) / 1000.0)
+ str(i + 1),
+ position=(
+ ts_h_offs
+ + 20
+ + h_offs_extra
+ - max_score_width
+ - 8.0,
+ ts_height / 2
+ + -ts_height * (i + 1) / 10
+ + v_offs
+ - 30.0,
),
+ scale=0.5,
+ h_align=Text.HAlign.RIGHT,
+ v_align=Text.VAlign.CENTER,
+ color=(0.3, 0.3, 0.3),
+ transition=Text.Transition.IN_LEFT,
+ transition_delay=tdelay1,
+ ).autoretain()
+
+ # Score.
+ Text(
+ sstr,
position=(
ts_h_offs + 20 + h_offs_extra,
ts_height / 2
@@ -1360,6 +1413,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
+ v_offs
- 30.0,
),
+ maxwidth=max_score_width,
h_align=Text.HAlign.RIGHT,
v_align=Text.VAlign.CENTER,
color=color0,
@@ -1367,6 +1421,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay1,
).autoretain()
+ # Player name.
Text(
bs.Lstr(value=name_str),
position=(
@@ -1470,16 +1525,12 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
]
# pylint: disable=useless-suppression
# pylint: disable=unbalanced-tuple-unpacking
- (
- pr1,
- pv1,
- pr2,
- pv2,
- pr3,
- pv3,
- ) = bs.app.classic.get_tournament_prize_strings(
- tourney_info
+ (pr1, pv1, pr2, pv2, pr3, pv3) = (
+ bs.app.classic.get_tournament_prize_strings(
+ tourney_info, include_tickets=False
+ )
)
+
# pylint: enable=unbalanced-tuple-unpacking
# pylint: enable=useless-suppression
@@ -1495,10 +1546,14 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
transition_delay=2.0,
).autoretain()
vval = -107 + 70
- for rng, val in ((pr1, pv1), (pr2, pv2), (pr3, pv3)):
+ for i, rng, val in (
+ (0, pr1, pv1),
+ (1, pr2, pv2),
+ (2, pr3, pv3),
+ ):
Text(
rng,
- position=(-410 + 10, vval),
+ position=(-430 + 10, vval),
color=(1, 1, 1, 0.7),
h_align=Text.HAlign.RIGHT,
v_align=Text.VAlign.CENTER,
@@ -1509,7 +1564,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
).autoretain()
Text(
val,
- position=(-390 + 10, vval),
+ position=(-410 + 10, vval),
color=(0.7, 0.7, 0.7, 1.0),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
@@ -1518,6 +1573,9 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
maxwidth=300,
transition_delay=2.0,
).autoretain()
+ bs.app.classic.create_in_game_tournament_prize_image(
+ tourney_info, i, (-410 + 70, vval)
+ )
vval -= 35
except Exception:
logging.exception('Error showing prize ranges.')
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/image.py b/src/assets/ba_data/python/bascenev1lib/actor/image.py
index 2a71c878..4e38f6b1 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/image.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/image.py
@@ -56,15 +56,21 @@ class Image(bs.Actor):
# pylint: disable=too-many-locals
super().__init__()
- # If they provided a dict as texture, assume its an icon.
- # otherwise its just a texture value itself.
+ # If they provided a dict as texture, use it to wire up extended
+ # stuff like tints and masks.
mask_texture: bs.Texture | None
if isinstance(texture, dict):
tint_color = texture['tint_color']
tint2_color = texture['tint2_color']
tint_texture = texture['tint_texture']
+
+ # Assume we're dealing with a character icon but allow
+ # overriding.
+ mask_tex_name = texture.get('mask_texture', 'characterIconMask')
+ mask_texture = (
+ None if mask_tex_name is None else bs.gettexture(mask_tex_name)
+ )
texture = texture['texture']
- mask_texture = bs.gettexture('characterIconMask')
else:
tint_color = (1, 1, 1)
tint2_color = None
diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py
index 2195ad5c..1d51fdd6 100644
--- a/src/assets/ba_data/python/bauiv1/__init__.py
+++ b/src/assets/ba_data/python/bauiv1/__init__.py
@@ -114,9 +114,12 @@ from _bauiv1 import (
hscrollwidget,
imagewidget,
Mesh,
+ root_ui_pause_updates,
+ root_ui_resume_updates,
rowwidget,
scrollwidget,
set_party_window_open,
+ spinnerwidget,
Sound,
Texture,
textwidget,
@@ -218,6 +221,8 @@ __all__ = [
'quit',
'QuitType',
'request_permission',
+ 'root_ui_pause_updates',
+ 'root_ui_resume_updates',
'rowwidget',
'safecolor',
'screenmessage',
@@ -228,6 +233,7 @@ __all__ = [
'set_ui_input_device',
'Sound',
'SpecialChar',
+ 'spinnerwidget',
'supports_max_fps',
'supports_vsync',
'supports_unicode_display',
diff --git a/src/assets/ba_data/python/bauiv1lib/account/viewer.py b/src/assets/ba_data/python/bauiv1lib/account/viewer.py
index 9f0d606a..e38211f6 100644
--- a/src/assets/ba_data/python/bauiv1lib/account/viewer.py
+++ b/src/assets/ba_data/python/bauiv1lib/account/viewer.py
@@ -100,17 +100,20 @@ class AccountViewerWindow(PopupWindow):
)
bui.widget(edit=self._scrollwidget, autoselect=True)
+ # Note to self: Make sure to always update loading text and
+ # spinner visibility together.
self._loading_text = bui.textwidget(
parent=self._scrollwidget,
scale=0.5,
- text=bui.Lstr(
- value='${A}...',
- subs=[('${A}', bui.Lstr(resource='loadingText'))],
- ),
+ text='',
size=(self._width - 60, 100),
h_align='center',
v_align='center',
)
+ self._loading_spinner = bui.spinnerwidget(
+ parent=self.root_widget,
+ position=(self._width * 0.5, self._height * 0.5),
+ )
# In cases where the user most likely has a browser/email, lets
# offer a 'report this user' button.
@@ -227,9 +230,11 @@ class AccountViewerWindow(PopupWindow):
edit=self._loading_text,
text=bui.Lstr(resource='internal.unavailableNoConnectionText'),
)
+ bui.spinnerwidget(edit=self._loading_spinner, visible=False)
else:
try:
self._loading_text.delete()
+ self._loading_spinner.delete()
trophystr = ''
try:
trophystr = data['trophies']
diff --git a/src/assets/ba_data/python/bauiv1lib/chest.py b/src/assets/ba_data/python/bauiv1lib/chest.py
index f46449f8..5172bee6 100644
--- a/src/assets/ba_data/python/bauiv1lib/chest.py
+++ b/src/assets/ba_data/python/bauiv1lib/chest.py
@@ -1,23 +1,27 @@
# Released under the MIT License. See LICENSE for details.
#
+# pylint: disable=too-many-lines
"""Provides chest related ui."""
from __future__ import annotations
+import math
+import random
from typing import override, TYPE_CHECKING
-import bacommon.cloud
+import bacommon.bs
import bauiv1 as bui
if TYPE_CHECKING:
- pass
+ import datetime
+
+ import baclassic
+
+_g_open_voices: list[tuple[float, str, float]] = []
class ChestWindow(bui.MainWindow):
- """Allows operations on a chest."""
-
- # def __del__(self) -> None:
- # print('~ChestWindow()')
+ """Allows viewing and performing operations on a chest."""
def __init__(
self,
@@ -31,17 +35,25 @@ class ChestWindow(bui.MainWindow):
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
- self._width = 1050 if uiscale is bui.UIScale.SMALL else 850
- self._height = (
- 500
- if uiscale is bui.UIScale.SMALL
- else 500 if uiscale is bui.UIScale.MEDIUM else 500
- )
+ self._width = 1050 if uiscale is bui.UIScale.SMALL else 650
+ self._height = 550 if uiscale is bui.UIScale.SMALL else 450
self._xoffs = 70 if uiscale is bui.UIScale.SMALL else 0
- self._yoffs = -42 if uiscale is bui.UIScale.SMALL else -25
+ self._yoffs = -50 if uiscale is bui.UIScale.SMALL else -35
self._action_in_flight = False
self._open_now_button: bui.Widget | None = None
+ self._open_now_spinner: bui.Widget | None = None
+ self._open_now_texts: list[bui.Widget] = []
+ self._open_now_images: list[bui.Widget] = []
self._watch_ad_button: bui.Widget | None = None
+ self._time_string_timer: bui.AppTimer | None = None
+ self._time_string_text: bui.Widget | None = None
+ self._prizesets: list[bacommon.bs.ChestInfoResponse.Chest.PrizeSet] = []
+ self._prizeindex = -1
+ self._prizesettxts: dict[int, list[bui.Widget]] = {}
+ self._prizesetimgs: dict[int, list[bui.Widget]] = {}
+ self._chestdisplayinfo: baclassic.ChestAppearanceDisplayInfo | None = (
+ None
+ )
# The set of widgets we keep when doing a clear.
self._core_widgets: list[bui.Widget] = []
@@ -51,7 +63,7 @@ class ChestWindow(bui.MainWindow):
size=(self._width, self._height),
toolbar_visibility='menu_full',
scale=(
- 1.55
+ 1.45
if uiscale is bui.UIScale.SMALL
else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.9
),
@@ -65,9 +77,15 @@ class ChestWindow(bui.MainWindow):
origin_widget=origin_widget,
)
+ # Tell the root-ui to stop updating toolbar values immediately;
+ # this allows it to run animations based on the results of our
+ # chest opening.
+ bui.root_ui_pause_updates()
+ self._root_ui_updates_paused = True
+
self._title_text = bui.textwidget(
parent=self._root_widget,
- position=(0, self._height - 45 + self._yoffs),
+ position=(0, self._height - 50 + self._yoffs),
size=(self._width, 25),
text=f'Chest Slot {self._index + 1}',
color=bui.app.ui_v1.title_color,
@@ -96,11 +114,18 @@ class ChestWindow(bui.MainWindow):
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
self._core_widgets.append(btn)
+ # Note: Don't need to explicitly clean this up. Just not adding
+ # it to core_widgets so it will go away on next reset.
+ self._loadingspinner = bui.spinnerwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5, self._height * 0.5),
+ )
+
self._infotext = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 200 + self._yoffs),
size=(0, 0),
- text=bui.Lstr(resource='loadingText'),
+ text='',
maxwidth=700,
scale=0.7,
color=(0.6, 0.5, 0.6),
@@ -131,192 +156,16 @@ class ChestWindow(bui.MainWindow):
self._action_in_flight = True
with plus.accounts.primary:
plus.cloud.send_message_cb(
- bacommon.cloud.BSChestInfoMessage(chest_id=str(self._index)),
+ bacommon.bs.ChestInfoMessage(chest_id=str(self._index)),
on_response=bui.WeakCall(self._on_chest_info_response),
)
- def _on_chest_info_response(
- self, response: bacommon.cloud.BSChestInfoResponse | Exception
- ) -> None:
- assert self._action_in_flight # Should be us.
- self._action_in_flight = False
+ def __del__(self) -> None:
+ # print('~ChestWindow()')
- if isinstance(response, Exception):
- self._error(
- bui.Lstr(resource='internal.unavailableNoConnectionText')
- )
- return
-
- if response.chest is None:
- self._show_about_chest_slots()
- return
-
- self.show_chest_actions(response.chest)
-
- def _on_chest_action_response(
- self, response: bacommon.cloud.BSChestActionResponse | Exception
- ) -> None:
- assert self._action_in_flight # Should be us.
- self._action_in_flight = False
-
- # Communication/local error:
- if isinstance(response, Exception):
- self._error(
- bui.Lstr(resource='internal.unavailableNoConnectionText')
- )
- return
-
- # Server-side error:
- if response.error is not None:
- self._error(bui.Lstr(translate=('serverResponses', response.error)))
- return
-
- # If there's contents listed in the response, show them.
- if response.contents is not None:
- print('WOULD SHOW CONTENTS:', response.contents)
- else:
- # Otherwise we're done here; just close out our UI.
- self.main_window_back()
-
- def show_chest_actions(
- self, chest: bacommon.cloud.BSChestInfoResponse.Chest
- ) -> None:
- """Show state for our chest."""
- # pylint: disable=cyclic-import
- from baclassic import ClassicAppMode
-
- # We expect to be run under classic.
- mode = bui.app.mode
- if not isinstance(mode, ClassicAppMode):
- self._error('Classic app mode not active.')
- return
-
- now = bui.utc_now_cloud()
- secs_till_open = max(0.0, (chest.unlock_time - now).total_seconds())
- tstr = bui.timestring(secs_till_open, centi=False)
-
- bui.textwidget(
- parent=self._root_widget,
- position=(self._width * 0.5, self._height - 120 + self._yoffs),
- size=(0, 0),
- text=tstr,
- maxwidth=700,
- scale=0.7,
- color=(0.6, 0.5, 0.6),
- h_align='center',
- v_align='center',
- )
- self._open_now_button = bui.buttonwidget(
- parent=self._root_widget,
- position=(
- self._width * 0.5 - 200,
- self._height - 250 + self._yoffs,
- ),
- size=(150, 100),
- label=f'OPEN NOW FOR {chest.unlock_tokens} TOKENS',
- button_type='square',
- autoselect=True,
- on_activate_call=bui.WeakCall(
- self._open_now_press, chest.unlock_tokens
- ),
- )
-
- self._watch_ad_button = bui.buttonwidget(
- parent=self._root_widget,
- position=(
- self._width * 0.5 + 50,
- self._height - 250 + self._yoffs,
- ),
- size=(150, 100),
- label='WATCH AN AD TO REDUCE WAIT',
- button_type='square',
- autoselect=True,
- on_activate_call=bui.WeakCall(self._watch_ad_press),
- )
- bui.textwidget(edit=self._infotext, text='')
-
- def _open_now_press(self, token_payment: int) -> None:
-
- # Allow only one in-flight action at once.
- if self._action_in_flight:
- bui.screenmessage(
- bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
- )
- bui.getsound('error').play()
- return
-
- plus = bui.app.plus
- assert plus is not None
-
- if plus.accounts.primary is None:
- self._error(bui.Lstr(resource='notSignedInText'))
- return
-
- self._action_in_flight = True
- with plus.accounts.primary:
- plus.cloud.send_message_cb(
- bacommon.cloud.BSChestActionMessage(
- chest_id=str(self._index),
- action=bacommon.cloud.BSChestActionMessage.Action.UNLOCK,
- token_payment=token_payment,
- ),
- on_response=bui.WeakCall(self._on_chest_action_response),
- )
-
- # Convey that something is in progress.
- if self._open_now_button:
- bui.buttonwidget(edit=self._open_now_button, label='...')
-
- def _watch_ad_press(self) -> None:
-
- # Allow only one in-flight action at once.
- if self._action_in_flight:
- bui.screenmessage(
- bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
- )
- bui.getsound('error').play()
- return
-
- plus = bui.app.plus
- assert plus is not None
-
- if plus.accounts.primary is None:
- self._error(bui.Lstr(resource='notSignedInText'))
- return
-
- self._action_in_flight = True
- with plus.accounts.primary:
- plus.cloud.send_message_cb(
- bacommon.cloud.BSChestActionMessage(
- chest_id=str(self._index),
- action=bacommon.cloud.BSChestActionMessage.Action.AD,
- token_payment=0,
- ),
- on_response=bui.WeakCall(self._on_chest_action_response),
- )
-
- # Convey that something is in progress.
- if self._watch_ad_button:
- bui.buttonwidget(edit=self._watch_ad_button, label='...')
-
- def _reset(self) -> None:
- """Clear all non-permanent widgets."""
- for widget in self._root_widget.get_children():
- if widget not in self._core_widgets:
- widget.delete()
-
- def _error(self, msg: str | bui.Lstr) -> None:
- """Put ourself in an error state with a visible error message."""
- self._reset()
- bui.textwidget(edit=self._infotext, text=msg, color=(1, 0, 0))
-
- def _show_about_chest_slots(self) -> None:
- self._reset()
- msg = (
- 'This empty slot can hold a treasure chest.\n'
- 'Treasure chests are earned through gameplay.'
- )
- bui.textwidget(edit=self._infotext, text=msg, color=(1, 1, 1))
+ # Make sure UI updates are resumed if we haven't done so.
+ if self._root_ui_updates_paused:
+ bui.root_ui_resume_updates()
@override
def get_main_window_state(self) -> bui.MainWindowState:
@@ -333,12 +182,909 @@ class ChestWindow(bui.MainWindow):
)
)
+ def _update_time_display(self, unlock_time: datetime.datetime) -> None:
+ # Once text disappears, kill our timer.
+ if not self._time_string_text:
+ self._time_string_timer = None
+ return
+ now = bui.utc_now_cloud()
+ secs_till_open = max(0.0, (unlock_time - now).total_seconds())
+ tstr = (
+ bui.timestring(secs_till_open, centi=False)
+ if secs_till_open > 0
+ else ''
+ )
+ bui.textwidget(edit=self._time_string_text, text=tstr)
-# Slight hack: we define different classes for our different chest slots
-# so that the default UI behavior is to replace each other when
-# different ones are pressed. If they are all the same class then the
-# default behavior for such presses is to toggle the existing one back
-# off.
+ def _on_chest_info_response(
+ self, response: bacommon.bs.ChestInfoResponse | Exception
+ ) -> None:
+ assert self._action_in_flight # Should be us.
+ self._action_in_flight = False
+
+ if isinstance(response, Exception):
+ self._error(
+ bui.Lstr(resource='internal.unavailableNoConnectionText')
+ )
+ return
+
+ if response.chest is None:
+ self._show_about_chest_slots()
+ return
+
+ assert response.user_tokens is not None
+ self._show_chest_actions(response.user_tokens, response.chest)
+
+ def _on_chest_action_response(
+ self, response: bacommon.bs.ChestActionResponse | Exception
+ ) -> None:
+ assert self._action_in_flight # Should be us.
+ self._action_in_flight = False
+
+ # Communication/local error:
+ if isinstance(response, Exception):
+ self._error(
+ bui.Lstr(resource='internal.unavailableNoConnectionText')
+ )
+ return
+
+ # Server-side error:
+ if response.error is not None:
+ self._error(bui.Lstr(translate=('serverResponses', response.error)))
+ return
+
+ # Show any bundled success message.
+ if response.success_msg is not None:
+ bui.screenmessage(
+ bui.Lstr(translate=('serverResponses', response.success_msg)),
+ color=(0, 1.0, 0),
+ )
+ bui.getsound('cashRegister').play()
+
+ # Show any bundled warning.
+ if response.warning is not None:
+ bui.screenmessage(
+ bui.Lstr(translate=('serverResponses', response.warning)),
+ color=(1, 0.5, 0),
+ )
+ bui.getsound('error').play()
+
+ # If we just paid for something, make a sound accordingly.
+ if bool(False): # Hmm maybe this feels odd.
+ if response.tokens_charged > 0:
+ bui.getsound('cashRegister').play()
+
+ # If there's contents listed in the response, show them.
+ if response.contents is not None:
+ self._show_chest_contents(response)
+ else:
+ # Otherwise we're done here; just close out our UI.
+ self.main_window_back()
+
+ def _show_chest_actions(
+ self, user_tokens: int, chest: bacommon.bs.ChestInfoResponse.Chest
+ ) -> None:
+ """Show state for our chest."""
+ # pylint: disable=too-many-locals
+ # pylint: disable=cyclic-import
+ from baclassic import (
+ ClassicAppMode,
+ CHEST_APPEARANCE_DISPLAY_INFOS,
+ CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
+ )
+
+ plus = bui.app.plus
+ assert plus is not None
+
+ # We expect to be run under classic app mode.
+ mode = bui.app.mode
+ if not isinstance(mode, ClassicAppMode):
+ self._error('Classic app mode not active.')
+ return
+
+ self._reset()
+
+ self._chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
+ chest.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
+ )
+
+ bui.textwidget(
+ edit=self._title_text, text=f'{chest.appearance.name} Chest'
+ )
+
+ imgsize = 145
+ bui.imagewidget(
+ parent=self._root_widget,
+ position=(
+ self._width * 0.5 - imgsize * 0.5,
+ self._height - 223 + self._yoffs,
+ ),
+ color=self._chestdisplayinfo.color,
+ size=(imgsize, imgsize),
+ texture=bui.gettexture(self._chestdisplayinfo.texclosed),
+ tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint),
+ tint_color=self._chestdisplayinfo.tint,
+ tint2_color=self._chestdisplayinfo.tint2,
+ )
+
+ # Store the prize-sets so we can display odds/etc. Sort them
+ # with largest weights first.
+ self._prizesets = sorted(
+ chest.prizesets, key=lambda s: s.weight, reverse=True
+ )
+
+ if chest.unlock_tokens > 0:
+ lsize = 30
+ bui.imagewidget(
+ parent=self._root_widget,
+ position=(
+ self._width * 0.5 - imgsize * 0.4 - lsize * 0.5,
+ self._height - 223 + 27.0 + self._yoffs,
+ ),
+ size=(lsize, lsize),
+ texture=bui.gettexture('lock'),
+ )
+
+ # Time string.
+ self._time_string_text = bui.textwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5, self._height - 85 + self._yoffs),
+ size=(0, 0),
+ text='',
+ maxwidth=700,
+ scale=0.6,
+ color=(0.6, 1.0, 0.6),
+ h_align='center',
+ v_align='center',
+ )
+ self._update_time_display(chest.unlock_time)
+ self._time_string_timer = bui.AppTimer(
+ 1.0,
+ repeat=True,
+ call=bui.WeakCall(self._update_time_display, chest.unlock_time),
+ )
+
+ # Allow watching an ad IF the server tells us we can AND we have
+ # an ad ready to show.
+ show_ad_button = (
+ chest.unlock_tokens > 0
+ and chest.ad_allow
+ and plus.have_incentivized_ad()
+ )
+
+ bwidth = 130
+ bheight = 90
+ bposy = -330 if chest.unlock_tokens == 0 else -340
+ hspace = 20
+ boffsx = (hspace * -0.5 - bwidth * 0.5) if show_ad_button else 0.0
+
+ self._open_now_button = bui.buttonwidget(
+ parent=self._root_widget,
+ position=(
+ self._width * 0.5 - bwidth * 0.5 + boffsx,
+ self._height + bposy + self._yoffs,
+ ),
+ size=(bwidth, bheight),
+ label='',
+ button_type='square',
+ autoselect=True,
+ on_activate_call=bui.WeakCall(
+ self._open_press, user_tokens, chest.unlock_tokens
+ ),
+ enable_sound=False,
+ )
+ self._open_now_images = []
+ self._open_now_texts = []
+
+ iconsize = 50
+ if chest.unlock_tokens == 0:
+ self._open_now_texts.append(
+ bui.textwidget(
+ parent=self._root_widget,
+ text='Open',
+ position=(
+ self._width * 0.5 + boffsx,
+ self._height + bposy + self._yoffs + bheight * 0.5,
+ ),
+ color=(0, 1, 0),
+ draw_controller=self._open_now_button,
+ scale=0.7,
+ maxwidth=bwidth * 0.8,
+ size=(0, 0),
+ h_align='center',
+ v_align='center',
+ )
+ )
+ else:
+ self._open_now_texts.append(
+ bui.textwidget(
+ parent=self._root_widget,
+ text='Open Now',
+ position=(
+ self._width * 0.5 + boffsx,
+ self._height + bposy + self._yoffs + bheight * 1.15,
+ ),
+ maxwidth=bwidth * 0.8,
+ scale=0.7,
+ color=(0.7, 1, 0.7),
+ size=(0, 0),
+ h_align='center',
+ v_align='center',
+ )
+ )
+ self._open_now_images.append(
+ bui.imagewidget(
+ parent=self._root_widget,
+ size=(iconsize, iconsize),
+ position=(
+ self._width * 0.5 - iconsize * 0.5 + boffsx,
+ self._height + bposy + self._yoffs + bheight * 0.35,
+ ),
+ draw_controller=self._open_now_button,
+ texture=bui.gettexture('coin'),
+ )
+ )
+ self._open_now_texts.append(
+ bui.textwidget(
+ parent=self._root_widget,
+ text=bui.Lstr(
+ resource='tokens.numTokensText',
+ subs=[('${COUNT}', str(chest.unlock_tokens))],
+ ),
+ position=(
+ self._width * 0.5 + boffsx,
+ self._height + bposy + self._yoffs + bheight * 0.25,
+ ),
+ scale=0.65,
+ color=(0, 1, 0),
+ draw_controller=self._open_now_button,
+ maxwidth=bwidth * 0.8,
+ size=(0, 0),
+ h_align='center',
+ v_align='center',
+ )
+ )
+ self._open_now_spinner = bui.spinnerwidget(
+ parent=self._root_widget,
+ position=(
+ self._width * 0.5 + boffsx,
+ self._height + bposy + self._yoffs + 0.5 * bheight,
+ ),
+ visible=False,
+ )
+
+ if show_ad_button:
+ bui.textwidget(
+ parent=self._root_widget,
+ text='Reduce Wait',
+ position=(
+ self._width * 0.5 + hspace * 0.5 + bwidth * 0.5,
+ self._height + bposy + self._yoffs + bheight * 1.15,
+ ),
+ maxwidth=bwidth * 0.8,
+ scale=0.7,
+ color=(0.7, 1, 0.7),
+ size=(0, 0),
+ h_align='center',
+ v_align='center',
+ )
+ self._watch_ad_button = bui.buttonwidget(
+ parent=self._root_widget,
+ position=(
+ self._width * 0.5 + hspace * 0.5,
+ self._height + bposy + self._yoffs,
+ ),
+ size=(bwidth, bheight),
+ label='',
+ button_type='square',
+ autoselect=True,
+ on_activate_call=bui.WeakCall(self._watch_ad_press),
+ enable_sound=False,
+ )
+ bui.imagewidget(
+ parent=self._root_widget,
+ size=(iconsize, iconsize),
+ position=(
+ self._width * 0.5
+ + hspace * 0.5
+ + bwidth * 0.5
+ - iconsize * 0.5,
+ self._height + bposy + self._yoffs + bheight * 0.35,
+ ),
+ draw_controller=self._watch_ad_button,
+ color=(1.5, 1.0, 2.0),
+ texture=bui.gettexture('tv'),
+ )
+ # Note to self: AdMob requires rewarded ad usage
+ # specifically says 'Ad' in it.
+ bui.textwidget(
+ parent=self._root_widget,
+ text=bui.Lstr(resource='watchAnAdText'),
+ position=(
+ self._width * 0.5 + hspace * 0.5 + bwidth * 0.5,
+ self._height + bposy + self._yoffs + bheight * 0.25,
+ ),
+ scale=0.65,
+ color=(0, 1, 0),
+ draw_controller=self._watch_ad_button,
+ maxwidth=bwidth * 0.8,
+ size=(0, 0),
+ h_align='center',
+ v_align='center',
+ )
+
+ self._show_odds(initial_highlighted_row=-1)
+ # bui.textwidget(edit=self._infotext, text='')
+
+ def _highlight_odds_row(self, row: int, extra: bool = False) -> None:
+
+ for rindex, imgs in self._prizesetimgs.items():
+ opacity = (
+ (0.9 if extra else 0.75)
+ if rindex == row
+ else (0.4 if extra else 0.5)
+ )
+ for img in imgs:
+ if img:
+ bui.imagewidget(edit=img, opacity=opacity)
+
+ for rindex, txts in self._prizesettxts.items():
+ opacity = (
+ (0.9 if extra else 0.75)
+ if rindex == row
+ else (0.4 if extra else 0.5)
+ )
+ for txt in txts:
+ if txt:
+ bui.textwidget(edit=txt, color=(0.7, 0.65, 1, opacity))
+
+ def _show_odds(
+ self,
+ *,
+ initial_highlighted_row: int,
+ initial_highlighted_extra: bool = False,
+ ) -> None:
+ # pylint: disable=too-many-locals
+ xoffs = 110
+
+ totalweight = max(0.001, sum(t.weight for t in self._prizesets))
+
+ rowheight = 25
+ totalheight = (len(self._prizesets) + 1) * rowheight
+ x = self._width * 0.5 + xoffs
+ y = self._height + self._yoffs - 150.0 + totalheight * 0.5
+
+ # Title.
+ bui.textwidget(
+ parent=self._root_widget,
+ text='Prize Odds',
+ color=(0.7, 0.65, 1, 0.5),
+ flatness=1.0,
+ shadow=1.0,
+ position=(x, y),
+ scale=0.55,
+ size=(0, 0),
+ h_align='left',
+ v_align='center',
+ )
+ y -= 5.0
+
+ prizesettxts: list[bui.Widget]
+ prizesetimgs: list[bui.Widget]
+
+ def _mkicon(img: str) -> None:
+ iconsize = 20.0
+ nonlocal x
+ nonlocal prizesetimgs
+ prizesetimgs.append(
+ bui.imagewidget(
+ parent=self._root_widget,
+ size=(iconsize, iconsize),
+ position=(x, y - iconsize * 0.5),
+ texture=bui.gettexture(img),
+ opacity=0.4,
+ )
+ )
+ x += iconsize
+
+ def _mktxt(txt: str, advance: bool = True) -> None:
+ tscale = 0.45
+ nonlocal x
+ nonlocal prizesettxts
+ prizesettxts.append(
+ bui.textwidget(
+ parent=self._root_widget,
+ text=txt,
+ flatness=1.0,
+ shadow=1.0,
+ position=(x, y),
+ scale=tscale,
+ size=(0, 0),
+ h_align='left',
+ v_align='center',
+ )
+ )
+ if advance:
+ x += (bui.get_string_width(txt, suppress_warning=True)) * tscale
+
+ self._prizesettxts = {}
+ self._prizesetimgs = {}
+
+ for i, p in enumerate(self._prizesets):
+ prizesettxts = self._prizesettxts.setdefault(i, [])
+ prizesetimgs = self._prizesetimgs.setdefault(i, [])
+ x = self._width * 0.5 + xoffs
+ y -= rowheight
+ percent = 100.0 * p.weight / totalweight
+
+ # Show decimals only if we get very small percentages (looks
+ # better than rounding as '0%').
+ percenttxt = (
+ f'{percent:.2f}'
+ if percent < 0.1
+ else (
+ f'{percent:.1f}' if percent < 1.0 else f'{round(percent)}%:'
+ )
+ )
+
+ # We advance manually here to keep values lined up
+ # (otherwise single digit percent rows don't line up with
+ # double digit ones).
+ _mktxt(percenttxt, advance=False)
+ x += 35.0
+
+ for item in p.contents:
+ x += 5.0
+ if isinstance(item.item, bacommon.bs.TicketsDisplayItem):
+ _mktxt(str(item.item.count))
+ _mkicon('tickets')
+ elif isinstance(item.item, bacommon.bs.TokensDisplayItem):
+ _mktxt(str(item.item.count))
+ _mkicon('coin')
+ else:
+ # For other cases just fall back on text desc.
+ #
+ # Translate the wrapper description and apply any subs.
+ descfin = bui.Lstr(
+ translate=('serverResponses', item.description)
+ ).evaluate()
+ subs = (
+ []
+ if item.description_subs is None
+ else item.description_subs
+ )
+ assert len(subs) % 2 == 0 # Should always be even.
+ for j in range(0, len(subs) - 1, 2):
+ descfin = descfin.replace(subs[j], subs[j + 1])
+ _mktxt(descfin)
+ self._highlight_odds_row(
+ initial_highlighted_row, extra=initial_highlighted_extra
+ )
+
+ def _open_press(self, user_tokens: int, token_payment: int) -> None:
+ from bauiv1lib.gettokens import show_get_tokens_prompt
+
+ bui.getsound('click01').play()
+
+ # Allow only one in-flight action at once.
+ if self._action_in_flight:
+ bui.screenmessage(
+ bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
+ )
+ bui.getsound('error').play()
+ return
+
+ plus = bui.app.plus
+ assert plus is not None
+
+ if plus.accounts.primary is None:
+ self._error(bui.Lstr(resource='notSignedInText'))
+ return
+
+ # Offer to purchase tokens if they don't have enough.
+ if user_tokens < token_payment:
+ # Hack: We disable normal swish for the open button and it
+ # seems weird without a swish here, so explicitly do one.
+ bui.getsound('swish').play()
+ show_get_tokens_prompt()
+ return
+
+ self._action_in_flight = True
+ with plus.accounts.primary:
+ plus.cloud.send_message_cb(
+ bacommon.bs.ChestActionMessage(
+ chest_id=str(self._index),
+ action=bacommon.bs.ChestActionMessage.Action.UNLOCK,
+ token_payment=token_payment,
+ ),
+ on_response=bui.WeakCall(self._on_chest_action_response),
+ )
+
+ # Convey that something is in progress.
+ if self._open_now_button:
+ # bui.buttonwidget(edit=self._open_now_button,
+ # color=(0.4, 1.0, 0.4))
+ bui.spinnerwidget(edit=self._open_now_spinner, visible=True)
+ for twidget in self._open_now_texts:
+ bui.textwidget(edit=twidget, color=(1, 1, 1, 0.2))
+ for iwidget in self._open_now_images:
+ bui.imagewidget(edit=iwidget, opacity=0.2)
+
+ def _watch_ad_press(self) -> None:
+
+ bui.getsound('click01').play()
+
+ # Allow only one in-flight action at once.
+ if self._action_in_flight:
+ bui.screenmessage(
+ bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
+ )
+ bui.getsound('error').play()
+ return
+
+ assert bui.app.classic is not None
+
+ self._action_in_flight = True
+ bui.app.classic.ads.show_ad_2(
+ 'reduce_chest_wait',
+ on_completion_call=bui.WeakCall(self._watch_ad_complete),
+ )
+
+ # Convey that something is in progress.
+ if self._watch_ad_button:
+ bui.buttonwidget(edit=self._watch_ad_button, color=(0.4, 0.4, 0.4))
+
+ def _watch_ad_complete(self, actually_showed: bool) -> None:
+
+ assert self._action_in_flight # Should be ad view.
+ self._action_in_flight = False
+
+ if not actually_showed:
+ return
+
+ # Allow only one in-flight action at once.
+ if self._action_in_flight:
+ bui.screenmessage(
+ bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
+ )
+ bui.getsound('error').play()
+ return
+
+ plus = bui.app.plus
+ assert plus is not None
+
+ if plus.accounts.primary is None:
+ self._error(bui.Lstr(resource='notSignedInText'))
+ return
+
+ self._action_in_flight = True
+ with plus.accounts.primary:
+ plus.cloud.send_message_cb(
+ bacommon.bs.ChestActionMessage(
+ chest_id=str(self._index),
+ action=bacommon.bs.ChestActionMessage.Action.AD,
+ token_payment=0,
+ ),
+ on_response=bui.WeakCall(self._on_chest_action_response),
+ )
+
+ def _reset(self) -> None:
+ """Clear all non-permanent widgets and clear infotext."""
+ for widget in self._root_widget.get_children():
+ if widget not in self._core_widgets:
+ widget.delete()
+ bui.textwidget(edit=self._infotext, text='', color=(1, 1, 1))
+
+ def _error(self, msg: str | bui.Lstr) -> None:
+ """Put ourself in an error state with a visible error message."""
+ self._reset()
+ bui.textwidget(edit=self._infotext, text=msg, color=(1, 0, 0))
+
+ def _show_about_chest_slots(self) -> None:
+ # No-op if our ui is dead.
+ if not self._root_widget:
+ return
+
+ self._reset()
+ msg = (
+ 'This slot can hold a treasure chest.\n\n'
+ 'Earn chests by beating campaing levels,\n'
+ 'placing in tournaments, and completing\n'
+ 'achievements.'
+ )
+ bui.textwidget(edit=self._infotext, text=msg, color=(1, 1, 1))
+
+ def _show_chest_contents(
+ self, response: bacommon.bs.ChestActionResponse
+ ) -> None:
+ # pylint: disable=too-many-locals
+
+ # No-op if our ui is dead.
+ if not self._root_widget:
+ return
+
+ assert response.contents is not None
+
+ tincr = 0.4
+ tendoffs = tincr * 4.0
+ toffs = 0.0
+
+ bui.getsound('revUp').play(volume=2.0)
+
+ # Show nothing but the chest icon and animate it shaking.
+ self._reset()
+ imgsize = 145
+ assert self._chestdisplayinfo is not None
+ img = bui.imagewidget(
+ parent=self._root_widget,
+ color=self._chestdisplayinfo.color,
+ texture=bui.gettexture(self._chestdisplayinfo.texclosed),
+ tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint),
+ tint_color=self._chestdisplayinfo.tint,
+ tint2_color=self._chestdisplayinfo.tint2,
+ )
+
+ def _set_img(x: float, scale: float) -> None:
+ if not img:
+ return
+ bui.imagewidget(
+ edit=img,
+ position=(
+ self._width * 0.5 - imgsize * scale * 0.5 + x,
+ self._height
+ - 223
+ + self._yoffs
+ + imgsize * 0.5
+ - imgsize * scale * 0.5,
+ ),
+ size=(imgsize * scale, imgsize * scale),
+ )
+
+ # Set initial place.
+ _set_img(0.0, 1.0)
+
+ sign = 1.0
+ while toffs < tendoffs:
+ toffs += 0.03 * random.uniform(0.5, 1.5)
+ sign = -sign
+ bui.apptimer(
+ toffs,
+ bui.Call(
+ _set_img,
+ x=(
+ 20.0
+ * random.uniform(0.3, 1.0)
+ * math.pow(toffs / tendoffs, 2.0)
+ * sign
+ ),
+ scale=1.0 - 0.2 * math.pow(toffs / tendoffs, 2.0),
+ ),
+ )
+
+ xspacing = 150
+ xoffs = -0.5 * (len(response.contents) - 1) * xspacing
+ bui.apptimer(
+ toffs - 0.2, lambda: bui.getsound('corkPop2').play(volume=4.0)
+ )
+ # Play a variety of voice sounds.
+
+ # We keep a global list of voice options which we randomly pull
+ # from and refill when empty. This ensures everything gets
+ # played somewhat frequently and minimizes annoying repeats.
+ global _g_open_voices # pylint: disable=global-statement
+ if not _g_open_voices:
+ _g_open_voices = [
+ (0.3, 'woo3', 2.5),
+ (0.1, 'gasp', 1.3),
+ (0.2, 'woo2', 2.0),
+ (0.2, 'wow', 2.0),
+ (0.2, 'kronk2', 2.0),
+ (0.2, 'mel03', 2.0),
+ (0.2, 'aww', 2.0),
+ (0.4, 'nice', 2.0),
+ (0.3, 'yeah', 1.5),
+ (0.2, 'woo', 1.0),
+ (0.5, 'ooh', 0.8),
+ ]
+
+ voicetimeoffs, voicename, volume = _g_open_voices.pop(
+ random.randrange(len(_g_open_voices))
+ )
+ bui.apptimer(
+ toffs + voicetimeoffs,
+ lambda: bui.getsound(voicename).play(volume=volume),
+ )
+
+ toffsopen = toffs
+ bui.apptimer(toffs, bui.WeakCall(self._show_chest_opening))
+ toffs += tincr * 1.0
+ width = xspacing * 0.75
+ for obj in response.contents:
+ toffs += tincr
+ bui.apptimer(
+ toffs - 0.1, lambda: bui.getsound('cashRegister').play()
+ )
+ bui.apptimer(
+ toffs, bui.WeakCall(self._show_chest_item, obj, xoffs, width)
+ )
+ xoffs += xspacing
+ toffs += tincr
+ bui.apptimer(toffs, bui.WeakCall(self._show_done_button))
+
+ self._show_odds(initial_highlighted_row=-1)
+
+ # Store this for later
+ self._prizeindex = response.prizeindex
+
+ # The final result was already randomly selected on the server,
+ # but we want to give the illusion of randomness here, so cycle
+ # through highlighting our options and stop on the winner when
+ # the chest opens. To do this, we start at the end at the prize
+ # and work backwards setting timers.
+ if self._prizesets:
+ toffs2 = toffsopen - 0.01
+ amt = 0.02
+ i = self._prizeindex
+ while toffs2 > 0.0:
+ bui.apptimer(
+ toffs2,
+ bui.WeakCall(self._highlight_odds_row, i),
+ )
+ toffs2 -= amt
+ amt *= 1.05 * random.uniform(0.9, 1.1)
+ i = (i - 1) % len(self._prizesets)
+
+ def _show_chest_opening(self) -> None:
+
+ # No-op if our ui is dead.
+ if not self._root_widget:
+ return
+
+ self._reset()
+ imgsize = 145
+ bui.getsound('hiss').play()
+ assert self._chestdisplayinfo is not None
+ img = bui.imagewidget(
+ parent=self._root_widget,
+ color=self._chestdisplayinfo.color,
+ texture=bui.gettexture(self._chestdisplayinfo.texopen),
+ tint_texture=bui.gettexture(self._chestdisplayinfo.texopentint),
+ tint_color=self._chestdisplayinfo.tint,
+ tint2_color=self._chestdisplayinfo.tint2,
+ )
+ tincr = 0.8
+ tendoffs = tincr * 2.0
+ toffs = 0.0
+
+ def _set_img(x: float, scale: float) -> None:
+ if not img:
+ return
+ bui.imagewidget(
+ edit=img,
+ position=(
+ self._width * 0.5 - imgsize * scale * 0.5 + x,
+ self._height
+ - 223
+ + self._yoffs
+ + imgsize * 0.5
+ - imgsize * scale * 0.5,
+ ),
+ size=(imgsize * scale, imgsize * scale),
+ )
+
+ # Set initial place.
+ _set_img(0.0, 1.0)
+
+ sign = 1.0
+ while toffs < tendoffs:
+ toffs += 0.03 * random.uniform(0.5, 1.5)
+ sign = -sign
+ # Note: we speed x along here (multing toffs) so position
+ # comes to rest before scale.
+ bui.apptimer(
+ toffs,
+ bui.Call(
+ _set_img,
+ x=(
+ 1.0
+ * random.uniform(0.3, 1.0)
+ * (
+ 1.0
+ - math.pow(min(1.0, 3.0 * toffs / tendoffs), 2.0)
+ )
+ * sign
+ ),
+ scale=1.0 - 0.1 * math.pow(toffs / tendoffs, 0.5),
+ ),
+ )
+
+ self._show_odds(
+ initial_highlighted_row=self._prizeindex,
+ initial_highlighted_extra=True,
+ )
+
+ def _show_chest_item(
+ self,
+ itemwrapper: bacommon.bs.DisplayItemWrapper,
+ xoffs: float,
+ width: float,
+ ) -> None:
+ # No-op if our ui is dead.
+ if not self._root_widget:
+ return
+
+ img: str | None = None
+ if isinstance(itemwrapper.item, bacommon.bs.TicketsDisplayItem):
+ img = 'tickets'
+ elif isinstance(itemwrapper.item, bacommon.bs.TokensDisplayItem):
+ img = 'coin'
+
+ # Translate the wrapper description and apply any subs.
+ descfin = bui.Lstr(
+ translate=('serverResponses', itemwrapper.description)
+ ).evaluate()
+ subs = (
+ []
+ if itemwrapper.description_subs is None
+ else itemwrapper.description_subs
+ )
+ assert len(subs) % 2 == 0 # Should always be even.
+ for j in range(0, len(subs) - 1, 2):
+ descfin = descfin.replace(subs[j], subs[j + 1])
+
+ imgsize = 34
+ if img is not None:
+ bui.imagewidget(
+ parent=self._root_widget,
+ position=(
+ self._width * 0.5 + xoffs - imgsize * 0.5,
+ self._height - 252 + 14.0 + self._yoffs - imgsize * 0.5,
+ ),
+ size=(imgsize, imgsize),
+ texture=bui.gettexture(img),
+ )
+ bui.textwidget(
+ parent=self._root_widget,
+ position=(
+ self._width * 0.5 + xoffs,
+ self._height - 252 - 14.0 + self._yoffs,
+ ),
+ scale=0.65,
+ size=(0, 0),
+ text=f'+ {descfin}',
+ maxwidth=width,
+ color=(0.0, 1.0, 0.0),
+ h_align='center',
+ v_align='center',
+ )
+
+ def _show_done_button(self) -> None:
+ # No-op if our ui is dead.
+ if not self._root_widget:
+ return
+
+ bwidth = 200
+ bheight = 60
+
+ btn = bui.buttonwidget(
+ parent=self._root_widget,
+ position=(
+ self._width * 0.5 - bwidth * 0.5,
+ self._height - 350 + self._yoffs,
+ ),
+ size=(bwidth, bheight),
+ label=bui.Lstr(resource='doneText'),
+ autoselect=True,
+ on_activate_call=self.main_window_back,
+ )
+ bui.containerwidget(edit=self._root_widget, start_button=btn)
+
+
+# Slight hack: we define window different classes for our different
+# chest slots so that the default UI behavior is to replace each other
+# when different ones are pressed. If they are all the same window class
+# then the default behavior for such presses is to toggle the existing
+# one back off.
class ChestWindow0(ChestWindow):
diff --git a/src/assets/ba_data/python/bauiv1lib/connectivity.py b/src/assets/ba_data/python/bauiv1lib/connectivity.py
index 8f76fc62..0b62315a 100644
--- a/src/assets/ba_data/python/bauiv1lib/connectivity.py
+++ b/src/assets/ba_data/python/bauiv1lib/connectivity.py
@@ -57,7 +57,7 @@ class WaitForConnectivityWindow(bui.Window):
)
bui.textwidget(
parent=self._root_widget,
- position=(self._width * 0.5, self._height * 0.65),
+ position=(self._width * 0.5, self._height * 0.7),
size=(0, 0),
scale=1.2,
h_align='center',
@@ -65,9 +65,15 @@ class WaitForConnectivityWindow(bui.Window):
text=bui.Lstr(resource='internal.connectingToPartyText'),
maxwidth=self._width * 0.9,
)
+
+ self._spinner = bui.spinnerwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5, self._height * 0.54),
+ )
+
self._info_text = bui.textwidget(
parent=self._root_widget,
- position=(self._width * 0.5, self._height * 0.45),
+ position=(self._width * 0.5, self._height * 0.4),
size=(0, 0),
color=(0.6, 0.5, 0.6),
flatness=1.0,
@@ -115,6 +121,15 @@ class WaitForConnectivityWindow(bui.Window):
def _connected(self) -> None:
if not self._root_widget or self._root_widget.transitioning_out:
return
+
+ # Show 'connected.' and kill the spinner for the brief moment
+ # we're visible on our way out.
+ bui.textwidget(
+ edit=self._info_text, text=bui.Lstr(resource='remote_app.connected')
+ )
+ if self._spinner:
+ self._spinner.delete()
+
bui.containerwidget(
edit=self._root_widget,
transition=('out_scale'),
diff --git a/src/assets/ba_data/python/bauiv1lib/coop/browser.py b/src/assets/ba_data/python/bauiv1lib/coop/browser.py
index c73137da..ca694656 100644
--- a/src/assets/ba_data/python/bauiv1lib/coop/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/coop/browser.py
@@ -231,11 +231,11 @@ class CoopBrowserWindow(bui.MainWindow):
# Don't want initial construction affecting our last-selected.
self._do_selection_callbacks = False
v = self._height - 95
- txt = bui.textwidget(
+ bui.textwidget(
parent=self._root_widget,
position=(
self._width * 0.5,
- v + 40 - (0 if uiscale is bui.UIScale.SMALL else 0),
+ v + 40 - (25 if uiscale is bui.UIScale.SMALL else 0),
),
size=(0, 0),
text=bui.Lstr(
@@ -244,14 +244,11 @@ class CoopBrowserWindow(bui.MainWindow):
),
h_align='center',
color=app.ui_v1.title_color,
- scale=1.5,
- maxwidth=500,
+ scale=0.85 if uiscale is bui.UIScale.SMALL else 1.5,
+ maxwidth=280 if uiscale is bui.UIScale.SMALL else 500,
v_align='center',
)
- if uiscale is bui.UIScale.SMALL:
- bui.textwidget(edit=txt, text='')
-
self._selected_row = cfg.get('Selected Coop Row', None)
self._scroll_width = self._width - (130 + 2 * x_inset)
diff --git a/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py b/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py
index 549f5b90..a7584685 100644
--- a/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py
+++ b/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py
@@ -12,6 +12,11 @@ import bauiv1 as bui
if TYPE_CHECKING:
from typing import Any, Callable
+# As of 1.7.37, no longer charging entry fees for tourneys (tourneys now
+# reward chests and the game now makes its money from tokens/ads used to
+# speed up chest openings).
+USE_ENTRY_FEES = False
+
class TournamentButton:
"""Button showing a tournament in coop window."""
@@ -25,6 +30,7 @@ class TournamentButton:
on_pressed: Callable[[TournamentButton], None],
) -> None:
# pylint: disable=too-many-positional-arguments
+ # pylint: disable=too-many-statements
self._r = 'coopSelectWindow'
sclx = 300
scly = 195.0
@@ -37,6 +43,7 @@ class TournamentButton:
self.has_time_remaining: bool = False
self.leader: Any = None
self.required_league: str | None = None
+ self._base_x_offs = 0 if USE_ENTRY_FEES else -45.0
self.button = btn = bui.buttonwidget(
parent=parent,
position=(x + 23, y + 4),
@@ -96,69 +103,72 @@ class TournamentButton:
header_color = (0.43, 0.4, 0.5, 1)
value_color = (0.6, 0.6, 0.6, 1)
- x_offs = 0
- bui.textwidget(
- parent=parent,
- draw_controller=btn,
- position=(x + 360, y + scly - 20),
- size=(0, 0),
- h_align='center',
- text=bui.Lstr(resource=f'{self._r}.entryFeeText'),
- v_align='center',
- maxwidth=100,
- scale=0.9,
- color=header_color,
- flatness=1.0,
- )
+ x_offs = self._base_x_offs
- self.entry_fee_text_top = bui.textwidget(
- parent=parent,
- draw_controller=btn,
- position=(x + 360, y + scly - 60),
- size=(0, 0),
- h_align='center',
- text='-',
- v_align='center',
- maxwidth=60,
- scale=1.3,
- color=value_color,
- flatness=1.0,
- )
- self.entry_fee_text_or = bui.textwidget(
- parent=parent,
- draw_controller=btn,
- position=(x + 360, y + scly - 90),
- size=(0, 0),
- h_align='center',
- text='',
- v_align='center',
- maxwidth=60,
- scale=0.5,
- color=value_color,
- flatness=1.0,
- )
- self.entry_fee_text_remaining = bui.textwidget(
- parent=parent,
- draw_controller=btn,
- position=(x + 360, y + scly - 90),
- size=(0, 0),
- h_align='center',
- text='',
- v_align='center',
- maxwidth=60,
- scale=0.5,
- color=value_color,
- flatness=1.0,
- )
+ # No longer using entry fees.
+ if USE_ENTRY_FEES:
+ bui.textwidget(
+ parent=parent,
+ draw_controller=btn,
+ position=(x + 360, y + scly - 20),
+ size=(0, 0),
+ h_align='center',
+ text=bui.Lstr(resource=f'{self._r}.entryFeeText'),
+ v_align='center',
+ maxwidth=100,
+ scale=0.9,
+ color=header_color,
+ flatness=1.0,
+ )
- self.entry_fee_ad_image = bui.imagewidget(
- parent=parent,
- size=(40, 40),
- draw_controller=btn,
- position=(x + 360 - 20, y + scly - 140),
- opacity=0.0,
- texture=bui.gettexture('tv'),
- )
+ self.entry_fee_text_top = bui.textwidget(
+ parent=parent,
+ draw_controller=btn,
+ position=(x + 360, y + scly - 60),
+ size=(0, 0),
+ h_align='center',
+ text='-',
+ v_align='center',
+ maxwidth=60,
+ scale=1.3,
+ color=value_color,
+ flatness=1.0,
+ )
+ self.entry_fee_text_or = bui.textwidget(
+ parent=parent,
+ draw_controller=btn,
+ position=(x + 360, y + scly - 90),
+ size=(0, 0),
+ h_align='center',
+ text='',
+ v_align='center',
+ maxwidth=60,
+ scale=0.5,
+ color=value_color,
+ flatness=1.0,
+ )
+ self.entry_fee_text_remaining = bui.textwidget(
+ parent=parent,
+ draw_controller=btn,
+ position=(x + 360, y + scly - 90),
+ size=(0, 0),
+ h_align='center',
+ text='',
+ v_align='center',
+ maxwidth=60,
+ scale=0.5,
+ color=value_color,
+ flatness=1.0,
+ )
+
+ self.entry_fee_ad_image = bui.imagewidget(
+ parent=parent,
+ size=(40, 40),
+ draw_controller=btn,
+ position=(x + 360 - 20, y + scly - 140),
+ opacity=0.0,
+ texture=bui.gettexture('tv'),
+ )
x_offs += 50
@@ -180,8 +190,8 @@ class TournamentButton:
self.button_y = y
self.button_scale_y = scly
- xo2 = 0
- prize_value_scale = 1.5
+ # Offset for prize range/values.
+ xo2 = 0.0
self.prize_range_1_text = bui.textwidget(
parent=parent,
@@ -191,7 +201,7 @@ class TournamentButton:
h_align='right',
v_align='center',
maxwidth=50,
- text='-',
+ text='',
scale=0.8,
color=header_color,
flatness=1.0,
@@ -202,13 +212,21 @@ class TournamentButton:
position=(x + 380 + xo2 + x_offs, y + scly - 93),
size=(0, 0),
h_align='left',
- text='-',
+ text='',
v_align='center',
maxwidth=100,
- scale=prize_value_scale,
color=value_color,
flatness=1.0,
)
+ self._chestsz = 50
+ self.prize_chest_1_image = bui.imagewidget(
+ parent=parent,
+ draw_controller=btn,
+ texture=bui.gettexture('white'),
+ position=(x + 380 + xo2 + x_offs, y + scly - 93),
+ size=(self._chestsz, self._chestsz),
+ opacity=0.0,
+ )
self.prize_range_2_text = bui.textwidget(
parent=parent,
@@ -216,6 +234,7 @@ class TournamentButton:
position=(x + 355 + xo2 + x_offs, y + scly - 93),
size=(0, 0),
h_align='right',
+ text='',
v_align='center',
maxwidth=50,
scale=0.8,
@@ -231,10 +250,17 @@ class TournamentButton:
text='',
v_align='center',
maxwidth=100,
- scale=prize_value_scale,
color=value_color,
flatness=1.0,
)
+ self.prize_chest_2_image = bui.imagewidget(
+ parent=parent,
+ draw_controller=btn,
+ texture=bui.gettexture('white'),
+ position=(x + 380 + xo2 + x_offs, y + scly - 93),
+ size=(self._chestsz, self._chestsz),
+ opacity=0.0,
+ )
self.prize_range_3_text = bui.textwidget(
parent=parent,
@@ -242,6 +268,7 @@ class TournamentButton:
position=(x + 355 + xo2 + x_offs, y + scly - 93),
size=(0, 0),
h_align='right',
+ text='',
v_align='center',
maxwidth=50,
scale=0.8,
@@ -257,15 +284,22 @@ class TournamentButton:
text='',
v_align='center',
maxwidth=100,
- scale=prize_value_scale,
color=value_color,
flatness=1.0,
)
+ self.prize_chest_3_image = bui.imagewidget(
+ parent=parent,
+ draw_controller=btn,
+ texture=bui.gettexture('white'),
+ position=(x + 380 + xo2 + x_offs, y + scly - 93),
+ size=(self._chestsz, self._chestsz),
+ opacity=0.0,
+ )
bui.textwidget(
parent=parent,
draw_controller=btn,
- position=(x + 620 + x_offs, y + scly - 20),
+ position=(x + 625 + x_offs, y + scly - 20),
size=(0, 0),
h_align='center',
text=bui.Lstr(resource=f'{self._r}.currentBestText'),
@@ -279,7 +313,7 @@ class TournamentButton:
parent=parent,
draw_controller=btn,
position=(
- x + 620 + x_offs - (170 / 1.4) * 0.5,
+ x + 625 + x_offs - (170 / 1.4) * 0.5,
y + scly - 60 - 40 * 0.5,
),
selectable=True,
@@ -299,7 +333,7 @@ class TournamentButton:
self.current_leader_score_text = bui.textwidget(
parent=parent,
draw_controller=btn,
- position=(x + 620 + x_offs, y + scly - 113 + 10),
+ position=(x + 625 + x_offs, y + scly - 113 + 10),
size=(0, 0),
h_align='center',
text='-',
@@ -312,7 +346,7 @@ class TournamentButton:
self.more_scores_button = bui.buttonwidget(
parent=parent,
- position=(x + 620 + x_offs - 60, y + scly - 50 - 125),
+ position=(x + 625 + x_offs - 60, y + scly - 50 - 125),
color=(0.5, 0.5, 0.6),
textcolor=(0.7, 0.7, 0.8),
label='-',
@@ -330,7 +364,7 @@ class TournamentButton:
bui.textwidget(
parent=parent,
draw_controller=btn,
- position=(x + 820 + x_offs, y + scly - 20),
+ position=(x + 840 + x_offs, y + scly - 20),
size=(0, 0),
h_align='center',
text=bui.Lstr(resource=f'{self._r}.timeRemainingText'),
@@ -343,7 +377,7 @@ class TournamentButton:
self.time_remaining_value_text = bui.textwidget(
parent=parent,
draw_controller=btn,
- position=(x + 820 + x_offs, y + scly - 68),
+ position=(x + 840 + x_offs, y + scly - 68),
size=(0, 0),
h_align='center',
text='-',
@@ -356,7 +390,7 @@ class TournamentButton:
self.time_remaining_out_of_text = bui.textwidget(
parent=parent,
draw_controller=btn,
- position=(x + 820 + x_offs, y + scly - 110),
+ position=(x + 840 + x_offs, y + scly - 110),
size=(0, 0),
h_align='center',
text='-',
@@ -415,26 +449,26 @@ class TournamentButton:
plus = bui.app.plus
assert plus is not None
- assert bui.app.classic is not None
+ classic = bui.app.classic
+ assert classic is not None
+
prize_y_offs = (
34
if 'prizeRange3' in entry
else 20 if 'prizeRange2' in entry else 12
)
- x_offs = 90
+ x_offs = self._base_x_offs + 90
- # pylint: disable=useless-suppression
- # pylint: disable=unbalanced-tuple-unpacking
- (
- pr1,
- pv1,
- pr2,
- pv2,
- pr3,
- pv3,
- ) = bui.app.classic.get_tournament_prize_strings(entry)
- # pylint: enable=unbalanced-tuple-unpacking
- # pylint: enable=useless-suppression
+ # Special offset for prize ranges/vals.
+ x_offs2 = x_offs - 20.0
+
+ # Special offset for prize chests.
+ x_offs2c = x_offs2 + 50
+
+ # Fetch prize range and trophy strings.
+ (pr1, pv1, pr2, pv2, pr3, pv3) = classic.get_tournament_prize_strings(
+ entry, include_tickets=False
+ )
enabled = 'requiredLeague' not in entry
bui.buttonwidget(
@@ -446,74 +480,91 @@ class TournamentButton:
edit=self.prize_range_1_text,
text='-' if pr1 == '' else pr1,
position=(
- self.button_x + 365 + x_offs,
+ self.button_x + 365 + x_offs2,
self.button_y + self.button_scale_y - 93 + prize_y_offs,
),
)
- # We want to draw values containing tickets a bit smaller
- # (scratch that; we now draw medals a bit bigger).
- ticket_char = bui.charstr(bui.SpecialChar.TICKET_BACKING)
- prize_value_scale_large = 1.0
- prize_value_scale_small = 1.0
-
bui.textwidget(
edit=self.prize_value_1_text,
text='-' if pv1 == '' else pv1,
- scale=(
- prize_value_scale_large
- if ticket_char not in pv1
- else prize_value_scale_small
- ),
position=(
- self.button_x + 380 + x_offs,
+ self.button_x + 380 + x_offs2,
self.button_y + self.button_scale_y - 93 + prize_y_offs,
),
)
+ bui.imagewidget(
+ edit=self.prize_chest_1_image,
+ position=(
+ self.button_x + 380 + x_offs2c,
+ self.button_y
+ + self.button_scale_y
+ - 93
+ + prize_y_offs
+ - 0.5 * self._chestsz,
+ ),
+ )
+ classic.set_tournament_prize_image(entry, 0, self.prize_chest_1_image)
bui.textwidget(
edit=self.prize_range_2_text,
text=pr2,
position=(
- self.button_x + 365 + x_offs,
+ self.button_x + 365 + x_offs2,
self.button_y + self.button_scale_y - 93 - 45 + prize_y_offs,
),
)
bui.textwidget(
edit=self.prize_value_2_text,
text=pv2,
- scale=(
- prize_value_scale_large
- if ticket_char not in pv2
- else prize_value_scale_small
- ),
position=(
- self.button_x + 380 + x_offs,
+ self.button_x + 380 + x_offs2,
self.button_y + self.button_scale_y - 93 - 45 + prize_y_offs,
),
)
+ bui.imagewidget(
+ edit=self.prize_chest_2_image,
+ position=(
+ self.button_x + 380 + x_offs2c,
+ self.button_y
+ + self.button_scale_y
+ - 93
+ - 45
+ + prize_y_offs
+ - 0.5 * self._chestsz,
+ ),
+ )
+ classic.set_tournament_prize_image(entry, 1, self.prize_chest_2_image)
bui.textwidget(
edit=self.prize_range_3_text,
text=pr3,
position=(
- self.button_x + 365 + x_offs,
+ self.button_x + 365 + x_offs2,
self.button_y + self.button_scale_y - 93 - 90 + prize_y_offs,
),
)
bui.textwidget(
edit=self.prize_value_3_text,
text=pv3,
- scale=(
- prize_value_scale_large
- if ticket_char not in pv3
- else prize_value_scale_small
- ),
position=(
- self.button_x + 380 + x_offs,
+ self.button_x + 380 + x_offs2,
self.button_y + self.button_scale_y - 93 - 90 + prize_y_offs,
),
)
+ bui.imagewidget(
+ edit=self.prize_chest_3_image,
+ position=(
+ self.button_x + 380 + x_offs2c,
+ self.button_y
+ + self.button_scale_y
+ - 93
+ - 90
+ + prize_y_offs
+ - 0.5 * self._chestsz,
+ ),
+ )
+ classic.set_tournament_prize_image(entry, 2, self.prize_chest_3_image)
leader_name = '-'
leader_score: str | bui.Lstr = '-'
@@ -599,6 +650,7 @@ class TournamentButton:
)
fee = entry['fee']
+ assert isinstance(fee, int | None)
if fee is None:
fee_var = None
@@ -610,18 +662,23 @@ class TournamentButton:
fee_var = 'price.tournament_entry_2'
elif fee == 1:
fee_var = 'price.tournament_entry_1'
+ elif fee == -1:
+ fee_var = None
else:
if fee != 0:
print('Unknown fee value:', fee)
fee_var = 'price.tournament_entry_0'
- self.allow_ads = allow_ads = entry['allowAds']
+ self.allow_ads = allow_ads = (
+ entry['allowAds'] if USE_ENTRY_FEES else False
+ )
- final_fee: int | None = (
+ final_fee = (
None
if fee_var is None
else plus.get_v1_account_misc_read_val(fee_var, '?')
)
+ assert isinstance(final_fee, int | None)
final_fee_str: str | bui.Lstr
if fee_var is None:
@@ -638,72 +695,77 @@ class TournamentButton:
ad_tries_remaining = bui.app.classic.accounts.tournament_info[
self.tournament_id
]['adTriesRemaining']
+ assert isinstance(ad_tries_remaining, int | None)
free_tries_remaining = bui.app.classic.accounts.tournament_info[
self.tournament_id
]['freeTriesRemaining']
+ assert isinstance(free_tries_remaining, int | None)
# Now, if this fee allows ads and we support video ads, show
# the 'or ad' version.
- if allow_ads and plus.has_video_ads():
- ads_enabled = plus.have_incentivized_ad()
- bui.imagewidget(
- edit=self.entry_fee_ad_image,
- opacity=1.0 if ads_enabled else 0.25,
- )
- or_text = (
- bui.Lstr(resource='orText', subs=[('${A}', ''), ('${B}', '')])
- .evaluate()
- .strip()
- )
- bui.textwidget(edit=self.entry_fee_text_or, text=or_text)
- bui.textwidget(
- edit=self.entry_fee_text_top,
- position=(
- self.button_x + 360,
- self.button_y + self.button_scale_y - 60,
- ),
- scale=1.3,
- text=final_fee_str,
- )
+ if USE_ENTRY_FEES:
+ if allow_ads and plus.has_video_ads():
+ ads_enabled = plus.have_incentivized_ad()
+ bui.imagewidget(
+ edit=self.entry_fee_ad_image,
+ opacity=1.0 if ads_enabled else 0.25,
+ )
+ or_text = (
+ bui.Lstr(
+ resource='orText', subs=[('${A}', ''), ('${B}', '')]
+ )
+ .evaluate()
+ .strip()
+ )
+ bui.textwidget(edit=self.entry_fee_text_or, text=or_text)
+ bui.textwidget(
+ edit=self.entry_fee_text_top,
+ position=(
+ self.button_x + 360,
+ self.button_y + self.button_scale_y - 60,
+ ),
+ scale=1.3,
+ text=final_fee_str,
+ )
- # Possibly show number of ad-plays remaining.
- bui.textwidget(
- edit=self.entry_fee_text_remaining,
- position=(
- self.button_x + 360,
- self.button_y + self.button_scale_y - 146,
- ),
- text=(
- ''
- if ad_tries_remaining in [None, 0]
- else ('' + str(ad_tries_remaining))
- ),
- color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2),
- )
- else:
- bui.imagewidget(edit=self.entry_fee_ad_image, opacity=0.0)
- bui.textwidget(edit=self.entry_fee_text_or, text='')
- bui.textwidget(
- edit=self.entry_fee_text_top,
- position=(
- self.button_x + 360,
- self.button_y + self.button_scale_y - 80,
- ),
- scale=1.3,
- text=final_fee_str,
- )
+ # Possibly show number of ad-plays remaining.
+ bui.textwidget(
+ edit=self.entry_fee_text_remaining,
+ position=(
+ self.button_x + 360,
+ self.button_y + self.button_scale_y - 146,
+ ),
+ text=(
+ ''
+ if ad_tries_remaining in [None, 0]
+ else ('' + str(ad_tries_remaining))
+ ),
+ color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2),
+ )
+ else:
+ bui.imagewidget(edit=self.entry_fee_ad_image, opacity=0.0)
+ bui.textwidget(edit=self.entry_fee_text_or, text='')
+ bui.textwidget(
+ edit=self.entry_fee_text_top,
+ position=(
+ self.button_x + 360,
+ self.button_y + self.button_scale_y - 80,
+ ),
+ scale=1.3,
+ text=final_fee_str,
+ )
- # Possibly show number of free-plays remaining.
- bui.textwidget(
- edit=self.entry_fee_text_remaining,
- position=(
- self.button_x + 360,
- self.button_y + self.button_scale_y - 100,
- ),
- text=(
- ''
- if (free_tries_remaining in [None, 0] or final_fee != 0)
- else ('' + str(free_tries_remaining))
- ),
- color=(0.6, 0.6, 0.6, 1),
- )
+ # Possibly show number of free-plays remaining.
+ bui.textwidget(
+ edit=self.entry_fee_text_remaining,
+ position=(
+ self.button_x + 360,
+ self.button_y + self.button_scale_y - 100,
+ ),
+ text=(
+ ''
+ if (free_tries_remaining in [None, 0] or final_fee != 0)
+ else ('' + str(free_tries_remaining))
+ ),
+ color=(0.6, 0.6, 0.6, 1),
+ )
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py
index dc185f2d..a2744290 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py
@@ -62,7 +62,7 @@ class PrivateGatherTab(GatherTab):
self._state: State = State()
self._last_datacode_refresh_time: float | None = None
self._hostingstate = PrivateHostingState()
- self._v2state: bacommon.cloud.BSPrivatePartyResponse | None = None
+ self._v2state: bacommon.bs.PrivatePartyResponse | None = None
self._join_sub_tab_text: bui.Widget | None = None
self._host_sub_tab_text: bui.Widget | None = None
self._update_timer: bui.AppTimer | None = None
@@ -339,7 +339,7 @@ class PrivateGatherTab(GatherTab):
if plus.accounts.primary is not None:
with plus.accounts.primary:
plus.cloud.send_message_cb(
- bacommon.cloud.BSPrivatePartyMessage(
+ bacommon.bs.PrivatePartyMessage(
need_datacode=(
self._last_datacode_refresh_time is None
or time.monotonic()
@@ -355,7 +355,7 @@ class PrivateGatherTab(GatherTab):
self._last_v2_state_query_time = now
def _on_private_party_query_response(
- self, response: bacommon.cloud.BSPrivatePartyResponse | Exception
+ self, response: bacommon.bs.PrivatePartyResponse | Exception
) -> None:
if isinstance(response, Exception):
self._debug_server_comm('got pp v2 state response (err)')
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py
index eb6804ca..25d666ff 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py
@@ -367,6 +367,7 @@ class PublicGatherTab(GatherTab):
self._last_server_list_query_time: float | None = None
self._join_list_column: bui.Widget | None = None
self._join_status_text: bui.Widget | None = None
+ self._join_status_spinner: bui.Widget | None = None
self._no_servers_found_text: bui.Widget | None = None
self._host_max_party_size_value: bui.Widget | None = None
self._host_max_party_size_minus_button: bui.Widget | None = None
@@ -665,6 +666,9 @@ class PublicGatherTab(GatherTab):
size=(400, 400),
claims_left_right=True,
)
+
+ # Create join status text and join spinner. Always make sure to
+ # update both of these together.
self._join_status_text = bui.textwidget(
parent=self._container,
text='',
@@ -678,6 +682,10 @@ class PublicGatherTab(GatherTab):
color=(0.6, 0.6, 0.6),
position=(c_width * 0.5, c_height * 0.5),
)
+ self._join_status_spinner = bui.spinnerwidget(
+ parent=self._container, position=(c_width * 0.5, c_height * 0.5)
+ )
+
self._no_servers_found_text = bui.textwidget(
parent=self._container,
text='',
@@ -944,37 +952,51 @@ class PublicGatherTab(GatherTab):
name = cast(str, bui.textwidget(query=self._host_name_text))
bs.set_public_party_name(name)
- # Update status text.
- status_text = self._join_status_text
- if status_text:
+ # Update status text and loading spinner.
+ if self._join_status_text:
+ assert self._join_status_spinner
if not signed_in:
bui.textwidget(
- edit=status_text, text=bui.Lstr(resource='notSignedInText')
+ edit=self._join_status_text,
+ text=bui.Lstr(resource='notSignedInText'),
)
+ bui.spinnerwidget(edit=self._join_status_spinner, visible=False)
else:
# If we have a valid list, show no status; just the list.
# Otherwise show either 'loading...' or 'error' depending
# on whether this is our first go-round.
if self._have_valid_server_list:
- bui.textwidget(edit=status_text, text='')
+ bui.textwidget(edit=self._join_status_text, text='')
+ bui.spinnerwidget(
+ edit=self._join_status_spinner, visible=False
+ )
else:
if self._have_server_list_response:
bui.textwidget(
- edit=status_text,
+ edit=self._join_status_text,
text=bui.Lstr(resource='errorText'),
)
+ bui.spinnerwidget(
+ edit=self._join_status_spinner, visible=False
+ )
else:
- bui.textwidget(
- edit=status_text,
- text=bui.Lstr(
- value='${A}...',
- subs=[
- (
- '${A}',
- bui.Lstr(resource='store.loadingText'),
- )
- ],
- ),
+ # Show our loading spinner.
+ bui.textwidget(edit=self._join_status_text, text='')
+ # bui.textwidget(
+ # edit=self._join_status_text,
+ # text=bui.Lstr(
+ # value='${A}...',
+ # subs=[
+ # (
+ # '${A}',
+ #
+ # bui.Lstr(resource='store.loadingText'),
+ # )
+ # ],
+ # ),
+ # )
+ bui.spinnerwidget(
+ edit=self._join_status_spinner, visible=True
)
self._update_party_rows()
@@ -1005,16 +1027,11 @@ class PublicGatherTab(GatherTab):
self._ui_rows = self._ui_rows[:-clipcount]
# If we have no parties to show, we're done.
- if not self._parties_displayed:
- text = self._join_status_text
- if (
- plus.get_v1_account_state() == 'signed_in'
- and cast(str, bui.textwidget(query=text)) == ''
- ):
- bui.textwidget(
- edit=self._no_servers_found_text,
- text=bui.Lstr(resource='noServersFoundText'),
- )
+ if self._have_valid_server_list and not self._parties_displayed:
+ bui.textwidget(
+ edit=self._no_servers_found_text,
+ text=bui.Lstr(resource='noServersFoundText'),
+ )
return
sub_scroll_width = 830
diff --git a/src/assets/ba_data/python/bauiv1lib/gettokens.py b/src/assets/ba_data/python/bauiv1lib/gettokens.py
index c923ae18..a29971c7 100644
--- a/src/assets/ba_data/python/bauiv1lib/gettokens.py
+++ b/src/assets/ba_data/python/bauiv1lib/gettokens.py
@@ -863,7 +863,7 @@ def show_get_tokens_prompt() -> None:
if bool(True):
ConfirmWindow(
bui.Lstr(resource='tokens.notEnoughTokensText'),
- GetTokensWindow,
+ _show_get_tokens,
ok_text=bui.Lstr(resource='tokens.getTokensText'),
width=460,
height=130,
@@ -875,3 +875,30 @@ def show_get_tokens_prompt() -> None:
width=460,
height=130,
)
+
+
+def _show_get_tokens() -> None:
+
+ # NOTE TO USERS: The code below is not the proper way to do things;
+ # whenever possible one should use a MainWindow's
+ # main_window_replace() or main_window_back() methods. We just need
+ # to do things a bit more manually in this case.
+
+ prev_main_window = bui.app.ui_v1.get_main_window()
+
+ # Special-case: If it seems we're already in the account window, do
+ # nothing.
+ if isinstance(prev_main_window, GetTokensWindow):
+ return
+
+ # Set our new main window.
+ bui.app.ui_v1.set_main_window(
+ GetTokensWindow(),
+ from_window=False,
+ is_auxiliary=True,
+ suppress_warning=True,
+ )
+
+ # Transition out any previous main window.
+ if prev_main_window is not None:
+ prev_main_window.main_window_close()
diff --git a/src/assets/ba_data/python/bauiv1lib/inbox.py b/src/assets/ba_data/python/bauiv1lib/inbox.py
index 6996bb29..2783208f 100644
--- a/src/assets/ba_data/python/bauiv1lib/inbox.py
+++ b/src/assets/ba_data/python/bauiv1lib/inbox.py
@@ -6,30 +6,100 @@ from __future__ import annotations
import weakref
from dataclasses import dataclass
-from typing import override
+from typing import override, assert_never
from efro.error import CommunicationError
-import bacommon.cloud
+import bacommon.bs
import bauiv1 as bui
-# Messages with format versions higher than this will show up as
-# 'app needs to be updated to view this'
-SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION = 1
+
+class _Section:
+ def get_height(self) -> float:
+ """Return section height."""
+ raise NotImplementedError()
+
+ def draw(self, subcontainer: bui.Widget, y: float) -> None:
+ """Draw the section."""
+
+
+class _TextSection(_Section):
+
+ def __init__(
+ self,
+ sub_width: float,
+ text: str,
+ *,
+ subs: list[str],
+ spacing_top: float = 0.0,
+ spacing_bottom: float = 0.0,
+ scale: float = 0.6,
+ color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
+ ) -> None:
+ self.sub_width = sub_width
+ self.spacing_top = spacing_top
+ self.spacing_bottom = spacing_bottom
+ self.color = color
+
+ self.textfin = bui.Lstr(translate=('serverResponses', text)).evaluate()
+ assert len(subs) % 2 == 0 # Should always be even.
+ for j in range(0, len(subs) - 1, 2):
+ self.textfin = self.textfin.replace(subs[j], subs[j + 1])
+
+ # Calc scale to fit width and then see what height we need at
+ # that scale.
+ t_width = max(
+ 10.0,
+ bui.get_string_width(self.textfin, suppress_warning=True) * scale,
+ )
+ self.text_scale = scale * min(1.0, (sub_width * 0.9) / t_width)
+
+ self.text_height = (
+ 0.0
+ if not self.textfin
+ else bui.get_string_height(self.textfin, suppress_warning=True)
+ ) * self.text_scale
+
+ self.full_height = self.text_height + spacing_top + spacing_bottom
+
+ @override
+ def get_height(self) -> float:
+ return self.full_height
+
+ @override
+ def draw(self, subcontainer: bui.Widget, y: float) -> None:
+ bui.textwidget(
+ parent=subcontainer,
+ position=(
+ self.sub_width * 0.5,
+ y - self.spacing_top - self.text_height * 0.5,
+ # y - self.height * 0.5 - 23.0,
+ ),
+ color=self.color,
+ scale=self.text_scale,
+ flatness=1.0,
+ shadow=0.0,
+ text=self.textfin,
+ size=(0, 0),
+ h_align='center',
+ v_align='center',
+ )
@dataclass
-class _MessageEntry:
- type: bacommon.cloud.BSInboxEntryType
+class _EntryDisplay:
+ interaction_style: bacommon.bs.BasicClientUI.InteractionStyle
+ button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel
+ button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel
+ sections: list[_Section]
id: str
- height: float
- text_height: float
- scale: float
- text: str
+ total_height: float
color: tuple[float, float, float]
backing: bui.Widget | None = None
button_positive: bui.Widget | None = None
+ button_spinner_positive: bui.Widget | None = None
button_negative: bui.Widget | None = None
- message_text: bui.Widget | None = None
+ button_spinner_negative: bui.Widget | None = None
+ # message_text: bui.Widget | None = None
processing_complete: bool = False
@@ -45,15 +115,15 @@ class InboxWindow(bui.MainWindow):
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
- self._message_entries: list[_MessageEntry] = []
+ self._entry_displays: list[_EntryDisplay] = []
- self._width = 600 if uiscale is bui.UIScale.SMALL else 450
+ self._width = 800 if uiscale is bui.UIScale.SMALL else 500
self._height = (
- 375
+ 455
if uiscale is bui.UIScale.SMALL
else 370 if uiscale is bui.UIScale.MEDIUM else 450
)
- yoffs = -47 if uiscale is bui.UIScale.SMALL else 0
+ yoffs = -42 if uiscale is bui.UIScale.SMALL else 0
super().__init__(
root_widget=bui.containerwidget(
@@ -62,9 +132,9 @@ class InboxWindow(bui.MainWindow):
'menu_full' if uiscale is bui.UIScale.SMALL else 'menu_full'
),
scale=(
- 2.3
+ 1.7
if uiscale is bui.UIScale.SMALL
- else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
+ else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.15
),
stack_offset=(
(0, 0)
@@ -101,7 +171,7 @@ class InboxWindow(bui.MainWindow):
position=(
self._width * 0.5,
self._height
- - (27 if uiscale is bui.UIScale.SMALL else 20)
+ - (24 if uiscale is bui.UIScale.SMALL else 20)
+ yoffs,
),
size=(0, 0),
@@ -122,11 +192,15 @@ class InboxWindow(bui.MainWindow):
flatness=1.0,
color=(0.4, 0.4, 0.5),
shadow=0.0,
- text=bui.Lstr(resource='loadingText'),
+ text='',
size=(0, 0),
h_align='center',
v_align='center',
)
+ self._loading_spinner = bui.spinnerwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5, self._height * 0.5),
+ )
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
size=(
@@ -141,6 +215,7 @@ class InboxWindow(bui.MainWindow):
simple_culling_v=200,
claims_left_right=True,
claims_up_down=True,
+ center_small_content_horizontally=True,
)
bui.widget(edit=self._scrollwidget, autoselect=True)
if uiscale is bui.UIScale.SMALL:
@@ -163,7 +238,7 @@ class InboxWindow(bui.MainWindow):
with plus.accounts.primary:
plus.cloud.send_message_cb(
- bacommon.cloud.BSInboxRequestMessage(),
+ bacommon.bs.InboxRequestMessage(),
on_response=bui.WeakCall(self._on_inbox_request_response),
)
@@ -179,26 +254,33 @@ class InboxWindow(bui.MainWindow):
def _error(self, errmsg: bui.Lstr | str) -> None:
"""Put ourself in a permanent error state."""
+ bui.spinnerwidget(edit=self._loading_spinner, visible=False)
bui.textwidget(
edit=self._infotext,
color=(1, 0, 0),
text=errmsg,
)
- def _on_message_entry_press(
+ def _on_entry_display_press(
self,
- entry_weak: weakref.ReferenceType[_MessageEntry],
- process_type: bacommon.cloud.BSInboxEntryProcessType,
+ display_weak: weakref.ReferenceType[_EntryDisplay],
+ action: bacommon.bs.ClientUIAction,
) -> None:
- entry = entry_weak()
- if entry is None:
+ display = display_weak()
+ if display is None:
return
- self._neuter_message_entry(entry)
+ bui.getsound('click01').play()
- # We don't do anything for invalid messages.
- if entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN:
- entry.processing_complete = True
+ self._neuter_entry_display(display)
+
+ # We currently only recognize basic entries and their possible
+ # interaction types.
+ if (
+ display.interaction_style
+ is bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN
+ ):
+ display.processing_complete = True
self._close_soon_if_all_processed()
return
@@ -211,38 +293,43 @@ class InboxWindow(bui.MainWindow):
bui.getsound('error').play()
return
- # Message the master-server to process the entry.
+ # Ask the master-server to run our action.
with plus.accounts.primary:
plus.cloud.send_message_cb(
- bacommon.cloud.BSInboxEntryProcessMessage(
- entry.id, process_type
- ),
+ bacommon.bs.ClientUIActionMessage(display.id, action),
on_response=bui.WeakCall(
- self._on_inbox_entry_process_response,
- entry_weak,
- process_type,
+ self._on_client_ui_action_response,
+ display_weak,
+ action,
),
)
- # Tweak the button to show this is in progress.
+ # Tweak the UI to show that things are in motion.
button = (
- entry.button_positive
- if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE
- else entry.button_negative
+ display.button_positive
+ if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE
+ else display.button_negative
+ )
+ button_spinner = (
+ display.button_spinner_positive
+ if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE
+ else display.button_spinner_negative
)
if button is not None:
- bui.buttonwidget(edit=button, label='...')
+ bui.buttonwidget(edit=button, label='')
+ if button_spinner is not None:
+ bui.spinnerwidget(edit=button_spinner, visible=True)
def _close_soon_if_all_processed(self) -> None:
bui.apptimer(0.25, bui.WeakCall(self._close_if_all_processed))
def _close_if_all_processed(self) -> None:
- if not all(m.processing_complete for m in self._message_entries):
+ if not all(m.processing_complete for m in self._entry_displays):
return
self.main_window_back()
- def _neuter_message_entry(self, entry: _MessageEntry) -> None:
+ def _neuter_entry_display(self, entry: _EntryDisplay) -> None:
errsound = bui.getsound('error')
if entry.button_positive is not None:
bui.buttonwidget(
@@ -260,22 +347,20 @@ class InboxWindow(bui.MainWindow):
)
if entry.backing is not None:
bui.imagewidget(edit=entry.backing, color=(0.4, 0.4, 0.4))
- if entry.message_text is not None:
- bui.textwidget(edit=entry.message_text, color=(0.5, 0.5, 0.5, 0.5))
- def _on_inbox_entry_process_response(
+ def _on_client_ui_action_response(
self,
- entry_weak: weakref.ReferenceType[_MessageEntry],
- process_type: bacommon.cloud.BSInboxEntryProcessType,
- response: bacommon.cloud.BSInboxEntryProcessResponse | Exception,
+ display_weak: weakref.ReferenceType[_EntryDisplay],
+ action: bacommon.bs.ClientUIAction,
+ response: bacommon.bs.ClientUIActionResponse | Exception,
) -> None:
# pylint: disable=too-many-branches
- entry = entry_weak()
- if entry is None:
+ display = display_weak()
+ if display is None:
return
- assert not entry.processing_complete
- entry.processing_complete = True
+ assert not display.processing_complete
+ display.processing_complete = True
self._close_soon_if_all_processed()
# No-op if our UI is dead or on its way out.
@@ -284,10 +369,18 @@ class InboxWindow(bui.MainWindow):
# Tweak the button to show results.
button = (
- entry.button_positive
- if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE
- else entry.button_negative
+ display.button_positive
+ if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE
+ else display.button_negative
)
+ button_spinner = (
+ display.button_spinner_positive
+ if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE
+ else display.button_spinner_negative
+ )
+ # Always hide spinner at this point.
+ if button_spinner is not None:
+ bui.spinnerwidget(edit=button_spinner, visible=False)
# See if we should show an error message.
if isinstance(response, Exception):
@@ -297,9 +390,11 @@ class InboxWindow(bui.MainWindow):
)
else:
error_message = bui.Lstr(resource='errorText')
- elif response.error is not None:
+ elif response.error_type is not None:
+ # If error_type is set, error should be also.
+ assert response.error_message is not None
error_message = bui.Lstr(
- translate=('serverResponses', response.error)
+ translate=('serverResponses', response.error_message)
)
else:
error_message = None
@@ -314,6 +409,13 @@ class InboxWindow(bui.MainWindow):
)
return
+ # Success!
+ assert not isinstance(response, Exception)
+
+ # Run any bundled effects.
+ assert bui.app.classic is not None
+ bui.app.classic.run_bs_client_effects(response.effects)
+
# Whee; no error. Mark as done.
if button is not None:
# If we have full unicode, just show a checkmark in all cases.
@@ -321,24 +423,11 @@ class InboxWindow(bui.MainWindow):
if bui.supports_unicode_display():
label = '✓'
else:
- # For positive claim buttons, say 'success'.
- # Otherwise default to 'done.'
- if (
- entry.type
- in {
- bacommon.cloud.BSInboxEntryType.CLAIM,
- bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD,
- }
- and process_type
- is bacommon.cloud.BSInboxEntryProcessType.POSITIVE
- ):
- label = bui.Lstr(resource='successText')
- else:
- label = bui.Lstr(resource='doneText')
+ label = bui.Lstr(resource='doneText')
bui.buttonwidget(edit=button, label=label)
def _on_inbox_request_response(
- self, response: bacommon.cloud.BSInboxRequestResponse | Exception
+ self, response: bacommon.bs.InboxRequestResponse | Exception
) -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
@@ -364,11 +453,12 @@ class InboxWindow(bui.MainWindow):
self._error(errmsg)
return
- assert isinstance(response, bacommon.cloud.BSInboxRequestResponse)
+ assert isinstance(response, bacommon.bs.InboxRequestResponse)
# If we got no messages, don't touch anything. This keeps
# keyboard control working in the empty case.
- if not response.entries:
+ if not response.wrappers:
+ bui.spinnerwidget(edit=self._loading_spinner, visible=False)
bui.textwidget(
edit=self._infotext,
color=(0.4, 0.4, 0.5),
@@ -376,63 +466,96 @@ class InboxWindow(bui.MainWindow):
)
return
+ bui.spinnerwidget(edit=self._loading_spinner, visible=False)
bui.textwidget(edit=self._infotext, text='')
- sub_width = self._width - 90
+ # Even though our window size varies with uiscale, we want
+ # notifications to target a fixed width.
+ sub_width = 400.0
sub_height = 0.0
- # Run the math on row heights/etc.
- for i, entry in enumerate(response.entries):
+ # Construct entries for everything we'll display.
+ for i, wrapper in enumerate(response.wrappers):
+
# We need to flatten text here so we can measure it.
- textfin: str
+ # textfin: str
color: tuple[float, float, float]
- # Messages with either newer formatting or unrecognized
- # types show up as 'upgrade your app to see this'.
- if (
- entry.format_version > SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION
- or entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN
- ):
- textfin = bui.Lstr(
- translate=(
- 'serverResponses',
- 'You must update the app to view this.',
- )
- ).evaluate()
- color = (0.6, 0.6, 0.6)
- else:
- # Translate raw response and apply any replacements.
- textfin = bui.Lstr(
- translate=('serverResponses', entry.message)
- ).evaluate()
- assert len(entry.subs) % 2 == 0 # Should always be even.
- for j in range(0, len(entry.subs) - 1, 2):
- textfin = textfin.replace(entry.subs[j], entry.subs[j + 1])
- color = (0.55, 0.5, 0.7)
+ interaction_style: bacommon.bs.BasicClientUI.InteractionStyle
+ button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel
+ button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel
- # Calc scale to fit width and then see what height we need
- # at that scale.
- t_width = max(
- 10.0, bui.get_string_width(textfin, suppress_warning=True)
- )
- scale = min(0.6, (sub_width * 0.9) / t_width)
- t_height = (
- max(10.0, bui.get_string_height(textfin, suppress_warning=True))
- * scale
- )
- entry_height = 90.0 + t_height
- self._message_entries.append(
- _MessageEntry(
- type=entry.type,
- id=entry.id,
- height=entry_height,
- text_height=t_height,
- scale=scale,
- text=textfin,
+ sections: list[_Section] = []
+ total_height = 90.0
+
+ # Display only entries where we recognize all style/label
+ # values and ui component types.
+ if (
+ isinstance(wrapper.ui, bacommon.bs.BasicClientUI)
+ and not wrapper.ui.contains_unknown_elements()
+ ):
+ color = (0.55, 0.5, 0.7)
+ interaction_style = wrapper.ui.interaction_style
+ button_label_positive = wrapper.ui.button_label_positive
+ button_label_negative = wrapper.ui.button_label_negative
+
+ idcls = bacommon.bs.BasicClientUIComponentTypeID
+ for component in wrapper.ui.components:
+ ctypeid = component.get_type_id()
+ if ctypeid is idcls.TEXT:
+ assert isinstance(
+ component, bacommon.bs.BasicClientUIComponentText
+ )
+ section = _TextSection(
+ sub_width=sub_width,
+ text=component.text,
+ subs=component.subs,
+ color=component.color,
+ scale=component.scale,
+ spacing_top=component.spacing_top,
+ spacing_bottom=component.spacing_bottom,
+ )
+ total_height += section.get_height()
+ sections.append(section)
+
+ elif ctypeid is idcls.UNKNOWN:
+ raise RuntimeError('Should not get here.')
+ else:
+ # Make sure we handle all types.
+ assert_never(ctypeid)
+ else:
+
+ # Display anything with unknown components as an
+ # 'upgrade your app to see this' message.
+ color = (0.6, 0.6, 0.6)
+ interaction_style = (
+ bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN
+ )
+ button_label_positive = bacommon.bs.BasicClientUI.ButtonLabel.OK
+ button_label_negative = (
+ bacommon.bs.BasicClientUI.ButtonLabel.CANCEL
+ )
+
+ section = _TextSection(
+ sub_width=sub_width,
+ text='You must update the app to view this.',
+ subs=[],
+ )
+ total_height += section.get_height()
+ sections.append(section)
+
+ self._entry_displays.append(
+ _EntryDisplay(
+ interaction_style=interaction_style,
+ button_label_positive=button_label_positive,
+ button_label_negative=button_label_negative,
+ id=wrapper.id,
+ sections=sections,
+ total_height=total_height,
color=color,
)
)
- sub_height += entry_height
+ sub_height += total_height
subcontainer = bui.containerwidget(
id='inboxsub',
@@ -446,98 +569,116 @@ class InboxWindow(bui.MainWindow):
backing_tex = bui.gettexture('buttonSquareWide')
+ assert bui.app.classic is not None
+
buttonrows: list[list[bui.Widget]] = []
y = sub_height
- for i, _entry in enumerate(response.entries):
- message_entry = self._message_entries[i]
- message_entry_weak = weakref.ref(message_entry)
+ for i, _wrapper in enumerate(response.wrappers):
+ entry_display = self._entry_displays[i]
+ entry_display_weak = weakref.ref(entry_display)
bwidth = 140
bheight = 40
+ ysection = y - 23.0
+
# Backing.
- message_entry.backing = img = bui.imagewidget(
+ entry_display.backing = img = bui.imagewidget(
parent=subcontainer,
- position=(-0.022 * sub_width, y - message_entry.height * 1.09),
+ position=(
+ -0.022 * sub_width,
+ y - entry_display.total_height * 1.09,
+ ),
texture=backing_tex,
- size=(sub_width * 1.07, message_entry.height * 1.15),
- color=message_entry.color,
+ size=(sub_width * 1.07, entry_display.total_height * 1.15),
+ color=entry_display.color,
opacity=0.9,
)
bui.widget(edit=img, depth_range=(0, 0.1))
+ # Section contents.
+ for sec in entry_display.sections:
+ sec.draw(subcontainer, ysection)
+ ysection -= sec.get_height()
+
buttonrow: list[bui.Widget] = []
have_negative_button = (
- message_entry.type
- is bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD
+ entry_display.interaction_style
+ is (
+ bacommon.bs.BasicClientUI
+ ).InteractionStyle.BUTTON_POSITIVE_NEGATIVE
)
- message_entry.button_positive = btn = bui.buttonwidget(
+ bpos = (
+ (
+ (sub_width - bwidth - 25)
+ if have_negative_button
+ else ((sub_width - bwidth) * 0.5)
+ ),
+ y - entry_display.total_height + 15.0,
+ )
+ entry_display.button_positive = btn = bui.buttonwidget(
parent=subcontainer,
- position=(
- (
- (sub_width - bwidth - 25)
- if have_negative_button
- else ((sub_width - bwidth) * 0.5)
- ),
- y - message_entry.height + 15.0,
- ),
+ position=bpos,
size=(bwidth, bheight),
- label=bui.Lstr(
- resource=(
- 'claimText'
- if message_entry.type
- in {
- bacommon.cloud.BSInboxEntryType.CLAIM,
- bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD,
- }
- else 'okText'
- )
+ label=bui.app.classic.basic_client_ui_button_label_str(
+ entry_display.button_label_positive
),
- color=message_entry.color,
+ color=entry_display.color,
textcolor=(0, 1, 0),
on_activate_call=bui.WeakCall(
- self._on_message_entry_press,
- message_entry_weak,
- bacommon.cloud.BSInboxEntryProcessType.POSITIVE,
+ self._on_entry_display_press,
+ entry_display_weak,
+ bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE,
),
+ enable_sound=False,
)
bui.widget(edit=btn, depth_range=(0.1, 1.0))
buttonrow.append(btn)
+ spinner = entry_display.button_spinner_positive = bui.spinnerwidget(
+ parent=subcontainer,
+ position=(
+ bpos[0] + 0.5 * bwidth,
+ bpos[1] + 0.5 * bheight,
+ ),
+ visible=False,
+ )
+ bui.widget(edit=spinner, depth_range=(0.1, 1.0))
if have_negative_button:
- message_entry.button_negative = btn2 = bui.buttonwidget(
+ bpos = (25, y - entry_display.total_height + 15.0)
+ entry_display.button_negative = btn2 = bui.buttonwidget(
parent=subcontainer,
- position=(25, y - message_entry.height + 15.0),
+ position=bpos,
size=(bwidth, bheight),
- label=bui.Lstr(resource='discardText'),
+ label=bui.app.classic.basic_client_ui_button_label_str(
+ entry_display.button_label_negative
+ ),
color=(0.85, 0.5, 0.7),
textcolor=(1, 0.4, 0.4),
on_activate_call=bui.WeakCall(
- self._on_message_entry_press,
- message_entry_weak,
- bacommon.cloud.BSInboxEntryProcessType.NEGATIVE,
+ self._on_entry_display_press,
+ entry_display_weak,
+ (bacommon.bs.ClientUIAction).BUTTON_PRESS_NEGATIVE,
),
+ enable_sound=False,
)
bui.widget(edit=btn2, depth_range=(0.1, 1.0))
buttonrow.append(btn2)
+ spinner = entry_display.button_spinner_negative = (
+ bui.spinnerwidget(
+ parent=subcontainer,
+ position=(
+ bpos[0] + 0.5 * bwidth,
+ bpos[1] + 0.5 * bheight,
+ ),
+ visible=False,
+ )
+ )
+ bui.widget(edit=spinner, depth_range=(0.1, 1.0))
buttonrows.append(buttonrow)
- message_entry.message_text = bui.textwidget(
- parent=subcontainer,
- position=(
- sub_width * 0.5,
- y - message_entry.text_height * 0.5 - 23.0,
- ),
- scale=message_entry.scale,
- flatness=1.0,
- shadow=0.0,
- text=message_entry.text,
- size=(0, 0),
- h_align='center',
- v_align='center',
- )
- y -= message_entry.height
+ y -= entry_display.total_height
uiscale = bui.app.ui_v1.uiscale
above_widget = (
diff --git a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py
index 0666147f..cfa21aef 100644
--- a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py
+++ b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py
@@ -39,6 +39,7 @@ class LeagueRankWindow(bui.MainWindow):
self._league_text: bui.Widget | None = None
self._league_number_text: bui.Widget | None = None
self._your_power_ranking_text: bui.Widget | None = None
+ self._loading_spinner: bui.Widget | None = None
self._season_ends_text: bui.Widget | None = None
self._power_ranking_rank_text: bui.Widget | None = None
self._to_ranked_text: bui.Widget | None = None
@@ -150,7 +151,7 @@ class LeagueRankWindow(bui.MainWindow):
self._doing_power_ranking_query = False
self._subcontainer: bui.Widget | None = None
- self._subcontainerwidth = 800
+ self._subcontainerwidth = max(800, self._scroll_width)
self._subcontainerheight = 483
self._power_ranking_score_widgets: list[bui.Widget] = []
@@ -330,13 +331,8 @@ class LeagueRankWindow(bui.MainWindow):
bui.textwidget(edit=self._league_title_text, text='')
bui.textwidget(edit=self._league_text, text='')
bui.textwidget(edit=self._league_number_text, text='')
- bui.textwidget(
- edit=self._your_power_ranking_text,
- text=bui.Lstr(
- value='${A}...',
- subs=[('${A}', bui.Lstr(resource='loadingText'))],
- ),
- )
+ bui.textwidget(edit=self._your_power_ranking_text, text='')
+ bui.spinnerwidget(edit=self._loading_spinner, visible=True)
bui.textwidget(edit=self._to_ranked_text, text='')
bui.textwidget(edit=self._power_ranking_rank_text, text='')
bui.textwidget(edit=self._season_ends_text, text='')
@@ -618,6 +614,14 @@ class LeagueRankWindow(bui.MainWindow):
flatness=1.0,
)
+ self._loading_spinner = bui.spinnerwidget(
+ parent=w_parent,
+ position=(
+ self._subcontainerwidth * 0.5,
+ self._subcontainerheight * 0.5,
+ ),
+ size=64,
+ )
self._your_power_ranking_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + 470, v - 142 - 70),
@@ -968,6 +972,7 @@ class LeagueRankWindow(bui.MainWindow):
else ''
),
)
+ bui.spinnerwidget(edit=self._loading_spinner, visible=False)
bui.textwidget(
edit=self._power_ranking_rank_text,
diff --git a/src/assets/ba_data/python/bauiv1lib/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py
index 8495903a..0f6152d5 100644
--- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py
+++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py
@@ -431,7 +431,6 @@ class MainMenuWindow(bui.MainWindow):
)
# Credits button.
- # self._tdelay += self._t_delay_inc
thistdelay = self._tdelay + td5 * self._t_delay_inc
h += side_button_width * side_button_scale * 0.5 + hspace2
@@ -454,15 +453,16 @@ class MainMenuWindow(bui.MainWindow):
transition_delay=thistdelay,
on_activate_call=self._credits,
)
- # self._tdelay += self._t_delay_inc
self._quit_button: bui.Widget | None
if self._have_quit_button:
v -= 1.1 * side_button_2_height * side_button_2_scale
+ # Nudge this a tiny bit right so we can press right from the
+ # credits button to get to it.
self._quit_button = quit_button = bui.buttonwidget(
parent=self._root_widget,
autoselect=self._use_autoselect,
- position=(h, v),
+ position=(h + 4.0, v),
size=(side_button_2_width, side_button_2_height),
scale=side_button_2_scale,
label=bui.Lstr(
diff --git a/src/assets/ba_data/python/bauiv1lib/partyqueue.py b/src/assets/ba_data/python/bauiv1lib/partyqueue.py
index 3c4d10be..dbcfd7ad 100644
--- a/src/assets/ba_data/python/bauiv1lib/partyqueue.py
+++ b/src/assets/ba_data/python/bauiv1lib/partyqueue.py
@@ -579,6 +579,10 @@ class PartyQueueWindow(bui.Window):
if plus.get_v1_account_ticket_count() < self._boost_tickets:
bui.getsound('error').play()
+ bui.screenmessage(
+ bui.Lstr(resource='notEnoughTicketsText'),
+ color=(1, 0, 0),
+ )
# gettickets.show_get_tickets_prompt()
return
diff --git a/src/assets/ba_data/python/bauiv1lib/play.py b/src/assets/ba_data/python/bauiv1lib/play.py
index 27b05a40..961113f3 100644
--- a/src/assets/ba_data/python/bauiv1lib/play.py
+++ b/src/assets/ba_data/python/bauiv1lib/play.py
@@ -42,9 +42,9 @@ class PlayWindow(bui.MainWindow):
self._playlist_select_context = playlist_select_context
uiscale = bui.app.ui_v1.uiscale
- width = 1100 if uiscale is bui.UIScale.SMALL else 800
- x_offs = 150 if uiscale is bui.UIScale.SMALL else 0
- y_offs = -60 if uiscale is bui.UIScale.SMALL else 0
+ width = 1100 if uiscale is bui.UIScale.SMALL else 1000
+ x_offs = 150 if uiscale is bui.UIScale.SMALL else 90
+ y_offs = -60 if uiscale is bui.UIScale.SMALL else 45
height = 650 if uiscale is bui.UIScale.SMALL else 550
button_width = 400
@@ -89,7 +89,7 @@ class PlayWindow(bui.MainWindow):
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
- position=(55 + x_offs, height - 132 + y_offs),
+ position=(5 + x_offs, height - 162 + y_offs),
size=(60, 60),
scale=1.1,
text_res_scale=1.5,
@@ -103,11 +103,12 @@ class PlayWindow(bui.MainWindow):
edit=self._root_widget, cancel_button=self._back_button
)
- txt = bui.textwidget(
+ bui.textwidget(
parent=self._root_widget,
- position=(width * 0.5, height - 101 + y_offs),
- # position=(width * 0.5, height -
- # (101 if main_menu else 61)),
+ position=(
+ width * 0.5,
+ height - (83 if uiscale is bui.UIScale.SMALL else 131) + y_offs,
+ ),
size=(0, 0),
text=bui.Lstr(
resource=(
@@ -116,17 +117,13 @@ class PlayWindow(bui.MainWindow):
else 'playlistsText'
)
),
- scale=1.7,
+ scale=1.2 if uiscale is bui.UIScale.SMALL else 1.7,
res_scale=2.0,
maxwidth=400,
color=bui.app.ui_v1.heading_color,
h_align='center',
v_align='center',
)
-
- if uiscale is bui.UIScale.SMALL:
- bui.textwidget(edit=txt, text='')
-
v = (
height
- (110 if self._playlist_select_context is None else 90)
@@ -134,14 +131,14 @@ class PlayWindow(bui.MainWindow):
)
v -= 100
clr = (0.6, 0.7, 0.6, 1.0)
- v -= 280 if self._playlist_select_context is None else 180
- v += 30 if uiscale is bui.UIScale.SMALL else 0
+ v -= 270 if self._playlist_select_context is None else 280
+ v += 65 if uiscale is bui.UIScale.SMALL else 0
hoffs = (
- x_offs + 80
+ x_offs - 45
if self._playlist_select_context is None
else x_offs - 100
)
- scl = 1.13 if self._playlist_select_context is None else 0.68
+ scl = 0.75 if self._playlist_select_context is None else 0.68
self._lineup_tex = bui.gettexture('playerLineup')
angry_computer_transparent_mesh = bui.getmesh(
@@ -167,16 +164,15 @@ class PlayWindow(bui.MainWindow):
if self._playlist_select_context is None:
self._coop_button = btn = bui.buttonwidget(
parent=self._root_widget,
- position=(hoffs, v + (scl * 15)),
+ position=(hoffs, v),
size=(
scl * button_width,
- scl * 300,
+ scl * 360,
),
extra_touch_border_scale=0.1,
autoselect=True,
label='',
button_type='square',
- text_scale=1.13,
on_activate_call=self._coop,
)
@@ -247,7 +243,7 @@ class PlayWindow(bui.MainWindow):
h_align='center',
v_align='center',
color=(0.7, 0.9, 0.7, 1.0),
- scale=scl * 2.3,
+ scale=scl * 1.5,
)
bui.textwidget(
@@ -264,34 +260,29 @@ class PlayWindow(bui.MainWindow):
color=clr,
)
- scl = 0.5 if self._playlist_select_context is None else 0.68
- hoffs += 440 if self._playlist_select_context is None else 216
- v += 180 if self._playlist_select_context is None else -68
+ scl = 0.75 if self._playlist_select_context is None else 0.68
+ hoffs += 300 if self._playlist_select_context is None else 216
+ # v += 0 if self._playlist_select_context is None else -68
self._teams_button = btn = bui.buttonwidget(
parent=self._root_widget,
position=(
hoffs,
- v + (scl * 15 if self._playlist_select_context is None else 0),
+ v,
+ # v + (scl * 15 if
+ # self._playlist_select_context is None else 0),
),
size=(
scl * button_width,
- scl * (300 if self._playlist_select_context is None else 360),
+ scl * (360 if self._playlist_select_context is None else 360),
),
extra_touch_border_scale=0.1,
autoselect=True,
label='',
button_type='square',
- text_scale=1.13,
on_activate_call=self._team_tourney,
)
- bui.widget(
- edit=btn,
- up_widget=bui.get_special_widget('get_tokens_button'),
- right_widget=bui.get_special_widget('squad_button'),
- )
-
xxx = -14
self._draw_dude(
2,
@@ -381,7 +372,7 @@ class PlayWindow(bui.MainWindow):
h_align='center',
v_align='center',
color=(0.7, 0.9, 0.7, 1.0),
- scale=scl * 2.3,
+ scale=scl * 1.5,
)
bui.textwidget(
parent=self._root_widget,
@@ -392,29 +383,30 @@ class PlayWindow(bui.MainWindow):
h_align='center',
v_align='center',
res_scale=1.5,
- scale=0.9 * scl,
+ scale=0.83 * scl,
flatness=1.0,
maxwidth=scl * button_width * 0.7,
color=clr,
)
- hoffs += 0 if self._playlist_select_context is None else 300
- v -= 155 if self._playlist_select_context is None else 0
+ hoffs += 300 if self._playlist_select_context is None else 300
+ # v -= 0 if self._playlist_select_context is None else 0
self._free_for_all_button = btn = bui.buttonwidget(
parent=self._root_widget,
position=(
hoffs,
- v + (scl * 15 if self._playlist_select_context is None else 0),
+ v,
+ # v + (scl * 15
+ # if self._playlist_select_context is None else 0),
),
size=(
scl * button_width,
- scl * (300 if self._playlist_select_context is None else 360),
+ scl * (360 if self._playlist_select_context is None else 360),
),
extra_touch_border_scale=0.1,
autoselect=True,
label='',
button_type='square',
- text_scale=1.13,
on_activate_call=self._free_for_all,
)
@@ -505,7 +497,7 @@ class PlayWindow(bui.MainWindow):
h_align='center',
v_align='center',
color=(0.7, 0.9, 0.7, 1.0),
- scale=scl * 1.9,
+ scale=scl * 1.5,
)
bui.textwidget(
parent=self._root_widget,
@@ -515,7 +507,7 @@ class PlayWindow(bui.MainWindow):
text=bui.Lstr(resource=f'{self._r}.twoToEightPlayersText'),
h_align='center',
v_align='center',
- scale=0.9 * scl,
+ scale=0.83 * scl,
flatness=1.0,
maxwidth=scl * button_width * 0.7,
color=clr,
diff --git a/src/assets/ba_data/python/bauiv1lib/playoptions.py b/src/assets/ba_data/python/bauiv1lib/playoptions.py
index fc23723e..0bf2028a 100644
--- a/src/assets/ba_data/python/bauiv1lib/playoptions.py
+++ b/src/assets/ba_data/python/bauiv1lib/playoptions.py
@@ -17,6 +17,8 @@ if TYPE_CHECKING:
from bauiv1lib.play import PlaylistSelectContext
+REQUIRE_PRO = False
+
class PlayOptionsWindow(PopupWindow):
"""A popup window for configuring play options."""
@@ -316,7 +318,7 @@ class PlayOptionsWindow(PopupWindow):
label=bui.Lstr(resource='teamNamesColorText'),
)
assert bui.app.classic is not None
- if not bui.app.classic.accounts.have_pro():
+ if REQUIRE_PRO and not bui.app.classic.accounts.have_pro():
bui.imagewidget(
parent=self.root_widget,
size=(30, 30),
@@ -440,7 +442,7 @@ class PlayOptionsWindow(PopupWindow):
assert plus is not None
assert bui.app.classic is not None
- if not bui.app.classic.accounts.have_pro():
+ if REQUIRE_PRO and not bui.app.classic.accounts.have_pro():
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
else:
diff --git a/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py b/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py
index a9139a3c..7fdc3a2a 100644
--- a/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py
+++ b/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py
@@ -205,7 +205,10 @@ class ProfileUpgradeWindow(bui.Window):
tickets = plus.get_v1_account_ticket_count()
if tickets < self._cost:
bui.getsound('error').play()
- print('FIXME - show not-enough-tickets msg.')
+ bui.screenmessage(
+ bui.Lstr(resource='notEnoughTicketsText'),
+ color=(1, 0, 0),
+ )
# gettickets.show_get_tickets_prompt()
return
bui.screenmessage(
diff --git a/src/assets/ba_data/python/bauiv1lib/purchase.py b/src/assets/ba_data/python/bauiv1lib/purchase.py
index 51094091..9d20efeb 100644
--- a/src/assets/ba_data/python/bauiv1lib/purchase.py
+++ b/src/assets/ba_data/python/bauiv1lib/purchase.py
@@ -162,7 +162,6 @@ class PurchaseWindow(bui.Window):
bui.containerwidget(edit=self._root_widget, transition='out_left')
def _purchase(self) -> None:
- # from bauiv1lib import gettickets
plus = bui.app.plus
assert plus is not None
@@ -176,9 +175,12 @@ 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()
- print('FIXME - show not-enough-tickets msg')
bui.getsound('error').play()
+ bui.screenmessage(
+ bui.Lstr(resource='notEnoughTicketsText'),
+ color=(1, 0, 0),
+ )
+ # gettickets.show_get_tickets_prompt()
return
def do_it() -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py b/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py
index 139ff240..b701805f 100644
--- a/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py
+++ b/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py
@@ -53,24 +53,25 @@ class ResourceTypeInfoWindow(PopupWindow):
iconscale=1.2,
)
- yoffs = self._height - 150
+ yoffs = self._height - 145
if resource_type == 'tickets':
rdesc = (
'Tickets can be used to unlock characters,\n'
'maps, minigames, and more in the store.\n'
'\n'
- 'Earn tickets by completing achievements\n'
- 'or by opening chests won in the game.'
+ 'Tickets can be found in chests won through\n'
+ 'campaigns, tournaments, and achievements.'
)
texname = 'tickets'
elif resource_type == 'tokens':
rdesc = (
- 'Tokens have various uses in the game such as\n'
- 'speeding up chest unlocks.\n'
+ 'Tokens are used to speed up chest unlocks\n'
+ 'and for other game and account features.\n'
'\n'
- 'You can buy packs of tokens or you can buy a\n'
- 'Gold Pass to get unlimited tokens.\n'
+ 'You can win tokens in the game or buy them\n'
+ 'in packs. Or buy a Gold Pass to get infinite\n'
+ 'tokens forever and never hear of them again.'
)
texname = 'coin'
elif resource_type == 'trophies':
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py
index badb77e5..37550242 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py
@@ -10,7 +10,7 @@ import logging
import bauiv1 as bui
if TYPE_CHECKING:
- pass
+ from typing import Callable
class AllSettingsWindow(bui.MainWindow):
@@ -21,7 +21,6 @@ class AllSettingsWindow(bui.MainWindow):
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
):
- # pylint: disable=too-many-statements
# pylint: disable=too-many-locals
# Preload some modules we use in a background thread so we won't
@@ -31,12 +30,12 @@ class AllSettingsWindow(bui.MainWindow):
bui.set_analytics_screen('Settings Window')
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
- width = 1000 if uiscale is bui.UIScale.SMALL else 580
+ width = 1000 if uiscale is bui.UIScale.SMALL else 900
x_inset = 125 if uiscale is bui.UIScale.SMALL else 0
- height = 500 if uiscale is bui.UIScale.SMALL else 435
+ height = 500 if uiscale is bui.UIScale.SMALL else 450
self._r = 'settingsWindow'
top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
- yoffs = -30 if uiscale is bui.UIScale.SMALL else 0
+ yoffs = -30 if uiscale is bui.UIScale.SMALL else -30
uiscale = bui.app.ui_v1.uiscale
super().__init__(
@@ -50,10 +49,7 @@ class AllSettingsWindow(bui.MainWindow):
scale=(
1.5
if uiscale is bui.UIScale.SMALL
- else 1.25 if uiscale is bui.UIScale.MEDIUM else 1.0
- ),
- stack_offset=(
- (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0)
+ else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.8
),
),
transition=transition,
@@ -69,12 +65,12 @@ class AllSettingsWindow(bui.MainWindow):
self._back_button = btn = bui.buttonwidget(
parent=self._root_widget,
autoselect=True,
- position=(40 + x_inset, height - 55 + yoffs),
- size=(130, 60),
+ position=(40 + x_inset, height - 60 + yoffs),
+ size=(70, 70),
scale=0.8,
text_scale=1.2,
- label=bui.Lstr(resource='backText'),
- button_type='back',
+ label=bui.charstr(bui.SpecialChar.BACK),
+ button_type='backSmall',
on_activate_call=self.main_window_back,
)
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
@@ -87,131 +83,116 @@ class AllSettingsWindow(bui.MainWindow):
color=bui.app.ui_v1.title_color,
h_align='center',
v_align='center',
+ scale=1.1,
maxwidth=130,
)
- if self._back_button is not None:
- bui.buttonwidget(
- edit=self._back_button,
- button_type='backSmall',
- size=(60, 60),
- label=bui.charstr(bui.SpecialChar.BACK),
+ bwidth = 200
+ bheight = 230
+ margin = 1
+ all_buttons_width = 4.0 * bwidth + 3.0 * margin
+
+ x = width * 0.5 - all_buttons_width * 0.5
+ y = height + yoffs - 320.0
+
+ def _button(
+ position: tuple[float, float],
+ label: bui.Lstr,
+ call: Callable[[], None],
+ texture: bui.Texture,
+ imgsize: float,
+ *,
+ color: tuple[float, float, float] = (1.0, 1.0, 1.0),
+ imgoffs: tuple[float, float] = (0.0, 0.0),
+ ) -> bui.Widget:
+ x, y = position
+ btn = bui.buttonwidget(
+ parent=self._root_widget,
+ autoselect=True,
+ position=(x, y),
+ size=(bwidth, bheight),
+ button_type='square',
+ label='',
+ on_activate_call=call,
)
-
- v = height - 80 + yoffs
- v -= 145
-
- basew = 280 if uiscale is bui.UIScale.SMALL else 230
- baseh = 170
- x_offs = (
- x_inset + (105 if uiscale is bui.UIScale.SMALL else 72) - basew
- ) # now unused
- x_offs2 = x_offs + basew - 7
- x_offs3 = x_offs + 2 * (basew - 7)
- x_offs4 = x_offs2
- x_offs5 = x_offs3
-
- def _b_title(
- x: float, y: float, button: bui.Widget, text: str | bui.Lstr
- ) -> None:
bui.textwidget(
parent=self._root_widget,
- text=text,
- position=(x + basew * 0.47, y + baseh * 0.22),
- maxwidth=basew * 0.7,
+ text=label,
+ position=(x + bwidth * 0.5, y + bheight * 0.25),
+ maxwidth=bwidth * 0.7,
size=(0, 0),
h_align='center',
v_align='center',
- draw_controller=button,
+ draw_controller=btn,
color=(0.7, 0.9, 0.7, 1.0),
)
+ bui.imagewidget(
+ parent=self._root_widget,
+ position=(
+ x + bwidth * 0.5 - imgsize * 0.5 + imgoffs[0],
+ y + bheight * 0.56 - imgsize * 0.5 + imgoffs[1],
+ ),
+ size=(imgsize, imgsize),
+ texture=texture,
+ draw_controller=btn,
+ color=color,
+ )
+ return btn
- ctb = self._controllers_button = bui.buttonwidget(
- parent=self._root_widget,
- autoselect=True,
- position=(x_offs2, v),
- size=(basew, baseh),
- button_type='square',
- label='',
- on_activate_call=self._do_controllers,
- )
- if self._back_button is None:
- bbtn = bui.get_special_widget('back_button')
- bui.widget(edit=ctb, left_widget=bbtn)
- _b_title(
- x_offs2, v, ctb, bui.Lstr(resource=f'{self._r}.controllersText')
- )
- imgw = imgh = 130
- bui.imagewidget(
- parent=self._root_widget,
- position=(x_offs2 + basew * 0.49 - imgw * 0.5, v + 35),
- size=(imgw, imgh),
+ self._controllers_button = _button(
+ position=(x, y),
+ label=bui.Lstr(resource=f'{self._r}.controllersText'),
+ call=self._do_controllers,
texture=bui.gettexture('controllerIcon'),
- draw_controller=ctb,
+ imgsize=150,
+ imgoffs=(-2.0, 2.0),
)
+ x += bwidth + margin
- gfxb = self._graphics_button = bui.buttonwidget(
- parent=self._root_widget,
- autoselect=True,
- position=(x_offs3, v),
- size=(basew, baseh),
- button_type='square',
- label='',
- on_activate_call=self._do_graphics,
- )
- pbtn = bui.get_special_widget('squad_button')
- bui.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn)
- _b_title(x_offs3, v, gfxb, bui.Lstr(resource=f'{self._r}.graphicsText'))
- imgw = imgh = 110
- bui.imagewidget(
- parent=self._root_widget,
- position=(x_offs3 + basew * 0.49 - imgw * 0.5, v + 42),
- size=(imgw, imgh),
+ self._graphics_button = _button(
+ position=(x, y),
+ label=bui.Lstr(resource=f'{self._r}.graphicsText'),
+ call=self._do_graphics,
texture=bui.gettexture('graphicsIcon'),
- draw_controller=gfxb,
+ imgsize=135,
+ imgoffs=(0, 4.0),
)
+ x += bwidth + margin
- v -= baseh - 5
-
- abtn = self._audio_button = bui.buttonwidget(
- parent=self._root_widget,
- autoselect=True,
- position=(x_offs4, v),
- size=(basew, baseh),
- button_type='square',
- label='',
- on_activate_call=self._do_audio,
- )
- _b_title(x_offs4, v, abtn, bui.Lstr(resource=f'{self._r}.audioText'))
- imgw = imgh = 120
- bui.imagewidget(
- parent=self._root_widget,
- position=(x_offs4 + basew * 0.49 - imgw * 0.5 + 5, v + 35),
- size=(imgw, imgh),
- color=(1, 1, 0),
+ self._audio_button = _button(
+ position=(x, y),
+ label=bui.Lstr(resource=f'{self._r}.audioText'),
+ call=self._do_audio,
texture=bui.gettexture('audioIcon'),
- draw_controller=abtn,
+ imgsize=150,
+ color=(1, 1, 0),
+ )
+ x += bwidth + margin
+
+ self._advanced_button = _button(
+ position=(x, y),
+ label=bui.Lstr(resource=f'{self._r}.advancedText'),
+ call=self._do_advanced,
+ texture=bui.gettexture('advancedIcon'),
+ imgsize=150,
+ color=(0.8, 0.95, 1),
+ imgoffs=(0, 5.0),
)
- avb = self._advanced_button = bui.buttonwidget(
- parent=self._root_widget,
- autoselect=True,
- position=(x_offs5, v),
- size=(basew, baseh),
- button_type='square',
- label='',
- on_activate_call=self._do_advanced,
- )
- _b_title(x_offs5, v, avb, bui.Lstr(resource=f'{self._r}.advancedText'))
- imgw = imgh = 120
- bui.imagewidget(
- parent=self._root_widget,
- position=(x_offs5 + basew * 0.49 - imgw * 0.5 + 5, v + 35),
- size=(imgw, imgh),
- color=(0.8, 0.95, 1),
- texture=bui.gettexture('advancedIcon'),
- draw_controller=avb,
- )
+ # Hmm; we're now wide enough that being limited to pressing up
+ # might be ok.
+ if bool(False):
+ # Left from our leftmost button should go to back button.
+ if self._back_button is None:
+ bbtn = bui.get_special_widget('back_button')
+ bui.widget(edit=self._controllers_button, left_widget=bbtn)
+
+ # Right from our rightmost widget should go to squad button.
+ bui.widget(
+ edit=self._advanced_button,
+ right_widget=bui.get_special_widget('squad_button'),
+ )
+
self._restore_state()
@override
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/audio.py b/src/assets/ba_data/python/bauiv1lib/settings/audio.py
index 323bb270..1a7251e4 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/audio.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/audio.py
@@ -34,7 +34,10 @@ class AudioSettingsWindow(bui.MainWindow):
spacing = 50.0
width = 460.0
- height = 210.0
+ height = 240.0
+ uiscale = bui.app.ui_v1.uiscale
+
+ yoffs = -5.0
# Update: hard-coding head-relative audio to true now,
# so not showing options.
@@ -49,11 +52,10 @@ class AudioSettingsWindow(bui.MainWindow):
show_soundtracks = True
height += spacing * 2.0
- uiscale = bui.app.ui_v1.uiscale
base_scale = (
- 2.05
+ 1.9
if uiscale is bui.UIScale.SMALL
- else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0
+ else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
)
popup_menu_scale = base_scale * 1.2
@@ -61,9 +63,6 @@ class AudioSettingsWindow(bui.MainWindow):
root_widget=bui.containerwidget(
size=(width, height),
scale=base_scale,
- stack_offset=(
- (0, -20) if uiscale is bui.UIScale.SMALL else (0, 0)
- ),
toolbar_visibility=(
None if uiscale is bui.UIScale.SMALL else 'menu_full'
),
@@ -74,21 +73,20 @@ class AudioSettingsWindow(bui.MainWindow):
self._back_button = back_button = btn = bui.buttonwidget(
parent=self._root_widget,
- position=(35, height - 55),
- size=(120, 60),
+ position=(35, height + yoffs - 55),
+ size=(60, 60),
scale=0.8,
text_scale=1.2,
- label=bui.Lstr(resource='backText'),
- button_type='back',
+ label=bui.charstr(bui.SpecialChar.BACK),
+ button_type='backSmall',
on_activate_call=self.main_window_back,
autoselect=True,
)
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
- v = height - 60
- v -= spacing * 1.0
+
bui.textwidget(
parent=self._root_widget,
- position=(width * 0.5, height - 32),
+ position=(width * 0.5, height + yoffs - 32),
size=(0, 0),
text=bui.Lstr(resource=f'{self._r}.titleText'),
color=bui.app.ui_v1.title_color,
@@ -97,12 +95,8 @@ class AudioSettingsWindow(bui.MainWindow):
v_align='center',
)
- bui.buttonwidget(
- edit=self._back_button,
- button_type='backSmall',
- size=(60, 60),
- label=bui.charstr(bui.SpecialChar.BACK),
- )
+ v = height + yoffs - 60
+ v -= spacing * 1.0
self._sound_volume_numedit = svne = ConfigNumberEdit(
parent=self._root_widget,
diff --git a/src/assets/ba_data/python/bauiv1lib/store/browser.py b/src/assets/ba_data/python/bauiv1lib/store/browser.py
index 575e61e7..ee41bfcf 100644
--- a/src/assets/ba_data/python/bauiv1lib/store/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/store/browser.py
@@ -141,11 +141,11 @@ class StoreBrowserWindow(bui.MainWindow):
parent=self._root_widget,
position=(
self._width * 0.5,
- self._height - (53 if uiscale is bui.UIScale.SMALL else 44),
+ self._height - (55 if uiscale is bui.UIScale.SMALL else 44),
),
size=(0, 0),
color=app.ui_v1.title_color,
- scale=1.5,
+ scale=1.1 if uiscale is bui.UIScale.SMALL else 1.5,
h_align='center',
v_align='center',
text=bui.Lstr(resource='storeText'),
@@ -536,7 +536,10 @@ class StoreBrowserWindow(bui.MainWindow):
our_tickets = plus.get_v1_account_ticket_count()
if price is not None and our_tickets < price:
bui.getsound('error').play()
- print('FIXME - show not-enough-tickets info.')
+ bui.screenmessage(
+ bui.Lstr(resource='notEnoughTicketsText'),
+ color=(1, 0, 0),
+ )
# gettickets.show_get_tickets_prompt()
else:
diff --git a/src/assets/ba_data/python/bauiv1lib/tournamententry.py b/src/assets/ba_data/python/bauiv1lib/tournamententry.py
index ba34cb2e..b8bfa9c1 100644
--- a/src/assets/ba_data/python/bauiv1lib/tournamententry.py
+++ b/src/assets/ba_data/python/bauiv1lib/tournamententry.py
@@ -33,6 +33,8 @@ class TournamentEntryWindow(PopupWindow):
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
+ from bauiv1lib.coop.tournamentbutton import USE_ENTRY_FEES
+
assert bui.app.classic is not None
assert bui.app.plus
bui.set_analytics_screen('Tournament Entry Window')
@@ -42,9 +44,15 @@ class TournamentEntryWindow(PopupWindow):
self._tournament_id
]
+ self._purchase_name: str | None
+ self._purchase_price_name: str | None
+
# Set a few vars depending on the tourney fee.
self._fee = self._tournament_info['fee']
- self._allow_ads = self._tournament_info['allowAds']
+ assert isinstance(self._fee, int | None)
+ self._allow_ads = (
+ self._tournament_info['allowAds'] if USE_ENTRY_FEES else False
+ )
if self._fee == 4:
self._purchase_name = 'tournament_entry_4'
self._purchase_price_name = 'price.tournament_entry_4'
@@ -57,6 +65,9 @@ class TournamentEntryWindow(PopupWindow):
elif self._fee == 1:
self._purchase_name = 'tournament_entry_1'
self._purchase_price_name = 'price.tournament_entry_1'
+ elif self._fee is None or self._fee == -1:
+ self._purchase_name = None
+ self._purchase_price_name = 'FREE-WOOT'
else:
if self._fee != 0:
raise ValueError('invalid fee: ' + str(self._fee))
@@ -218,7 +229,7 @@ class TournamentEntryWindow(PopupWindow):
h_align='center',
v_align='center',
scale=0.6,
- # Note: AdMob now requires rewarded ad usage
+ # Note to self: AdMob requires rewarded ad usage
# specifically says 'Ad' in it.
text=bui.Lstr(resource='watchAnAdText'),
maxwidth=95,
@@ -439,29 +450,52 @@ class TournamentEntryWindow(PopupWindow):
)
# Keep price up-to-date and update the button with it.
- self._purchase_price = plus.get_v1_account_misc_read_val(
- self._purchase_price_name, None
- )
+ if self._purchase_price_name is not None:
+ self._purchase_price = (
+ 0
+ if self._purchase_price_name == 'FREE-WOOT'
+ else plus.get_v1_account_misc_read_val(
+ self._purchase_price_name, None
+ )
+ )
+ # HACK - this is always free now, so just have this say 'PLAY'
bui.textwidget(
edit=self._ticket_cost_text,
text=(
- bui.Lstr(resource='getTicketsWindow.freeText')
- if self._purchase_price == 0
- else bui.Lstr(
- resource='getTicketsWindow.ticketsText',
- subs=[
- (
- '${COUNT}',
- (
- str(self._purchase_price)
- if self._purchase_price is not None
- else '?'
- ),
- )
- ],
- )
+ bui.Lstr(resource='playText')
+ # if self._purchase_price == 0
+ # else bui.Lstr(
+ # resource='getTicketsWindow.ticketsText',
+ # subs=[
+ # (
+ # '${COUNT}',
+ # (
+ # str(self._purchase_price)
+ # if self._purchase_price is not None
+ # else '?'
+ # ),
+ # )
+ # ],
+ # )
),
+ # text=(
+ # bui.Lstr(resource='getTicketsWindow.freeText')
+ # if self._purchase_price == 0
+ # else bui.Lstr(
+ # resource='getTicketsWindow.ticketsText',
+ # subs=[
+ # (
+ # '${COUNT}',
+ # (
+ # str(self._purchase_price)
+ # if self._purchase_price is not None
+ # else '?'
+ # ),
+ # )
+ # ],
+ # )
+ # ),
position=(
self._ticket_cost_text_position_free
if self._purchase_price == 0
@@ -472,19 +506,20 @@ class TournamentEntryWindow(PopupWindow):
bui.textwidget(
edit=self._free_plays_remaining_text,
- text=(
- ''
- if (
- self._tournament_info['freeTriesRemaining'] in [None, 0]
- or self._purchase_price != 0
- )
- else '' + str(self._tournament_info['freeTriesRemaining'])
- ),
+ # text=(
+ # ''
+ # if (
+ # self._tournament_info['freeTriesRemaining'] in [None, 0]
+ # or self._purchase_price != 0
+ # )
+ # else '' + str(self._tournament_info['freeTriesRemaining'])
+ # ),
+ text='', # No longer relevant.
)
bui.imagewidget(
edit=self._ticket_img,
- opacity=0.2 if self._purchase_price == 0 else 1.0,
+ opacity=0.0 if self._purchase_price == 0 else 1.0,
position=(
self._ticket_img_pos_free
if self._purchase_price == 0
@@ -547,15 +582,16 @@ class TournamentEntryWindow(PopupWindow):
self._launched = True
launched = False
- # If they gave us an existing, non-consistent
- # practice activity, just restart it.
+ # If they gave us an existing, non-consistent practice activity,
+ # just restart it.
if (
self._tournament_activity is not None
and not practice == self._tournament_activity.session.submit_score
):
try:
if not practice:
- bui.apptimer(0.1, bui.getsound('cashRegister').play)
+ bui.apptimer(0.1, bui.getsound('drumRollShort').play)
+ # bui.apptimer(0.1, bui.getsound('cashRegister').play)
bui.screenmessage(
bui.Lstr(
translate=(
@@ -584,7 +620,8 @@ class TournamentEntryWindow(PopupWindow):
# launch a new session.
if not launched:
if not practice:
- bui.apptimer(0.1, bui.getsound('cashRegister').play)
+ bui.apptimer(0.1, bui.getsound('drumRollShort').play)
+ # bui.apptimer(0.1, bui.getsound('cashRegister').play)
bui.screenmessage(
bui.Lstr(
translate=('serverResponses', 'Entering tournament...')
@@ -653,16 +690,21 @@ 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()
- print('FIXME - show not-enough-tickets msg.')
bui.getsound('error').play()
+ bui.screenmessage(
+ bui.Lstr(resource='notEnoughTicketsText'),
+ color=(1, 0, 0),
+ )
+ # gettickets.show_get_tickets_prompt()
self._transition_out()
return
cur_time = bui.apptime()
self._last_ticket_press_time = cur_time
- assert isinstance(ticket_cost, int)
- plus.in_game_purchase(self._purchase_name, ticket_cost)
+
+ if self._purchase_name is not None:
+ assert isinstance(ticket_cost, int)
+ plus.in_game_purchase(self._purchase_name, ticket_cost)
self._entering = True
plus.add_v1_account_transaction(
@@ -759,30 +801,20 @@ class TournamentEntryWindow(PopupWindow):
plus.run_v1_account_transactions()
self._launch()
- # def _on_get_tickets_press(self) -> None:
- # from bauiv1lib import gettickets
-
- # # If we're already entering, ignore presses.
- # if self._entering:
- # return
-
- # # Bring up get-tickets window and then kill ourself (we're on the
- # # overlay layer so we'd show up above it).
- # gettickets.GetTicketsWindow(
- # modal=True, origin_widget=self._get_tickets_button
- # )
- # self._transition_out()
-
def _on_cancel(self) -> None:
plus = bui.app.plus
assert plus is not None
# Don't allow canceling for several seconds after poking an enter
# button if it looks like we're waiting on a purchase or entering
# the tournament.
- if (bui.apptime() - self._last_ticket_press_time < 6.0) and (
- plus.have_outstanding_v1_account_transactions()
- or plus.get_v1_account_product_purchased(self._purchase_name)
- or self._entering
+ if (
+ (bui.apptime() - self._last_ticket_press_time < 6.0)
+ and self._purchase_name is not None
+ and (
+ plus.have_outstanding_v1_account_transactions()
+ or plus.get_v1_account_product_purchased(self._purchase_name)
+ or self._entering
+ )
):
bui.getsound('error').play()
return
diff --git a/src/ballistica/base/app_adapter/app_adapter_apple.cc b/src/ballistica/base/app_adapter/app_adapter_apple.cc
index 2fd6beee..6664642f 100644
--- a/src/ballistica/base/app_adapter/app_adapter_apple.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_apple.cc
@@ -138,7 +138,7 @@ auto AppAdapterApple::TryRender() -> bool {
// Keep on drawing until the drawn window size
// matches what we have (or until we try for too long or fail at drawing).
- seconds_t start_time = g_core->GetAppTimeSeconds();
+ seconds_t start_time = g_core->AppTimeSeconds();
for (int i = 0; i < 5; ++i) {
bool size_differs =
((std::abs(resize_target_resolution_.x
@@ -147,7 +147,7 @@ auto AppAdapterApple::TryRender() -> bool {
|| (std::abs(resize_target_resolution_.y
- g_base->graphics_server->screen_pixel_height())
> 0.01f));
- if (size_differs && g_core->GetAppTimeSeconds() - start_time < 0.1
+ if (size_differs && g_core->AppTimeSeconds() - start_time < 0.1
&& result) {
result = g_base->graphics_server->TryRender();
}
diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.cc b/src/ballistica/base/app_adapter/app_adapter_sdl.cc
index beec9de7..31fe570d 100644
--- a/src/ballistica/base/app_adapter/app_adapter_sdl.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_sdl.cc
@@ -210,7 +210,7 @@ void AppAdapterSDL::RunMainThreadEventLoopToCompletion() {
assert(g_core->InMainThread());
while (!done_) {
- microsecs_t cycle_start_time = g_core->GetAppTimeMicrosecs();
+ microsecs_t cycle_start_time = g_core->AppTimeMicrosecs();
// Events.
SDL_Event event;
@@ -274,7 +274,7 @@ void AppAdapterSDL::SleepUntilNextEventCycle_(microsecs_t cycle_start_time) {
// Normally we just calc when our next draw should happen and sleep 'til
// then.
- microsecs_t now = g_core->GetAppTimeMicrosecs();
+ microsecs_t now = g_core->AppTimeMicrosecs();
auto used_max_fps = max_fps_;
millisecs_t millisecs_per_frame = 1000000 / used_max_fps;
@@ -319,7 +319,7 @@ void AppAdapterSDL::SleepUntilNextEventCycle_(microsecs_t cycle_start_time) {
// Maintain an 'oversleep' amount to compensate for the timer not being
// exact. This should keep us exactly at our target frame-rate in the
// end.
- now = g_core->GetAppTimeMicrosecs();
+ now = g_core->AppTimeMicrosecs();
oversleep_ = now - target_time;
// Prevent oversleep from compensating by more than a few millisecs per
@@ -438,7 +438,7 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
break;
case SDL_QUIT:
- if (g_core->GetAppTimeSeconds() - last_windowevent_close_time_ < 0.1) {
+ if (g_core->AppTimeSeconds() - last_windowevent_close_time_ < 0.1) {
// If they hit the window close button, skip the confirm.
g_base->QuitApp(false);
} else {
@@ -459,7 +459,7 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
case SDL_WINDOWEVENT_CLOSE: {
// Simply note that this happened. We use this to adjust our
// SDL_QUIT behavior (quit is called right after this).
- last_windowevent_close_time_ = g_core->GetAppTimeSeconds();
+ last_windowevent_close_time_ = g_core->AppTimeSeconds();
break;
}
diff --git a/src/ballistica/base/assets/asset.cc b/src/ballistica/base/assets/asset.cc
index 2cb9df48..b03b6868 100644
--- a/src/ballistica/base/assets/asset.cc
+++ b/src/ballistica/base/assets/asset.cc
@@ -11,7 +11,7 @@ namespace ballistica::base {
Asset::Asset() {
assert(g_base);
assert(g_base->InLogicThread());
- last_used_time_ = g_core->GetAppTimeMillisecs();
+ last_used_time_ = g_core->AppTimeMillisecs();
}
auto Asset::AssetTypeName(AssetType assettype) -> const char* {
@@ -65,9 +65,9 @@ void Asset::Preload(bool already_locked) {
return std::string("preloading ") + AssetTypeName(GetAssetType()) + " "
+ GetName();
});
- preload_start_time_ = g_core->GetAppTimeMillisecs();
+ preload_start_time_ = g_core->AppTimeMillisecs();
DoPreload();
- preload_end_time_ = g_core->GetAppTimeMillisecs();
+ preload_end_time_ = g_core->AppTimeMillisecs();
preloaded_ = true;
}
}
@@ -87,9 +87,9 @@ void Asset::Load(bool already_locked) {
return std::string("loading ") + AssetTypeName(GetAssetType()) + " "
+ GetName();
});
- load_start_time_ = g_core->GetAppTimeMillisecs();
+ load_start_time_ = g_core->AppTimeMillisecs();
DoLoad();
- load_end_time_ = g_core->GetAppTimeMillisecs();
+ load_end_time_ = g_core->AppTimeMillisecs();
BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(50, GetName());
loaded_ = true;
}
diff --git a/src/ballistica/base/assets/assets.cc b/src/ballistica/base/assets/assets.cc
index a05f24d4..14bc31c9 100644
--- a/src/ballistica/base/assets/assets.cc
+++ b/src/ballistica/base/assets/assets.cc
@@ -165,6 +165,7 @@ void Assets::StartLoading() {
LoadSystemTexture(SysTextureID::kCharacterIconMask, "characterIconMask");
LoadSystemTexture(SysTextureID::kBlack, "black");
LoadSystemTexture(SysTextureID::kWings, "wings");
+ LoadSystemTexture(SysTextureID::kSpinner, "spinner");
// System cube map textures:
LoadSystemCubeMapTexture(SysCubeMapTextureID::kReflectionChar,
@@ -479,7 +480,7 @@ auto Assets::GetAsset(const std::string& file_name,
have_pending_loads_[static_cast(d->GetAssetType())] = true;
MarkAssetForLoad(d.get());
}
- d->set_last_used_time(g_core->GetAppTimeMillisecs());
+ d->set_last_used_time(g_core->AppTimeMillisecs());
return Object::Ref(d);
}
}
@@ -499,7 +500,7 @@ auto Assets::GetTexture(TextPacker* packer) -> Object::Ref {
have_pending_loads_[static_cast(d->GetAssetType())] = true;
MarkAssetForLoad(d.get());
}
- d->set_last_used_time(g_core->GetAppTimeMillisecs());
+ d->set_last_used_time(g_core->AppTimeMillisecs());
return Object::Ref(d);
}
}
@@ -519,7 +520,7 @@ auto Assets::GetQRCodeTexture(const std::string& url)
have_pending_loads_[static_cast(d->GetAssetType())] = true;
MarkAssetForLoad(d.get());
}
- d->set_last_used_time(g_core->GetAppTimeMillisecs());
+ d->set_last_used_time(g_core->AppTimeMillisecs());
return Object::Ref(d);
}
}
@@ -542,7 +543,7 @@ auto Assets::GetCubeMapTexture(const std::string& file_name)
have_pending_loads_[static_cast(d->GetAssetType())] = true;
MarkAssetForLoad(d.get());
}
- d->set_last_used_time(g_core->GetAppTimeMillisecs());
+ d->set_last_used_time(g_core->AppTimeMillisecs());
return Object::Ref(d);
}
}
@@ -598,7 +599,7 @@ auto Assets::GetTexture(const std::string& file_name)
have_pending_loads_[static_cast(d->GetAssetType())] = true;
MarkAssetForLoad(d.get());
}
- d->set_last_used_time(g_core->GetAppTimeMillisecs());
+ d->set_last_used_time(g_core->AppTimeMillisecs());
return Object::Ref(d);
}
}
@@ -750,7 +751,7 @@ auto Assets::RunPendingLoadsLogicThread() -> bool {
template
auto Assets::RunPendingLoadList(std::vector*>* c_list) -> bool {
bool flush = false;
- millisecs_t starttime = g_core->GetAppTimeMillisecs();
+ millisecs_t starttime = g_core->AppTimeMillisecs();
std::vector*> l;
std::vector*> l_unfinished;
@@ -760,8 +761,7 @@ auto Assets::RunPendingLoadList(std::vector*>* c_list) -> bool {
// If we're already out of time.
if (!flush
- && g_core->GetAppTimeMillisecs() - starttime
- > PENDING_LOAD_PROCESS_TIME) {
+ && g_core->AppTimeMillisecs() - starttime > PENDING_LOAD_PROCESS_TIME) {
bool return_val = (!c_list->empty());
return return_val;
}
@@ -790,8 +790,7 @@ auto Assets::RunPendingLoadList(std::vector*>* c_list) -> bool {
// If the load finished, pop it on our "done-loading" list.. otherwise
// keep it around.
l_finished.push_back(*i); // else l_unfinished.push_back(*i);
- if (g_core->GetAppTimeMillisecs() - starttime
- > PENDING_LOAD_PROCESS_TIME
+ if (g_core->AppTimeMillisecs() - starttime > PENDING_LOAD_PROCESS_TIME
&& !flush) {
out_of_time = true;
}
@@ -832,7 +831,7 @@ auto Assets::RunPendingLoadList(std::vector*>* c_list) -> bool {
void Assets::Prune(int level) {
assert(g_base->InLogicThread());
- millisecs_t current_time = g_core->GetAppTimeMillisecs();
+ millisecs_t current_time = g_core->AppTimeMillisecs();
// Need lists locked while accessing/modifying them.
AssetListLock lock;
@@ -1168,11 +1167,11 @@ auto Assets::FindAssetFile(FileType type, const std::string& name)
// We wanna fail gracefully for some types.
if (type == FileType::kSound && name != "blank") {
g_core->Log(LogName::kBaAssets, LogLevel::kError,
- "Unable to load audio: '" + name + "'; trying fallback...");
+ "Unable to load audio: '" + name + "'.");
return FindAssetFile(type, "blank");
} else if (type == FileType::kTexture && name != "white") {
g_core->Log(LogName::kBaAssets, LogLevel::kError,
- "Unable to load texture: '" + name + "'; trying fallback...");
+ "Unable to load texture: '" + name + "'.");
return FindAssetFile(type, "white");
}
@@ -1560,8 +1559,8 @@ auto DoCompileResourceString(cJSON* obj) -> std::string {
return result;
}
-auto Assets::CompileResourceString(const std::string& s, const std::string& loc,
- bool* valid) -> std::string {
+auto Assets::CompileResourceString(const std::string& s, bool* valid)
+ -> std::string {
bool dummyvalid;
if (valid == nullptr) {
valid = &dummyvalid;
@@ -1577,8 +1576,7 @@ auto Assets::CompileResourceString(const std::string& s, const std::string& loc,
cJSON* root = cJSON_Parse(s.c_str());
if (root == nullptr) {
g_core->Log(LogName::kBaAssets, LogLevel::kError,
- "CompileResourceString failed (loc " + loc
- + "); invalid json: '" + s + "'");
+ "CompileResourceString failed; invalid json: '" + s + "'");
*valid = false;
return "";
}
@@ -1588,8 +1586,8 @@ auto Assets::CompileResourceString(const std::string& s, const std::string& loc,
*valid = true;
} catch (const std::exception& e) {
g_core->Log(LogName::kBaAssets, LogLevel::kError,
- "CompileResourceString failed (loc " + loc
- + "): " + std::string(e.what()) + "; str='" + s + "'");
+ "CompileResourceString failed: " + std::string(e.what())
+ + "; str='" + s + "'");
result = "";
*valid = false;
}
diff --git a/src/ballistica/base/assets/assets.h b/src/ballistica/base/assets/assets.h
index aa396371..53744459 100644
--- a/src/ballistica/base/assets/assets.h
+++ b/src/ballistica/base/assets/assets.h
@@ -111,8 +111,8 @@ class Assets {
const std::unordered_map& language);
auto GetResourceString(const std::string& key) -> std::string;
auto CharStr(SpecialChar id) -> std::string;
- auto CompileResourceString(const std::string& s, const std::string& loc,
- bool* valid = nullptr) -> std::string;
+ auto CompileResourceString(const std::string& s, bool* valid = nullptr)
+ -> std::string;
auto sys_assets_loaded() const { return sys_assets_loaded_; }
diff --git a/src/ballistica/base/assets/sound_asset.cc b/src/ballistica/base/assets/sound_asset.cc
index 808b92e5..008980b9 100644
--- a/src/ballistica/base/assets/sound_asset.cc
+++ b/src/ballistica/base/assets/sound_asset.cc
@@ -324,7 +324,7 @@ void SoundAsset::DoUnload() {
}
void SoundAsset::UpdatePlayTime() {
- last_play_time_ = g_core->GetAppTimeMillisecs();
+ last_play_time_ = g_core->AppTimeMillisecs();
}
} // namespace ballistica::base
diff --git a/src/ballistica/base/audio/audio.cc b/src/ballistica/base/audio/audio.cc
index 7c7e5716..ff5e9a2a 100644
--- a/src/ballistica/base/audio/audio.cc
+++ b/src/ballistica/base/audio/audio.cc
@@ -156,7 +156,7 @@ auto Audio::SourceBeginExisting(uint32_t play_id, int debug_id)
}
auto Audio::ShouldPlay(SoundAsset* sound) -> bool {
- millisecs_t time = g_core->GetAppTimeMillisecs();
+ millisecs_t time = g_core->AppTimeMillisecs();
assert(sound);
return (time - sound->last_play_time() > 50);
}
diff --git a/src/ballistica/base/audio/audio_server.cc b/src/ballistica/base/audio/audio_server.cc
index 6ac888cd..ec729aa4 100644
--- a/src/ballistica/base/audio/audio_server.cc
+++ b/src/ballistica/base/audio/audio_server.cc
@@ -212,9 +212,8 @@ void AudioServer::OpenALSoftLogCallback(const std::string& msg) {
std::scoped_lock lock(openalsoft_android_log_mutex_);
if (openalsoft_android_log_.size() < log_cap) {
- openalsoft_android_log_ += "openal-log("
- + std::to_string(g_core->GetAppTimeSeconds())
- + "s): " + msg;
+ openalsoft_android_log_ +=
+ "openal-log(" + std::to_string(g_core->AppTimeSeconds()) + "s): " + msg;
if (openalsoft_android_log_.size() >= log_cap) {
openalsoft_android_log_ +=
"\n\n";
@@ -477,7 +476,7 @@ void AudioServer::OnAppStartInThread_() {
// Now make available any stopped sources (should be all of them).
UpdateAvailableSources_();
- last_started_playing_time_ = g_core->GetAppTimeSeconds();
+ last_started_playing_time_ = g_core->AppTimeSeconds();
#endif // BA_ENABLE_AUDIO
}
@@ -487,7 +486,7 @@ void AudioServer::Shutdown() {
return;
}
shutting_down_ = true;
- shutdown_start_time_ = g_core->GetAppTimeSeconds();
+ shutdown_start_time_ = g_core->AppTimeSeconds();
// Stop all playing sounds and note the time. We'll then give everything a
// moment to come to a halt before we tear down the audio context to
@@ -538,8 +537,8 @@ struct AudioServer::SoundFadeNode_ {
bool out;
SoundFadeNode_(uint32_t play_id_in, millisecs_t duration_in, bool out_in)
: play_id(play_id_in),
- starttime(g_core->GetAppTimeMillisecs()),
- endtime(g_core->GetAppTimeMillisecs() + duration_in),
+ starttime(g_core->AppTimeMillisecs()),
+ endtime(g_core->AppTimeMillisecs() + duration_in),
out(out_in) {}
};
@@ -566,16 +565,16 @@ void AudioServer::SetSuspended_(bool suspend) {
try {
g_core->platform->LowLevelDebugLog(
"Calling alcDevicePauseSOFT at "
- + std::to_string(g_core->GetAppTimeSeconds()));
+ + std::to_string(g_core->AppTimeSeconds()));
alcDevicePauseSOFT(device);
} catch (const std::exception& e) {
- g_core->Log(LogName::kBaAudio, LogLevel::kError,
- "Error in alcDevicePauseSOFT at time "
- + std::to_string(g_core->GetAppTimeSeconds())
- + "( playing since "
- + std::to_string(last_started_playing_time_) + "): "
- + g_core->platform->DemangleCXXSymbol(typeid(e).name())
- + " " + e.what());
+ g_core->Log(
+ LogName::kBaAudio, LogLevel::kError,
+ "Error in alcDevicePauseSOFT at time "
+ + std::to_string(g_core->AppTimeSeconds()) + "( playing since "
+ + std::to_string(last_started_playing_time_)
+ + "): " + g_core->platform->DemangleCXXSymbol(typeid(e).name())
+ + " " + e.what());
} catch (...) {
g_core->Log(LogName::kBaAudio, LogLevel::kError,
"Unknown error in alcDevicePauseSOFT");
@@ -609,12 +608,12 @@ void AudioServer::SetSuspended_(bool suspend) {
try {
g_core->platform->LowLevelDebugLog(
"Calling alcDeviceResumeSOFT at "
- + std::to_string(g_core->GetAppTimeSeconds()));
+ + std::to_string(g_core->AppTimeSeconds()));
alcDeviceResumeSOFT(device);
} catch (const std::exception& e) {
g_core->Log(LogName::kBaAudio, LogLevel::kError,
"Error in alcDeviceResumeSOFT at time "
- + std::to_string(g_core->GetAppTimeSeconds()) + ": "
+ + std::to_string(g_core->AppTimeSeconds()) + ": "
+ g_core->platform->DemangleCXXSymbol(typeid(e).name())
+ " " + e.what());
} catch (...) {
@@ -622,7 +621,7 @@ void AudioServer::SetSuspended_(bool suspend) {
"Unknown error in alcDeviceResumeSOFT");
}
#endif
- last_started_playing_time_ = g_core->GetAppTimeSeconds();
+ last_started_playing_time_ = g_core->AppTimeSeconds();
suspended_ = false;
#if BA_ENABLE_AUDIO
CHECK_AL_ERROR;
@@ -774,7 +773,7 @@ void AudioServer::UpdateAvailableSources_() {
// and see how many are in use, how many are currently locked by the client,
// etc.
#if (BA_DEBUG_BUILD || BA_TEST_BUILD)
- millisecs_t t = g_core->GetAppTimeMillisecs();
+ millisecs_t t = g_core->AppTimeMillisecs();
if (t - last_sanity_check_time_ > 5000) {
last_sanity_check_time_ = t;
@@ -1033,7 +1032,7 @@ void AudioServer::OnDeviceDisconnected() {
void AudioServer::Process_() {
assert(g_base->InAudioThread());
- seconds_t real_time_seconds = g_core->GetAppTimeSeconds();
+ seconds_t real_time_seconds = g_core->AppTimeSeconds();
millisecs_t real_time_millisecs = real_time_seconds * 1000;
// Only do real work if we're in normal running mode.
@@ -1085,7 +1084,7 @@ void AudioServer::Process_() {
// for the mixer to spit out some silence so we don't hear sudden cut-offs
// in one or both ears.
if (shutting_down_ && !shutdown_completed_) {
- if (g_core->GetAppTimeSeconds() - shutdown_start_time_ > 0.2) {
+ if (g_core->AppTimeSeconds() - shutdown_start_time_ > 0.2) {
CompleteShutdown_();
}
}
@@ -1125,13 +1124,13 @@ void AudioServer::ProcessSoundFades_() {
AudioServer::ThreadSource_* s = GetPlayingSound_(i->second.play_id);
if (s) {
- if (g_core->GetAppTimeMillisecs() > i->second.endtime) {
+ if (g_core->AppTimeMillisecs() > i->second.endtime) {
StopSound(i->second.play_id);
sound_fade_nodes_.erase(i);
} else {
float fade_val =
1
- - (static_cast(g_core->GetAppTimeMillisecs()
+ - (static_cast(g_core->AppTimeMillisecs()
- i->second.starttime)
/ static_cast(i->second.endtime - i->second.starttime));
s->SetFade(fade_val);
@@ -1639,12 +1638,12 @@ void AudioServer::ClearSoundRefDeleteList() {
// g_base->audio_server->PushSetSuspendedCall(true);
// // Wait a reasonable amount of time for the thread to act on it.
-// millisecs_t t = g_core->GetAppTimeMillisecs();
+// millisecs_t t = g_core->AppTimeMillisecs();
// while (true) {
// if (g_base->audio_server->suspended()) {
// break;
// }
-// if (g_core->GetAppTimeMillisecs() - t > 1000) {
+// if (g_core->AppTimeMillisecs() - t > 1000) {
// Log(LogLevel::kError, "Timed out waiting for audio suspend.");
// break;
// }
@@ -1657,12 +1656,12 @@ void AudioServer::ClearSoundRefDeleteList() {
// g_base->audio_server->PushSetSuspendedCall(false);
// // Wait a reasonable amount of time for the thread to act on it.
-// millisecs_t t = g_core->GetAppTimeMillisecs();
+// millisecs_t t = g_core->AppTimeMillisecs();
// while (true) {
// if (!g_base->audio_server->suspended()) {
// break;
// }
-// if (g_core->GetAppTimeMillisecs() - t > 1000) {
+// if (g_core->AppTimeMillisecs() - t > 1000) {
// Log(LogLevel::kError, "Timed out waiting for audio unsuspend.");
// break;
// }
diff --git a/src/ballistica/base/audio/audio_source.cc b/src/ballistica/base/audio/audio_source.cc
index fa420c86..e3f1dc5e 100644
--- a/src/ballistica/base/audio/audio_source.cc
+++ b/src/ballistica/base/audio/audio_source.cc
@@ -103,7 +103,7 @@ void AudioSource::Lock(int debug_id) {
BA_DEBUG_FUNCTION_TIMER_BEGIN();
mutex_.lock();
#if BA_DEBUG_BUILD
- last_lock_time_ = g_core->GetAppTimeMillisecs();
+ last_lock_time_ = g_core->AppTimeMillisecs();
lock_debug_id_ = debug_id;
locked_ = true;
#endif
@@ -115,7 +115,7 @@ auto AudioSource::TryLock(int debug_id) -> bool {
#if (BA_DEBUG_BUILD || BA_TEST_BUILD)
if (locked) {
locked_ = true;
- last_lock_time_ = g_core->GetAppTimeMillisecs();
+ last_lock_time_ = g_core->AppTimeMillisecs();
lock_debug_id_ = debug_id;
}
#endif
diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc
index 99b62cf1..049810e8 100644
--- a/src/ballistica/base/base.cc
+++ b/src/ballistica/base/base.cc
@@ -174,6 +174,11 @@ void BaseFeatureSet::ErrorScreenMessage() {
}
auto BaseFeatureSet::GetV2AccountID() -> std::optional {
+ // Guard against this getting called early.
+ if (!IsAppStarted()) {
+ return {};
+ }
+
auto gil = Python::ScopedInterpreterLock();
auto result =
python->objs().Get(BasePython::ObjID::kGetV2AccountIdCall).Call();
@@ -199,7 +204,7 @@ void BaseFeatureSet::StartApp() {
BA_PRECONDITION(g_core->InMainThread());
BA_PRECONDITION(g_base);
- auto start_time = g_core->GetAppTimeSeconds();
+ auto start_time = g_core->AppTimeSeconds();
// Currently limiting this to once per process.
BA_PRECONDITION(!called_start_app_);
@@ -253,7 +258,7 @@ void BaseFeatureSet::StartApp() {
// Make some noise if this takes more than a few seconds. If we pass 5
// seconds or so we start to trigger App-Not-Responding reports which
// isn't good.
- auto duration = g_core->GetAppTimeSeconds() - start_time;
+ auto duration = g_core->AppTimeSeconds() - start_time;
if (duration > 3.0) {
char buffer[128];
snprintf(buffer, sizeof(buffer),
@@ -272,7 +277,7 @@ void BaseFeatureSet::SuspendApp() {
return;
}
- millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()};
+ millisecs_t start_time{core::CorePlatform::TimeMonotonicMillisecs()};
// Apple mentioned 5 seconds to run stuff once backgrounded or they bring
// down the hammer. Let's aim to stay under 2.
@@ -280,7 +285,7 @@ void BaseFeatureSet::SuspendApp() {
g_core->platform->LowLevelDebugLog(
"SuspendApp@"
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
+ + std::to_string(core::CorePlatform::TimeMonotonicMillisecs()));
app_suspended_ = true;
// IMPORTANT: Any pause related stuff that event-loop-threads need to do
@@ -311,13 +316,13 @@ void BaseFeatureSet::SuspendApp() {
g_core->Log(
LogName::kBa, LogLevel::kDebug,
"SuspendApp() completed in "
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()
+ + std::to_string(core::CorePlatform::TimeMonotonicMillisecs()
- start_time)
+ "ms.");
}
return;
}
- } while (std::abs(core::CorePlatform::GetCurrentMillisecs() - start_time)
+ } while (std::abs(core::CorePlatform::TimeMonotonicMillisecs() - start_time)
< max_duration);
// If we made it here, we timed out. Complain.
@@ -325,7 +330,8 @@ void BaseFeatureSet::SuspendApp() {
std::string("SuspendApp() took too long; ")
+ std::to_string(running_loops.size())
+ " event-loops not yet suspended after "
- + std::to_string(core::CorePlatform::GetCurrentMillisecs() - start_time)
+ + std::to_string(core::CorePlatform::TimeMonotonicMillisecs()
+ - start_time)
+ " ms: (";
bool first = true;
for (auto* loop : running_loops) {
@@ -383,10 +389,10 @@ void BaseFeatureSet::UnsuspendApp() {
"AppAdapter::UnsuspendApp() called with app not in suspendedstate.");
return;
}
- millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()};
+ millisecs_t start_time{core::CorePlatform::TimeMonotonicMillisecs()};
g_core->platform->LowLevelDebugLog(
"UnsuspendApp@"
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
+ + std::to_string(core::CorePlatform::TimeMonotonicMillisecs()));
app_suspended_ = false;
// Spin all event-loops back up.
@@ -397,11 +403,12 @@ void BaseFeatureSet::UnsuspendApp() {
g_base->networking->OnAppUnsuspend();
if (g_buildconfig.debug_build()) {
- g_core->Log(LogName::kBa, LogLevel::kDebug,
- "UnsuspendApp() completed in "
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()
- - start_time)
- + "ms.");
+ g_core->Log(
+ LogName::kBa, LogLevel::kDebug,
+ "UnsuspendApp() completed in "
+ + std::to_string(core::CorePlatform::TimeMonotonicMillisecs()
+ - start_time)
+ + "ms.");
}
}
@@ -571,7 +578,7 @@ auto BaseFeatureSet::GetAppInstanceUUID() -> const std::string& {
g_core->Log(LogName::kBa, LogLevel::kWarning,
"GetSessionUUID() using rand fallback.");
srand(static_cast(
- core::CorePlatform::GetCurrentMillisecs())); // NOLINT
+ core::CorePlatform::TimeMonotonicMillisecs())); // NOLINT
app_instance_uuid =
std::to_string(static_cast(rand())); // NOLINT
have_app_instance_uuid = true;
@@ -966,7 +973,7 @@ void BaseFeatureSet::SetAppActive(bool active) {
g_core->platform->LowLevelDebugLog(
"SetAppActive(" + std::to_string(active) + ")@"
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
+ + std::to_string(core::CorePlatform::TimeMonotonicMillisecs()));
// Issue a gentle warning if they are feeding us the same state twice in a
// row; might imply faulty logic on an app-adapter or whatnot.
@@ -990,6 +997,12 @@ void BaseFeatureSet::Reset() {
audio->Reset();
}
+auto BaseFeatureSet::TimeSinceEpochCloudSeconds() -> seconds_t {
+ // TODO(ericf): wire this up. Just using local time for now. And make sure
+ // that this and utc_now_cloud() in the Python layer are synced up.
+ return core::CorePlatform::TimeSinceEpochSeconds();
+}
+
void BaseFeatureSet::SetUIScale(UIScale scale) {
assert(InLogicThread());
diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h
index 8bc37bb7..7da63538 100644
--- a/src/ballistica/base/base.h
+++ b/src/ballistica/base/base.h
@@ -476,7 +476,8 @@ enum class SysTextureID : uint8_t {
kFontExtras4,
kCharacterIconMask,
kBlack,
- kWings
+ kWings,
+ kSpinner
};
enum class SysCubeMapTextureID : uint8_t {
@@ -761,8 +762,8 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
void PushMainThreadRunnable(Runnable* runnable) override;
- /// Return the currently signed in V2 account id as
- /// reported by the Python layer.
+ /// Return the currently signed in V2 account id as reported by the Python
+ /// layer.
auto GetV2AccountID() -> std::optional;
/// Return whether clipboard operations are supported at all. This gets
@@ -784,6 +785,10 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// Set overall ui scale for the app.
void SetUIScale(UIScale scale);
+ /// Time since epoch on the master-server. Tries to
+ /// be correct even if local time is set wrong.
+ auto TimeSinceEpochCloudSeconds() -> seconds_t;
+
// Const subsystems.
AppAdapter* const app_adapter;
AppConfig* const app_config;
diff --git a/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc b/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc
index 29ef9e4a..8e8e02cf 100644
--- a/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc
+++ b/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc
@@ -96,7 +96,7 @@ class BGDynamicsServer::Terrain {
if (collision_mesh_) {
Object::Ref* ref = collision_mesh_;
g_base->logic->event_loop()->PushCall([ref] {
- (**ref).set_last_used_time(g_core->GetAppTimeMillisecs());
+ (**ref).set_last_used_time(g_core->AppTimeMillisecs());
delete ref;
});
collision_mesh_ = nullptr;
diff --git a/src/ballistica/base/graphics/gl/renderer_gl.cc b/src/ballistica/base/graphics/gl/renderer_gl.cc
index ed46a130..f04005fe 100644
--- a/src/ballistica/base/graphics/gl/renderer_gl.cc
+++ b/src/ballistica/base/graphics/gl/renderer_gl.cc
@@ -90,12 +90,12 @@ void RendererGL::CheckGLError(const char* file, int line) {
BA_PRECONDITION_FATAL(vendor);
const char* renderer = (const char*)glGetString(GL_RENDERER);
BA_PRECONDITION_FATAL(renderer);
- g_core->Log(
- LogName::kBaGraphics, LogLevel::kError,
- "OpenGL Error at " + std::string(file) + " line " + std::to_string(line)
- + ": " + GLErrorToString(err) + "\nrenderer: " + renderer
- + "\nvendor: " + vendor + "\nversion: " + version
- + "\ntime: " + std::to_string(g_core->GetAppTimeMillisecs()));
+ g_core->Log(LogName::kBaGraphics, LogLevel::kError,
+ "OpenGL Error at " + std::string(file) + " line "
+ + std::to_string(line) + ": " + GLErrorToString(err)
+ + "\nrenderer: " + renderer + "\nvendor: " + vendor
+ + "\nversion: " + version
+ + "\ntime: " + std::to_string(g_core->AppTimeMillisecs()));
}
}
diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc
index c9fcffa2..7c16f67f 100644
--- a/src/ballistica/base/graphics/graphics.cc
+++ b/src/ballistica/base/graphics/graphics.cc
@@ -253,13 +253,13 @@ auto Graphics::GraphicsQualityFromAppConfig() -> GraphicsQualityRequest {
void Graphics::SetGyroEnabled(bool enable) {
// If we're turning back on, suppress gyro updates for a bit.
if (enable && !gyro_enabled_) {
- last_suppress_gyro_time_ = g_core->GetAppTimeMicrosecs();
+ last_suppress_gyro_time_ = g_core->AppTimeMicrosecs();
}
gyro_enabled_ = enable;
}
void Graphics::UpdateProgressBarProgress(float target) {
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
float p = target;
if (p < 0) {
p = 0;
@@ -274,7 +274,7 @@ void Graphics::UpdateProgressBarProgress(float target) {
}
void Graphics::DrawProgressBar(RenderPass* pass, float opacity) {
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
float amount = progress_bar_progress_;
if (amount < 0) {
amount = 0;
@@ -361,9 +361,9 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
assert(g_base && g_base->InLogicThread());
// Every now and then, update our stats.
- while (g_core->GetAppTimeMillisecs() >= next_stat_update_time_) {
- if (g_core->GetAppTimeMillisecs() - next_stat_update_time_ > 1000) {
- next_stat_update_time_ = g_core->GetAppTimeMillisecs() + 1000;
+ while (g_core->AppTimeMillisecs() >= next_stat_update_time_) {
+ if (g_core->AppTimeMillisecs() - next_stat_update_time_ > 1000) {
+ next_stat_update_time_ = g_core->AppTimeMillisecs() + 1000;
} else {
next_stat_update_time_ += 1000;
}
@@ -482,7 +482,7 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
// Draw any debug graphs.
{
float debug_graph_y = 50.0;
- auto now = g_core->GetAppTimeMillisecs();
+ auto now = g_core->AppTimeMillisecs();
for (auto it = debug_graphs_.begin(); it != debug_graphs_.end();) {
assert(it->second.exists());
if (now - it->second->LastUsedTime() > 1000) {
@@ -508,7 +508,7 @@ auto Graphics::GetDebugGraph(const std::string& name, bool smoothed)
debug_graphs_[name]->SetLabel(name);
debug_graphs_[name]->SetSmoothed(smoothed);
}
- debug_graphs_[name]->SetLastUsedTime(g_core->GetAppTimeMillisecs());
+ debug_graphs_[name]->SetLastUsedTime(g_core->AppTimeMillisecs());
return debug_graphs_[name].get();
}
@@ -770,7 +770,7 @@ void Graphics::BuildAndPushFrameDef() {
assert(!building_frame_def_);
building_frame_def_ = true;
- microsecs_t app_time_microsecs = g_core->GetAppTimeMicrosecs();
+ microsecs_t app_time_microsecs = g_core->AppTimeMicrosecs();
// Store how much time this frame_def represents.
auto display_time_microsecs = g_base->logic->display_time_microsecs();
@@ -1222,7 +1222,7 @@ void Graphics::EnableProgressBar(bool fade_in) {
if (progress_bar_loads_ > 0) {
progress_bar_ = true;
progress_bar_fade_in_ = fade_in;
- last_progress_bar_draw_time_ = g_core->GetAppTimeMillisecs();
+ last_progress_bar_draw_time_ = g_core->AppTimeMillisecs();
last_progress_bar_start_time_ = last_progress_bar_draw_time_;
progress_bar_progress_ = 0.0f;
}
diff --git a/src/ballistica/base/graphics/graphics_server.cc b/src/ballistica/base/graphics/graphics_server.cc
index 493467ac..3be3790e 100644
--- a/src/ballistica/base/graphics/graphics_server.cc
+++ b/src/ballistica/base/graphics/graphics_server.cc
@@ -147,7 +147,7 @@ auto GraphicsServer::TryRender() -> bool {
auto GraphicsServer::WaitForRenderFrameDef_() -> FrameDef* {
assert(g_base->app_adapter->InGraphicsContext());
- millisecs_t start_time = g_core->GetAppTimeMillisecs();
+ millisecs_t start_time = g_core->AppTimeMillisecs();
// Spin and wait for a short bit for a frame_def to appear.
while (true) {
@@ -176,7 +176,7 @@ auto GraphicsServer::WaitForRenderFrameDef_() -> FrameDef* {
}
// If there's no frame_def for us, sleep for a bit and wait for it.
- millisecs_t t = g_core->GetAppTimeMillisecs() - start_time;
+ millisecs_t t = g_core->AppTimeMillisecs() - start_time;
if (t >= 1000) {
if (g_buildconfig.debug_build()) {
g_core->Log(LogName::kBaGraphics, LogLevel::kWarning,
diff --git a/src/ballistica/base/graphics/renderer/renderer.cc b/src/ballistica/base/graphics/renderer/renderer.cc
index 2a61bc60..27645a95 100644
--- a/src/ballistica/base/graphics/renderer/renderer.cc
+++ b/src/ballistica/base/graphics/renderer/renderer.cc
@@ -650,7 +650,7 @@ void Renderer::UpdatePixelScaleAndBackingBuffer(FrameDef* frame_def) {
}
void Renderer::LoadMedia(FrameDef* frame_def) {
- millisecs_t t = g_core->GetAppTimeMillisecs();
+ millisecs_t t = g_core->AppTimeMillisecs();
for (auto&& i : frame_def->media_components()) {
Asset* mc = i.get();
assert(mc);
@@ -667,7 +667,7 @@ void Renderer::LoadMedia(FrameDef* frame_def) {
// // default about 1 second after a res change, etc...
// // so if we're using a non-1.0 gamma, lets keep setting it periodically
// // to force the issue
-// millisecs_t t = g_core->GetAppTimeMillisecs();
+// millisecs_t t = g_core->AppTimeMillisecs();
// if (screen_gamma_requested_ != screen_gamma_
// || (t - last_screen_gamma_update_time_ > 300 && screen_gamma_ != 1.0f))
// {
diff --git a/src/ballistica/base/graphics/support/camera.cc b/src/ballistica/base/graphics/support/camera.cc
index e8137cb9..d236d970 100644
--- a/src/ballistica/base/graphics/support/camera.cc
+++ b/src/ballistica/base/graphics/support/camera.cc
@@ -207,10 +207,9 @@ void Camera::UpdatePosition() {
lr_jitter = 0.0f;
} else {
lr_jitter =
- sinf(static_cast(g_core->GetAppTimeMillisecs()) / 108.0f)
+ sinf(static_cast(g_core->AppTimeMillisecs()) / 108.0f)
* 0.4f
- + sinf(static_cast(g_core->GetAppTimeMillisecs())
- / 268.0f)
+ + sinf(static_cast(g_core->AppTimeMillisecs()) / 268.0f)
* 1.0f;
lr_jitter *= 0.05f;
}
@@ -891,7 +890,7 @@ void Camera::SetMode(CameraMode m) {
if (mode_ != m) {
mode_ = m;
smooth_next_frame_ = false;
- // last_mode_set_time_ = g_core->GetAppTimeMillisecs();
+ // last_mode_set_time_ = g_core->AppTimeMillisecs();
// last_mode_set_time_ = time_;
heading_ = kInitialHeading;
}
diff --git a/src/ballistica/base/graphics/support/screen_messages.cc b/src/ballistica/base/graphics/support/screen_messages.cc
index c857d99d..3a7dd8c3 100644
--- a/src/ballistica/base/graphics/support/screen_messages.cc
+++ b/src/ballistica/base/graphics/support/screen_messages.cc
@@ -65,8 +65,8 @@ void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) {
// Delete old ones.
if (!screen_messages_.empty()) {
millisecs_t cutoff;
- if (g_core->GetAppTimeMillisecs() > 5000) {
- cutoff = g_core->GetAppTimeMillisecs() - 5000;
+ if (g_core->AppTimeMillisecs() > 5000) {
+ cutoff = g_core->AppTimeMillisecs() - 5000;
for (auto i = screen_messages_.begin(); i != screen_messages_.end();) {
if (i->creation_time < cutoff) {
auto next = i;
@@ -128,7 +128,7 @@ void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) {
// which is calculated as part of it.
i->GetText();
- millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time;
+ millisecs_t age = g_core->AppTimeMillisecs() - i->creation_time;
youngest_age = std::min(youngest_age, age);
float s_extra = 1.0f;
if (age < 100) {
@@ -244,7 +244,7 @@ void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) {
for (auto i = screen_messages_.rbegin(); i != screen_messages_.rend();
i++) {
- millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time;
+ millisecs_t age = g_core->AppTimeMillisecs() - i->creation_time;
youngest_age = std::min(youngest_age, age);
float s_extra = 1.0f;
if (age < 100) {
@@ -315,8 +315,8 @@ void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) {
// Delete old ones.
if (!screen_messages_top_.empty()) {
millisecs_t cutoff;
- if (g_core->GetAppTimeMillisecs() > 5000) {
- cutoff = g_core->GetAppTimeMillisecs() - 5000;
+ if (g_core->AppTimeMillisecs() > 5000) {
+ cutoff = g_core->AppTimeMillisecs() - 5000;
for (auto i = screen_messages_top_.begin();
i != screen_messages_top_.end();) {
if (i->creation_time < cutoff) {
@@ -354,7 +354,7 @@ void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) {
// Update the translation if need be.
i->UpdateTranslation();
- millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time;
+ millisecs_t age = g_core->AppTimeMillisecs() - i->creation_time;
float s_extra = 1.0f;
if (age < 100) {
s_extra = std::min(1.1f, 1.1f * (static_cast(age) / 100.0f));
@@ -466,13 +466,13 @@ void ScreenMessages::AddScreenMessage(const std::string& msg,
start_v,
std::max(-100.0f, screen_messages_top_.back().v_smoothed - 25.0f));
}
- screen_messages_top_.emplace_back(m, true, g_core->GetAppTimeMillisecs(),
+ screen_messages_top_.emplace_back(m, true, g_core->AppTimeMillisecs(),
color, texture, tint_texture, tint,
tint2);
screen_messages_top_.back().v_smoothed = start_v;
} else {
- screen_messages_.emplace_back(m, false, g_core->GetAppTimeMillisecs(),
- color, texture, tint_texture, tint, tint2);
+ screen_messages_.emplace_back(m, false, g_core->AppTimeMillisecs(), color,
+ texture, tint_texture, tint, tint2);
}
}
@@ -534,8 +534,7 @@ auto ScreenMessages::ScreenMessageEntry::GetText() -> TextGroup& {
void ScreenMessages::ScreenMessageEntry::UpdateTranslation() {
if (translation_dirty) {
- s_translated = g_base->assets->CompileResourceString(
- s_raw, "Graphics::ScreenMessageEntry::UpdateTranslation");
+ s_translated = g_base->assets->CompileResourceString(s_raw);
translation_dirty = false;
mesh_dirty = true;
}
diff --git a/src/ballistica/base/input/device/joystick_input.cc b/src/ballistica/base/input/device/joystick_input.cc
index 09e8b6c5..92626ee5 100644
--- a/src/ballistica/base/input/device/joystick_input.cc
+++ b/src/ballistica/base/input/device/joystick_input.cc
@@ -37,7 +37,7 @@ JoystickInput::JoystickInput(int sdl_joystick_id,
calibration_break_threshold_(kJoystickCalibrationBreakThreshold),
custom_device_name_(custom_device_name),
can_configure_(can_configure),
- creation_time_(g_core->GetAppTimeMillisecs()),
+ creation_time_(g_core->AppTimeMillisecs()),
calibrate_(calibrate) {
// This is the default calibration for 'non-full' analog calibration.
for (float& analog_calibration_val : analog_calibration_vals_) {
@@ -374,7 +374,7 @@ void JoystickInput::Update() {
// Let's take this opportunity to update our calibration
// (should probably have a specific place to do that but this works)
if (calibrate_) {
- millisecs_t time = g_core->GetAppTimeMillisecs();
+ millisecs_t time = g_core->AppTimeMillisecs();
// If we're doing 'aggressive' auto-recalibration we expand extents outward
// but suck them inward a tiny bit too to account for jitter or random fluke
@@ -545,7 +545,7 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) {
return;
}
- millisecs_t time = g_core->GetAppTimeMillisecs();
+ millisecs_t time = g_core->AppTimeMillisecs();
SDL_Event e2;
// Ignore analog-stick input while we're holding a hat switch or d-pad
@@ -959,7 +959,7 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) {
&& (e->jbutton.button != hold_position_button_)
&& (e->jbutton.button != back_button_)) {
if (ui_only_ || e->jbutton.button == remote_enter_button_) {
- millisecs_t current_time = g_core->GetAppTimeMillisecs();
+ millisecs_t current_time = g_core->AppTimeMillisecs();
if (current_time - last_ui_only_print_time_ > 5000) {
g_base->python->objs()
.Get(BasePython::ObjID::kUIRemotePressCall)
diff --git a/src/ballistica/base/input/device/touch_input.cc b/src/ballistica/base/input/device/touch_input.cc
index b514bb15..1667a177 100644
--- a/src/ballistica/base/input/device/touch_input.cc
+++ b/src/ballistica/base/input/device/touch_input.cc
@@ -93,7 +93,7 @@ TouchInput::TouchInput() {
TouchInput::~TouchInput() = default;
void TouchInput::UpdateButtons(bool new_touch) {
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
float spread_scaled_actions =
kButtonSpread * base_controls_scale_ * controls_scale_actions_;
float width = g_base->graphics->screen_virtual_width();
@@ -134,7 +134,7 @@ void TouchInput::UpdateButtons(bool new_touch) {
closest_to_bomb = true;
}
if (buttons_touch_) {
- last_buttons_touch_time_ = g_core->GetAppTimeMillisecs();
+ last_buttons_touch_time_ = g_core->AppTimeMillisecs();
}
// Handle swipe mode.
diff --git a/src/ballistica/base/input/input.cc b/src/ballistica/base/input/input.cc
index 2363588e..461d586c 100644
--- a/src/ballistica/base/input/input.cc
+++ b/src/ballistica/base/input/input.cc
@@ -158,7 +158,7 @@ void Input::AnnounceConnects_() {
// For the first announcement just say "X controllers detected" and don't
// have a sound.
- if (first_print && g_core->GetAppTimeSeconds() < 3.0) {
+ if (first_print && g_core->AppTimeSeconds() < 3.0) {
first_print = false;
// If there's been several connected, just give a number.
@@ -225,7 +225,7 @@ void Input::ShowStandardInputDeviceConnectedMessage_(InputDevice* j) {
// On Android we never show messages for initial input-devices; we often
// get large numbers of strange virtual devices that aren't actually
// controllers so this is more confusing than helpful.
- if (g_buildconfig.ostype_android() && g_core->GetAppTimeSeconds() < 3.0) {
+ if (g_buildconfig.ostype_android() && g_core->AppTimeSeconds() < 3.0) {
return;
}
@@ -554,7 +554,7 @@ void Input::OnScreenSizeChange() { assert(g_base->InLogicThread()); }
void Input::StepDisplayTime() {
assert(g_base->InLogicThread());
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
// If input has been locked an excessively long amount of time, unlock it.
if (input_lock_count_temp_) {
@@ -622,13 +622,13 @@ void Input::LockAllInput(bool permanent, const std::string& label) {
} else {
input_lock_count_temp_++;
if (input_lock_count_temp_ == 1) {
- last_input_temp_lock_time_ = g_core->GetAppTimeMillisecs();
+ last_input_temp_lock_time_ = g_core->AppTimeMillisecs();
}
input_lock_temp_labels_.push_back(label);
recent_input_locks_unlocks_.push_back(
"temp lock: " + label + " time "
- + std::to_string(g_core->GetAppTimeMillisecs()));
+ + std::to_string(g_core->AppTimeMillisecs()));
while (recent_input_locks_unlocks_.size() > 10) {
recent_input_locks_unlocks_.pop_front();
}
@@ -641,7 +641,7 @@ void Input::UnlockAllInput(bool permanent, const std::string& label) {
recent_input_locks_unlocks_.push_back(
permanent ? "permanent unlock: "
: "temp unlock: " + label + " time "
- + std::to_string(g_core->GetAppTimeMillisecs()));
+ + std::to_string(g_core->AppTimeMillisecs()));
while (recent_input_locks_unlocks_.size() > 10) {
recent_input_locks_unlocks_.pop_front();
}
@@ -667,7 +667,7 @@ void Input::UnlockAllInput(bool permanent, const std::string& label) {
if (input_lock_count_temp_ < 0) {
g_core->Log(LogName::kBaInput, LogLevel::kWarning,
"temp input unlock at time "
- + std::to_string(g_core->GetAppTimeMillisecs())
+ + std::to_string(g_core->AppTimeMillisecs())
+ " with no active lock: '" + label + "'");
// This is to be expected since we can reset this to 0.
input_lock_count_temp_ = 0;
@@ -684,7 +684,7 @@ void Input::UnlockAllInput(bool permanent, const std::string& label) {
void Input::PrintLockLabels_() {
std::string s = "INPUT LOCK REPORT (time="
- + std::to_string(g_core->GetAppTimeMillisecs()) + "):";
+ + std::to_string(g_core->AppTimeMillisecs()) + "):";
int num;
s += "\n " + std::to_string(input_lock_temp_labels_.size()) + " TEMP LOCKS:";
@@ -926,7 +926,7 @@ void Input::HandleKeyPress_(const SDL_Keysym& keysym) {
// fluke repeat key press event due to funky OS circumstances.
static int count{};
static seconds_t last_count_reset_time{};
- auto now = g_core->GetAppTimeSeconds();
+ auto now = g_core->AppTimeSeconds();
if (now - last_count_reset_time > 2.0) {
count = 0;
last_count_reset_time = now;
@@ -1238,7 +1238,7 @@ void Input::HandleSmoothMouseScroll_(const Vector2f& velocity, bool momentum) {
WidgetMessage(WidgetMessage::Type::kMouseWheelVelocityH, nullptr,
cursor_pos_x_, cursor_pos_y_, velocity.x, momentum));
- last_mouse_move_time_ = g_core->GetAppTimeSeconds();
+ last_mouse_move_time_ = g_core->AppTimeSeconds();
mouse_move_count_++;
Camera* camera = g_base->graphics->camera();
@@ -1282,7 +1282,7 @@ void Input::HandleMouseMotion_(const Vector2f& position) {
cursor_pos_y_ = g_base->graphics->PixelToVirtualY(
position.y * g_base->graphics->screen_pixel_height());
- last_mouse_move_time_ = g_core->GetAppTimeSeconds();
+ last_mouse_move_time_ = g_core->AppTimeSeconds();
mouse_move_count_++;
// If we have a touch-input in editing mode, pass along events to it. (it
@@ -1324,7 +1324,7 @@ void Input::HandleMouseDown_(int button, const Vector2f& position) {
return;
}
- last_mouse_move_time_ = g_core->GetAppTimeSeconds();
+ last_mouse_move_time_ = g_core->AppTimeSeconds();
mouse_move_count_++;
// Convert normalized view coords to our virtual ones.
@@ -1333,7 +1333,7 @@ void Input::HandleMouseDown_(int button, const Vector2f& position) {
cursor_pos_y_ = g_base->graphics->PixelToVirtualY(
position.y * g_base->graphics->screen_pixel_height());
- millisecs_t click_time = g_core->GetAppTimeMillisecs();
+ millisecs_t click_time = g_core->AppTimeMillisecs();
bool double_click = (click_time - last_click_time_ <= double_click_time_);
last_click_time_ = click_time;
@@ -1528,7 +1528,7 @@ auto Input::IsCursorVisible() const -> bool {
bool val;
// Show our cursor only if its been moved recently.
- val = (g_core->GetAppTimeSeconds() - last_mouse_move_time_ < 2.071);
+ val = (g_core->AppTimeSeconds() - last_mouse_move_time_ < 2.071);
return val;
}
diff --git a/src/ballistica/base/input/support/remote_app_server.cc b/src/ballistica/base/input/support/remote_app_server.cc
index 856bf59f..809fc99a 100644
--- a/src/ballistica/base/input/support/remote_app_server.cc
+++ b/src/ballistica/base/input/support/remote_app_server.cc
@@ -221,7 +221,7 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt,
RemoteAppClient* client = clients_ + joystick_id;
// Take note that we heard from them.
- client->last_contact_time = g_core->GetAppTimeMillisecs();
+ client->last_contact_time = g_core->AppTimeMillisecs();
// Ok now iterate.
uint8_t* val = buffer + 4;
@@ -389,7 +389,7 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr,
}
// Don't reuse a slot for 5 seconds (if its been heard from since this time).
- millisecs_t cooldown_time = g_core->GetAppTimeMillisecs() - 5000;
+ millisecs_t cooldown_time = g_core->AppTimeMillisecs() - 5000;
// Ok, not there already.. now look for a non-taken one and return that.
for (int i = 0; i < kMaxRemoteAppClients; i++) {
@@ -412,7 +412,7 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr,
strcpy(clients_[i].display_name, clients_[i].name); // NOLINT
char* c = strchr(clients_[i].display_name, '#');
if (c) *c = 0;
- clients_[i].last_contact_time = g_core->GetAppTimeMillisecs();
+ clients_[i].last_contact_time = g_core->AppTimeMillisecs();
clients_[i].request_id = request_id;
char m[256];
diff --git a/src/ballistica/base/logic/logic.cc b/src/ballistica/base/logic/logic.cc
index 76e4f106..7b4ddc3f 100644
--- a/src/ballistica/base/logic/logic.cc
+++ b/src/ballistica/base/logic/logic.cc
@@ -236,7 +236,7 @@ void Logic::OnAppShutdown() {
assert(shutting_down_);
// Nuke the app from orbit if we get stuck while shutting down.
- g_core->StartSuicideTimer("shutdown", 10000);
+ g_core->StartSuicideTimer("shutdown", 15000);
// Tell base to disallow shutdown-suppressors from here on out.
g_base->ShutdownSuppressDisallow();
@@ -402,7 +402,7 @@ void Logic::UpdateDisplayTimeForHeadlessMode_() {
// scheduled (or at least close enough so we can fudge it and tell them
// its that exact time).
- auto app_time_microsecs = g_core->GetAppTimeMicrosecs();
+ auto app_time_microsecs = g_core->AppTimeMicrosecs();
// Set our int based time vals so we can exactly hit timers.
auto old_display_time_microsecs = display_time_microsecs_;
@@ -438,7 +438,7 @@ void Logic::PostUpdateDisplayTimeForHeadlessMode_() {
[headless_display_step_microsecs] {
auto sleepsecs =
static_cast(headless_display_step_microsecs) / 1000000.0;
- auto apptimesecs = g_core->GetAppTimeSeconds();
+ auto apptimesecs = g_core->AppTimeSeconds();
char buffer[256];
snprintf(buffer, sizeof(buffer),
"will try to sleep for %.4f at app-time %.4f (until %.4f)",
@@ -467,7 +467,7 @@ void Logic::UpdateDisplayTimeForFrameDraw_() {
// - 'current' should mostly show '(avg)'; rarely '(sample)'.
// - these can vary briefly during load spikes/etc. but should quickly
// reconverge to stability. If not, this may need further calibration.
- auto current_app_time = g_core->GetAppTimeSeconds();
+ auto current_app_time = g_core->AppTimeSeconds();
// We handle the first measurement specially.
if (last_display_time_update_app_time_ < 0) {
diff --git a/src/ballistica/base/python/methods/python_methods_base_1.cc b/src/ballistica/base/python/methods/python_methods_base_1.cc
index a7a923f2..b2821b16 100644
--- a/src/ballistica/base/python/methods/python_methods_base_1.cc
+++ b/src/ballistica/base/python/methods/python_methods_base_1.cc
@@ -330,8 +330,8 @@ static auto PyAppTime(PyObject* self, PyObject* args, PyObject* keywds)
const_cast(kwlist))) {
return nullptr;
}
- return PyFloat_FromDouble(
- 0.001 * static_cast(g_core->GetAppTimeMillisecs()));
+ return PyFloat_FromDouble(0.001
+ * static_cast(g_core->AppTimeMillisecs()));
BA_PYTHON_CATCH;
}
diff --git a/src/ballistica/base/python/methods/python_methods_base_2.cc b/src/ballistica/base/python/methods/python_methods_base_2.cc
index e235e954..96737c06 100644
--- a/src/ballistica/base/python/methods/python_methods_base_2.cc
+++ b/src/ballistica/base/python/methods/python_methods_base_2.cc
@@ -497,7 +497,7 @@ static auto PyEvaluateLstr(PyObject* self, PyObject* args, PyObject* keywds)
return nullptr;
}
return PyUnicode_FromString(
- g_base->assets->CompileResourceString(value, "evaluate_lstr").c_str());
+ g_base->assets->CompileResourceString(value).c_str());
BA_PYTHON_CATCH;
}
@@ -533,7 +533,7 @@ static auto PyGetStringHeight(PyObject* self, PyObject* args, PyObject* keywds)
}
s = g_base->python->GetPyLString(s_obj);
#if BA_DEBUG_BUILD
- if (g_base->assets->CompileResourceString(s, "get_string_height test") != s) {
+ if (g_base->assets->CompileResourceString(s) != s) {
BA_LOG_PYTHON_TRACE(
"resource-string passed to get_string_height; this should be avoided");
}
@@ -579,8 +579,7 @@ static auto PyGetStringWidth(PyObject* self, PyObject* args, PyObject* keywds)
}
s = g_base->python->GetPyLString(s_obj);
#if BA_DEBUG_BUILD
- if (g_base->assets->CompileResourceString(s, "get_string_width debug test")
- != s) {
+ if (g_base->assets->CompileResourceString(s) != s) {
BA_LOG_PYTHON_TRACE(
"resource-string passed to get_string_width; this should be avoided");
}
diff --git a/src/ballistica/base/support/classic_soft.h b/src/ballistica/base/support/classic_soft.h
index 5e388e93..45811a01 100644
--- a/src/ballistica/base/support/classic_soft.h
+++ b/src/ballistica/base/support/classic_soft.h
@@ -7,6 +7,7 @@
#include
#include "ballistica/base/base.h"
+#include "ballistica/shared/math/vector3f.h"
namespace ballistica::base {
@@ -46,6 +47,11 @@ class ClassicSoftInterface {
virtual auto GetV1AccountTypeIconString(int account_type) -> std::string = 0;
virtual auto V1AccountTypeToString(int account_type) -> std::string = 0;
virtual void PlayMusic(const std::string& music_type, bool continuous) = 0;
+ virtual void GetClassicChestDisplayInfo(const std::string& id,
+ std::string* texclosed,
+ std::string* texclosedtint,
+ Vector3f* color, Vector3f* tint,
+ Vector3f* tint2) = 0;
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/support/context.h b/src/ballistica/base/support/context.h
index ac1133bb..118decc5 100644
--- a/src/ballistica/base/support/context.h
+++ b/src/ballistica/base/support/context.h
@@ -15,6 +15,7 @@ namespace ballistica::base {
// other mechanisms are set up to preserve and restore context before
// running, and objects can also be invalidated or otherwise cleaned up
// when the context they were created under dies.
+//
// The end goal of all this is to support api styles for end users where
// standalone snippets of code can be useful; ie: something like
// bs.newnode() to create something meaningful without having to worry
@@ -36,26 +37,27 @@ class ContextRef {
ContextRef();
explicit ContextRef(Context* sgc);
- /// ContextRefs are considered equal if both are pointing to the exact same
- /// Context object (or both are pointing to no Context).
+ /// ContextRefs are considered equal if both are pointing to the exact
+ /// same Context object (or both are pointing to no Context).
auto operator==(const ContextRef& other) const -> bool;
template
auto GetContextTyped() const -> T* {
// Ew; dynamic cast.
- // Note: if it ever seems like speed is an issue here, we can
- // cache the results with std::type_index entries. There should
- // generally be a very small number of types involved.
+ //
+ // Note: if it ever seems like speed is an issue here, we can cache the
+ // results with std::type_index entries. There should generally be a
+ // very small number of types involved.
return dynamic_cast(target_.get());
}
- /// An empty context-ref was explicitly set to an empty state.
- /// Note that this is different than an expired context-ref, which
- /// originally pointed to some context that has since died.
+ /// An empty context-ref was explicitly set to an empty state. Note that
+ /// this is different than an expired context-ref, which originally
+ /// pointed to some context that has since died.
auto IsEmpty() const { return empty_; }
- /// Has this context died since it was set?
- /// Note that a context created as empty is not considered expired.
+ /// Has this context died since it was set? Note that a context created as
+ /// empty is not considered expired.
auto IsExpired() const -> bool {
if (empty_) {
return false; // Can't kill what was never alive.
@@ -64,7 +66,8 @@ class ContextRef {
}
/// Return the context this ref points to. This will be nullptr for empty
- /// contexts. Throws an exception if a target context was set but has expired.
+ /// contexts. Throws an exception if a target context was set but has
+ /// expired.
auto Get() const -> Context* {
auto* target = target_.get();
if (target == nullptr && !empty_) {
@@ -84,14 +87,13 @@ class ContextRef {
bool empty_;
};
-/// Object containing the actual context_ref data/information.
-/// App-modes can subclass this to provide the actual context_ref they desire,
-/// and then code can use CurrentTyped() to safely retrieve context_ref as that
-/// type.
+/// Object containing the actual context_ref data/information. App-modes can
+/// subclass this to provide the actual context_ref they desire, and then
+/// code can use CurrentTyped() to safely retrieve context_ref as that type.
class Context : public Object {
public:
- /// Return the current context_ref cast to a desired type.
- /// Throws an Exception if the context_ref is unset or is another type.
+ /// Return the current context_ref cast to a desired type. Throws an
+ /// Exception if the context_ref is unset or is another type.
template
static auto CurrentTyped() -> T& {
T* t = g_base->CurrentContext().GetContextTyped();
@@ -102,13 +104,13 @@ class Context : public Object {
return *t;
}
- /// Called when a PythonContextCall is created in this context_ref.
- /// The context_ref class may want to store a weak-reference to the
- /// call and inform the call when the context_ref is going down so that
- /// resources may be freed. Other permanent contexts may not need to
- /// bother.
- /// FIXME: This mechanism can probably be generalized so that other
- /// things such as assets and timers can use it.
+ /// Called when a PythonContextCall is created in this context_ref. The
+ /// context_ref class may want to store a weak-reference to the call and
+ /// inform the call when the context_ref is going down so that resources
+ /// may be freed. Other permanent contexts may not need to bother.
+ ///
+ /// FIXME: This mechanism can probably be generalized so that other things
+ /// such as assets and timers can use it.
virtual void RegisterContextCall(PythonContextCall* call);
/// Return a short description of the context_ref; will be used when
@@ -118,9 +120,9 @@ class Context : public Object {
/// Return whether this context should allow default timer-types to be
/// created within it (AppTimer, DisplayTimer). Scene type contexts
- /// generally have their own timer types which are better integrated
- /// with scenes (responding to changes in game speed/etc.) so this can
- /// be used to encourage/enforce usage of those timers.
+ /// generally have their own timer types which are better integrated with
+ /// scenes (responding to changes in game speed/etc.) so this can be used
+ /// to encourage/enforce usage of those timers.
virtual auto ContextAllowsDefaultTimerTypes() -> bool;
};
diff --git a/src/ballistica/base/ui/dev_console.cc b/src/ballistica/base/ui/dev_console.cc
index e3603ea4..50aa2df2 100644
--- a/src/ballistica/base/ui/dev_console.cc
+++ b/src/ballistica/base/ui/dev_console.cc
@@ -1683,7 +1683,7 @@ auto DevConsole::PasteFromClipboard() -> bool {
}
void DevConsole::UpdateCarat_() {
- last_carat_x_change_time_ = g_core->GetAppTimeMillisecs();
+ last_carat_x_change_time_ = g_core->AppTimeMillisecs();
auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjfwef");
auto unichars_clamped = unichars;
diff --git a/src/ballistica/base/ui/ui.cc b/src/ballistica/base/ui/ui.cc
index 749351cc..f6121078 100644
--- a/src/ballistica/base/ui/ui.cc
+++ b/src/ballistica/base/ui/ui.cc
@@ -370,7 +370,7 @@ void UI::SetUIInputDevice(InputDevice* input_device) {
ui_input_device_ = input_device;
// So they dont get stolen from immediately.
- last_input_device_use_time_ = g_core->GetAppTimeMillisecs();
+ last_input_device_use_time_ = g_core->AppTimeMillisecs();
}
void UI::Reset() {
@@ -432,7 +432,7 @@ auto UI::GetWidgetForInput(InputDevice* input_device) -> ui_v1::Widget* {
return nullptr;
}
- millisecs_t time = g_core->GetAppTimeMillisecs();
+ millisecs_t time = g_core->AppTimeMillisecs();
bool print_menu_owner{};
ui_v1::Widget* ret_val;
diff --git a/src/ballistica/classic/classic.cc b/src/ballistica/classic/classic.cc
index e011a655..f4cf91bd 100644
--- a/src/ballistica/classic/classic.cc
+++ b/src/ballistica/classic/classic.cc
@@ -250,4 +250,11 @@ void ClassicFeatureSet::PlayMusic(const std::string& music_type,
python->PlayMusic(music_type, continuous);
}
+void ClassicFeatureSet::GetClassicChestDisplayInfo(
+ const std::string& id, std::string* texclosed, std::string* texclosedtint,
+ Vector3f* color, Vector3f* tint, Vector3f* tint2) {
+ python->GetClassicChestDisplayInfo(id, texclosed, texclosedtint, color, tint,
+ tint2);
+}
+
} // namespace ballistica::classic
diff --git a/src/ballistica/classic/classic.h b/src/ballistica/classic/classic.h
index 2ae85657..94323506 100644
--- a/src/ballistica/classic/classic.h
+++ b/src/ballistica/classic/classic.h
@@ -108,6 +108,9 @@ class ClassicFeatureSet : public FeatureSetNativeComponent,
auto GetV1AccountTypeIconString(int account_type) -> std::string override;
auto V1AccountTypeToString(int account_type) -> std::string override;
auto GetV1AccountType() -> int override;
+ void GetClassicChestDisplayInfo(const std::string& id, std::string* texclosed,
+ std::string* texclosedtint, Vector3f* color,
+ Vector3f* tint, Vector3f* tint2) override;
ClassicPython* const python;
V1Account* const v1_account;
diff --git a/src/ballistica/classic/python/classic_python.cc b/src/ballistica/classic/python/classic_python.cc
index 4cc7a3b6..47b36f65 100644
--- a/src/ballistica/classic/python/classic_python.cc
+++ b/src/ballistica/classic/python/classic_python.cc
@@ -4,6 +4,7 @@
#include
+#include "ballistica/base/python/base_python.h"
#include "ballistica/classic/python/methods/python_methods_classic.h"
#include "ballistica/classic/support/classic_app_mode.h"
#include "ballistica/shared/python/python_command.h" // IWYU pragma: keep.
@@ -30,6 +31,60 @@ extern "C" auto PyInit__baclassic() -> PyObject* {
void ClassicPython::ImportPythonObjs() {
#include "ballistica/classic/mgen/pyembed/binding_classic.inc"
+
+ // Cache some basic display values for chests from the Python layer. This
+ // way C++ UI stuff doesn't have to call out to Python when drawing the
+ // root UI/etc.
+
+ // Pull default chest display info.
+ chest_display_default_ = {ChestDisplayFromPython(
+ objs().Get(ObjID::kChestAppearanceDisplayInfoDefault))};
+
+ // And overrides.
+ for (auto&& item :
+ objs().Get(ObjID::kChestAppearanceDisplayInfos).DictItems()) {
+ chest_displays_[item.first.GetAttr("value").ValueAsString()] =
+ ChestDisplayFromPython(item.second);
+ }
+}
+
+auto ClassicPython::ChestDisplayFromPython(const PythonRef& ref)
+ -> ChestDisplay_ {
+ ChestDisplay_ out;
+
+ out.texclosed = ref.GetAttr("texclosed").ValueAsString().c_str();
+ out.texclosedtint = ref.GetAttr("texclosedtint").ValueAsString().c_str();
+ out.color = base::BasePython::GetPyVector3f(ref.GetAttr("color").get());
+ out.tint = base::BasePython::GetPyVector3f(ref.GetAttr("tint").get());
+ out.tint2 = base::BasePython::GetPyVector3f(ref.GetAttr("tint2").get());
+
+ return out;
+}
+
+void ClassicPython::GetClassicChestDisplayInfo(const std::string& id,
+ std::string* texclosed,
+ std::string* texclosedtint,
+ Vector3f* color, Vector3f* tint,
+ Vector3f* tint2) {
+ assert(texclosed);
+ assert(texclosedtint);
+ assert(color);
+ assert(tint);
+ assert(tint2);
+ auto&& display{chest_displays_.find(id)};
+ if (display != chest_displays_.end()) {
+ *texclosed = display->second.texclosed;
+ *texclosedtint = display->second.texclosedtint;
+ *color = display->second.color;
+ *tint = display->second.tint;
+ *tint2 = display->second.tint2;
+ } else {
+ *texclosed = chest_display_default_.texclosed;
+ *texclosedtint = chest_display_default_.texclosedtint;
+ *color = chest_display_default_.color;
+ *tint = chest_display_default_.tint;
+ *tint2 = chest_display_default_.tint2;
+ }
}
void ClassicPython::PlayMusic(const std::string& music_type, bool continuous) {
diff --git a/src/ballistica/classic/python/classic_python.h b/src/ballistica/classic/python/classic_python.h
index b02f885a..47fe8c4b 100644
--- a/src/ballistica/classic/python/classic_python.h
+++ b/src/ballistica/classic/python/classic_python.h
@@ -4,9 +4,11 @@
#define BALLISTICA_CLASSIC_PYTHON_CLASSIC_PYTHON_H_
#include
+#include
#include "ballistica/base/base.h"
#include "ballistica/classic/classic.h"
+#include "ballistica/shared/math/vector3f.h"
#include "ballistica/shared/python/python_object_set.h"
namespace ballistica::classic {
@@ -20,6 +22,8 @@ class ClassicPython {
enum class ObjID {
kDoPlayMusicCall,
kGetInputDeviceMappedValueCall,
+ kChestAppearanceDisplayInfoDefault,
+ kChestAppearanceDisplayInfos,
kLast // Sentinel; must be at end.
};
@@ -34,7 +38,22 @@ class ClassicPython {
const auto& objs() { return objs_; }
+ void GetClassicChestDisplayInfo(const std::string& id, std::string* texclosed,
+ std::string* texclosedtint, Vector3f* color,
+ Vector3f* tint, Vector3f* tint2);
+
private:
+ struct ChestDisplay_ {
+ Vector3f color;
+ std::string texclosed;
+ std::string texclosedtint;
+ Vector3f tint;
+ Vector3f tint2;
+ };
+
+ auto ChestDisplayFromPython(const PythonRef& ref) -> ChestDisplay_;
+ ChestDisplay_ chest_display_default_;
+ std::unordered_map chest_displays_;
PythonObjectSet objs_;
};
diff --git a/src/ballistica/classic/python/methods/python_methods_classic.cc b/src/ballistica/classic/python/methods/python_methods_classic.cc
index 2cc6d478..1254ae54 100644
--- a/src/ballistica/classic/python/methods/python_methods_classic.cc
+++ b/src/ballistica/classic/python/methods/python_methods_classic.cc
@@ -3,7 +3,6 @@
#include "ballistica/classic/python/methods/python_methods_classic.h"
#include
-#include
#include
#include
@@ -296,9 +295,9 @@ static auto PySetRootUIAccountValues(PyObject* self, PyObject* args,
PyObject* keywds) -> PyObject* {
BA_PYTHON_TRY;
- const char* tickets_text;
- const char* tokens_text;
- const char* league_rank_text;
+ int tickets;
+ int tokens;
+ int league_rank;
const char* league_type;
const char* achievements_percent_text;
const char* level_text;
@@ -308,19 +307,19 @@ static auto PySetRootUIAccountValues(PyObject* self, PyObject* args,
const char* chest_1_appearance;
const char* chest_2_appearance;
const char* chest_3_appearance;
- float chest_0_unlock_time;
- float chest_1_unlock_time;
- float chest_2_unlock_time;
- float chest_3_unlock_time;
- float chest_0_ad_allow_time;
- float chest_1_ad_allow_time;
- float chest_2_ad_allow_time;
- float chest_3_ad_allow_time;
+ double chest_0_unlock_time;
+ double chest_1_unlock_time;
+ double chest_2_unlock_time;
+ double chest_3_unlock_time;
+ double chest_0_ad_allow_time;
+ double chest_1_ad_allow_time;
+ double chest_2_ad_allow_time;
+ double chest_3_ad_allow_time;
int gold_pass{};
- static const char* kwlist[] = {"tickets_text",
- "tokens_text",
- "league_rank_text",
+ static const char* kwlist[] = {"tickets",
+ "tokens",
+ "league_rank",
"league_type",
"achievements_percent_text",
"level_text",
@@ -341,8 +340,8 @@ static auto PySetRootUIAccountValues(PyObject* self, PyObject* args,
"chest_3_ad_allow_time",
nullptr};
if (!PyArg_ParseTupleAndKeywords(
- args, keywds, "sssssssspssssffffffff", const_cast(kwlist),
- &tickets_text, &tokens_text, &league_rank_text, &league_type,
+ args, keywds, "iiissssspssssdddddddd", const_cast(kwlist),
+ &tickets, &tokens, &league_rank, &league_type,
&achievements_percent_text, &level_text, &xp_text, &inbox_count_text,
&gold_pass, &chest_0_appearance, &chest_1_appearance,
&chest_2_appearance, &chest_3_appearance, &chest_0_unlock_time,
@@ -357,20 +356,21 @@ static auto PySetRootUIAccountValues(PyObject* self, PyObject* args,
// Pass these all along to the app-mode which will store them and forward
// them to any existing UI.
- appmode->SetRootUITicketsMeterText(tickets_text);
- appmode->SetRootUITokensMeterText(tokens_text);
- appmode->SetRootUILeagueRankText(league_rank_text);
+ appmode->SetRootUITicketsMeterValue(tickets);
+ appmode->SetRootUITokensMeterValue(tokens);
+ appmode->SetRootUILeagueRankValue(league_rank);
appmode->SetRootUILeagueType(league_type);
appmode->SetRootUIAchievementsPercentText(achievements_percent_text);
appmode->SetRootUILevelText(level_text);
appmode->SetRootUIXPText(xp_text);
appmode->SetRootUIInboxCountText(inbox_count_text);
appmode->SetRootUIGoldPass(gold_pass);
- appmode->SetRootUIChests(chest_0_appearance, chest_1_appearance,
- chest_2_appearance, chest_3_appearance);
+ appmode->SetRootUIChests(
+ chest_0_appearance, chest_1_appearance, chest_2_appearance,
+ chest_3_appearance, chest_0_unlock_time, chest_1_unlock_time,
+ chest_2_unlock_time, chest_3_unlock_time, chest_0_ad_allow_time,
+ chest_1_ad_allow_time, chest_2_ad_allow_time, chest_3_ad_allow_time);
- printf("WOULD SET TIMES TO %.2f %.2f\n", chest_0_unlock_time,
- chest_0_ad_allow_time);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
@@ -381,9 +381,9 @@ static PyMethodDef PySetRootUIAccountValuesDef = {
METH_VARARGS | METH_KEYWORDS, // flags
"set_root_ui_account_values(*,\n"
- " tickets_text: str,\n"
- " tokens_text: str,\n"
- " league_rank_text: str,\n"
+ " tickets: int,\n"
+ " tokens: int,\n"
+ " league_rank: int,\n"
" league_type: str,\n"
" achievements_percent_text: str,\n"
" level_text: str,\n"
diff --git a/src/ballistica/classic/support/classic_app_mode.cc b/src/ballistica/classic/support/classic_app_mode.cc
index eb261a57..8c50e5ee 100644
--- a/src/ballistica/classic/support/classic_app_mode.cc
+++ b/src/ballistica/classic/support/classic_app_mode.cc
@@ -149,10 +149,10 @@ void ClassicAppMode::Reset_() {
// At this point uiv1 is in a reset-to-default state. Now plug in our
// current values for everything.
if (auto* root_widget = uiv1_->root_widget()) {
- root_widget->SetTicketsMeterText(root_ui_tickets_meter_text_);
- root_widget->SetTokensMeterText(root_ui_tokens_meter_text_,
- root_ui_gold_pass_);
- root_widget->SetLeagueRankText(root_ui_league_rank_text_);
+ root_widget->SetTicketsMeterValue(root_ui_tickets_meter_value_);
+ root_widget->SetTokensMeterValue(root_ui_tokens_meter_value_,
+ root_ui_gold_pass_);
+ root_widget->SetLeagueRankValue(root_ui_league_rank_value_);
root_widget->SetLeagueType(root_ui_league_type_);
root_widget->SetAchievementPercentText(root_ui_achievement_percent_text_);
root_widget->SetLevelText(root_ui_level_text_);
@@ -160,7 +160,11 @@ void ClassicAppMode::Reset_() {
root_widget->SetInboxCountText(root_ui_inbox_count_text_);
root_widget->SetChests(
root_ui_chest_0_appearance_, root_ui_chest_1_appearance_,
- root_ui_chest_2_appearance_, root_ui_chest_3_appearance_);
+ root_ui_chest_2_appearance_, root_ui_chest_3_appearance_,
+ root_ui_chest_0_unlock_time_, root_ui_chest_1_unlock_time_,
+ root_ui_chest_2_unlock_time_, root_ui_chest_3_unlock_time_,
+ root_ui_chest_0_ad_allow_time_, root_ui_chest_1_ad_allow_time_,
+ root_ui_chest_2_ad_allow_time_, root_ui_chest_3_ad_allow_time_);
root_widget->SetHaveLiveValues(root_ui_have_live_values_);
}
}
@@ -332,7 +336,7 @@ void ClassicAppMode::HostScanCycle() {
&((reinterpret_cast(&from))->sin_addr),
buffer2, sizeof(buffer2));
entry.last_query_id = query_id;
- entry.last_contact_time = g_core->GetAppTimeMillisecs();
+ entry.last_contact_time = g_core->AppTimeMillisecs();
}
}
PruneScanResults_();
@@ -357,7 +361,7 @@ void ClassicAppMode::EndHostScanning() {
}
void ClassicAppMode::PruneScanResults_() {
- millisecs_t t = g_core->GetAppTimeMillisecs();
+ millisecs_t t = g_core->AppTimeMillisecs();
auto i = scan_results_.begin();
while (i != scan_results_.end()) {
auto i_next = i;
@@ -516,8 +520,8 @@ auto ClassicAppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t {
void ClassicAppMode::StepDisplayTime() {
assert(g_base->InLogicThread());
- auto startms{core::CorePlatform::GetCurrentMillisecs()};
- millisecs_t app_time = g_core->GetAppTimeMillisecs();
+ auto startms{core::CorePlatform::TimeMonotonicMillisecs()};
+ millisecs_t app_time = g_core->AppTimeMillisecs();
g_core->platform->SetDebugKey("LastUpdateTime", std::to_string(startms));
in_update_ = true;
@@ -592,7 +596,7 @@ void ClassicAppMode::StepDisplayTime() {
// Report excessively long updates.
if (g_core->core_config().debug_timing
&& app_time >= next_long_update_report_time_) {
- auto duration{core::CorePlatform::GetCurrentMillisecs() - startms};
+ auto duration{core::CorePlatform::TimeMonotonicMillisecs() - startms};
// Complain when our full update takes longer than 1/60th second.
if (duration > (1000 / 60)) {
@@ -762,7 +766,7 @@ void ClassicAppMode::UpdateKickVote_() {
kick_vote_in_progress_ = false;
return;
}
- millisecs_t current_time{g_core->GetAppTimeMillisecs()};
+ millisecs_t current_time{g_core->AppTimeMillisecs()};
int total_client_count = 0;
int yes_votes = 0;
int no_votes = 0;
@@ -859,7 +863,7 @@ void ClassicAppMode::UpdateKickVote_() {
void ClassicAppMode::StartKickVote(scene_v1::ConnectionToClient* starter,
scene_v1::ConnectionToClient* target) {
// Restrict votes per client.
- millisecs_t current_time = g_core->GetAppTimeMillisecs();
+ millisecs_t current_time = g_core->AppTimeMillisecs();
if (starter == target) {
// Don't let anyone kick themselves.
@@ -1413,7 +1417,7 @@ auto ClassicAppMode::ShouldAnnouncePartyJoinsAndLeaves() -> bool {
}
auto ClassicAppMode::IsPlayerBanned(const scene_v1::PlayerSpec& spec) -> bool {
- millisecs_t current_time = g_core->GetAppTimeMillisecs();
+ millisecs_t current_time = g_core->AppTimeMillisecs();
// Now is a good time to prune no-longer-banned specs.
while (!banned_players_.empty()
@@ -1431,7 +1435,7 @@ auto ClassicAppMode::IsPlayerBanned(const scene_v1::PlayerSpec& spec) -> bool {
void ClassicAppMode::BanPlayer(const scene_v1::PlayerSpec& spec,
millisecs_t duration) {
- banned_players_.emplace_back(g_core->GetAppTimeMillisecs() + duration, spec);
+ banned_players_.emplace_back(g_core->AppTimeMillisecs() + duration, spec);
}
void ClassicAppMode::HandleQuitOnIdle_() {
@@ -1550,51 +1554,51 @@ void ClassicAppMode::RunMainMenu() {
}
}
-void ClassicAppMode::SetRootUITicketsMeterText(const std::string text) {
+void ClassicAppMode::SetRootUITicketsMeterValue(int value) {
BA_PRECONDITION(g_base->InLogicThread());
- if (text == root_ui_tickets_meter_text_) {
+ if (value == root_ui_tickets_meter_value_) {
return;
}
// Store the value.
- root_ui_tickets_meter_text_ = text;
+ root_ui_tickets_meter_value_ = value;
// Apply it to any existing UI.
if (uiv1_) {
if (auto* root_widget = uiv1_->root_widget()) {
- root_widget->SetTicketsMeterText(root_ui_tickets_meter_text_);
+ root_widget->SetTicketsMeterValue(root_ui_tickets_meter_value_);
}
}
}
-void ClassicAppMode::SetRootUITokensMeterText(const std::string text) {
+void ClassicAppMode::SetRootUITokensMeterValue(int value) {
BA_PRECONDITION(g_base->InLogicThread());
- if (text == root_ui_tokens_meter_text_) {
+ if (value == root_ui_tokens_meter_value_) {
return;
}
// Store the value.
- root_ui_tokens_meter_text_ = text;
+ root_ui_tokens_meter_value_ = value;
// Apply it to any existing UI.
if (uiv1_) {
if (auto* root_widget = uiv1_->root_widget()) {
- root_widget->SetTokensMeterText(root_ui_tokens_meter_text_,
- root_ui_gold_pass_);
+ root_widget->SetTokensMeterValue(root_ui_tokens_meter_value_,
+ root_ui_gold_pass_);
}
}
}
-void ClassicAppMode::SetRootUILeagueRankText(const std::string text) {
+void ClassicAppMode::SetRootUILeagueRankValue(int value) {
BA_PRECONDITION(g_base->InLogicThread());
- if (text == root_ui_league_rank_text_) {
+ if (value == root_ui_league_rank_value_) {
return;
}
// Store the value.
- root_ui_league_rank_text_ = text;
+ root_ui_league_rank_value_ = value;
// Apply it to any existing UI.
if (uiv1_) {
if (auto* root_widget = uiv1_->root_widget()) {
- root_widget->SetLeagueRankText(root_ui_league_rank_text_);
+ root_widget->SetLeagueRankValue(root_ui_league_rank_value_);
}
}
}
@@ -1694,8 +1698,8 @@ void ClassicAppMode::SetRootUIGoldPass(bool enabled) {
// Apply it to any existing UI.
if (uiv1_) {
if (auto* root_widget = uiv1_->root_widget()) {
- root_widget->SetTokensMeterText(root_ui_tokens_meter_text_,
- root_ui_gold_pass_);
+ root_widget->SetTokensMeterValue(root_ui_tokens_meter_value_,
+ root_ui_gold_pass_);
}
}
}
@@ -1716,15 +1720,28 @@ void ClassicAppMode::SetRootUIHaveLiveValues(bool have_live_values) {
}
}
-void ClassicAppMode::SetRootUIChests(const std::string& chest_0_appearance,
- const std::string& chest_1_appearance,
- const std::string& chest_2_appearance,
- const std::string& chest_3_appearance) {
+void ClassicAppMode::SetRootUIChests(
+ const std::string& chest_0_appearance,
+ const std::string& chest_1_appearance,
+ const std::string& chest_2_appearance,
+ const std::string& chest_3_appearance, seconds_t chest_0_unlock_time,
+ seconds_t chest_1_unlock_time, seconds_t chest_2_unlock_time,
+ seconds_t chest_3_unlock_time, seconds_t chest_0_ad_allow_time,
+ seconds_t chest_1_ad_allow_time, seconds_t chest_2_ad_allow_time,
+ seconds_t chest_3_ad_allow_time) {
BA_PRECONDITION(g_base->InLogicThread());
if (chest_0_appearance == root_ui_chest_0_appearance_
&& chest_1_appearance == root_ui_chest_1_appearance_
&& chest_2_appearance == root_ui_chest_2_appearance_
- && chest_3_appearance == root_ui_chest_3_appearance_) {
+ && chest_3_appearance == root_ui_chest_3_appearance_
+ && chest_0_unlock_time == root_ui_chest_0_unlock_time_
+ && chest_1_unlock_time == root_ui_chest_1_unlock_time_
+ && chest_2_unlock_time == root_ui_chest_2_unlock_time_
+ && chest_3_unlock_time == root_ui_chest_3_unlock_time_
+ && chest_0_ad_allow_time == root_ui_chest_0_ad_allow_time_
+ && chest_1_ad_allow_time == root_ui_chest_1_ad_allow_time_
+ && chest_2_ad_allow_time == root_ui_chest_2_ad_allow_time_
+ && chest_3_ad_allow_time == root_ui_chest_3_ad_allow_time_) {
return;
}
@@ -1733,13 +1750,25 @@ void ClassicAppMode::SetRootUIChests(const std::string& chest_0_appearance,
root_ui_chest_1_appearance_ = chest_1_appearance;
root_ui_chest_2_appearance_ = chest_2_appearance;
root_ui_chest_3_appearance_ = chest_3_appearance;
+ root_ui_chest_0_unlock_time_ = chest_0_unlock_time;
+ root_ui_chest_1_unlock_time_ = chest_1_unlock_time;
+ root_ui_chest_2_unlock_time_ = chest_2_unlock_time;
+ root_ui_chest_3_unlock_time_ = chest_3_unlock_time;
+ root_ui_chest_0_ad_allow_time_ = chest_0_ad_allow_time;
+ root_ui_chest_1_ad_allow_time_ = chest_1_ad_allow_time;
+ root_ui_chest_2_ad_allow_time_ = chest_2_ad_allow_time;
+ root_ui_chest_3_ad_allow_time_ = chest_3_ad_allow_time;
// Apply it to any existing UI.
if (uiv1_) {
if (auto* root_widget = uiv1_->root_widget()) {
root_widget->SetChests(
root_ui_chest_0_appearance_, root_ui_chest_1_appearance_,
- root_ui_chest_2_appearance_, root_ui_chest_3_appearance_);
+ root_ui_chest_2_appearance_, root_ui_chest_3_appearance_,
+ root_ui_chest_0_unlock_time_, root_ui_chest_1_unlock_time_,
+ root_ui_chest_2_unlock_time_, root_ui_chest_3_unlock_time_,
+ root_ui_chest_0_ad_allow_time_, root_ui_chest_1_ad_allow_time_,
+ root_ui_chest_2_ad_allow_time_, root_ui_chest_3_ad_allow_time_);
}
}
}
diff --git a/src/ballistica/classic/support/classic_app_mode.h b/src/ballistica/classic/support/classic_app_mode.h
index edfaafe0..153a57e2 100644
--- a/src/ballistica/classic/support/classic_app_mode.h
+++ b/src/ballistica/classic/support/classic_app_mode.h
@@ -215,19 +215,24 @@ class ClassicAppMode : public base::AppMode {
public_party_public_address_ipv6_ = val;
}
- void SetRootUITicketsMeterText(const std::string text);
- void SetRootUITokensMeterText(const std::string text);
- void SetRootUILeagueRankText(const std::string text);
+ void SetRootUITicketsMeterValue(int value);
+ void SetRootUITokensMeterValue(int value);
+ void SetRootUILeagueRankValue(int value);
void SetRootUILeagueType(const std::string text);
void SetRootUIAchievementsPercentText(const std::string text);
void SetRootUILevelText(const std::string text);
void SetRootUIXPText(const std::string text);
void SetRootUIInboxCountText(const std::string text);
void SetRootUIGoldPass(bool enabled);
- void SetRootUIChests(const std::string& chest_0_appearance,
- const std::string& chest_1_appearance,
- const std::string& chest_2_appearance,
- const std::string& chest_3_appearance);
+ void SetRootUIChests(
+ const std::string& chest_0_appearance,
+ const std::string& chest_1_appearance,
+ const std::string& chest_2_appearance,
+ const std::string& chest_3_appearance, seconds_t chest_0_unlock_time,
+ seconds_t chest_1_unlock_time, seconds_t chest_2_unlock_time,
+ seconds_t chest_3_unlock_time, seconds_t chest_0_ad_allow_time,
+ seconds_t chest_1_ad_allow_time, seconds_t chest_2_ad_allow_time,
+ seconds_t chest_3_ad_allow_time);
void SetRootUIHaveLiveValues(bool val);
private:
@@ -250,6 +255,15 @@ class ClassicAppMode : public base::AppMode {
std::string root_ui_chest_1_appearance_;
std::string root_ui_chest_2_appearance_;
std::string root_ui_chest_3_appearance_;
+ seconds_t root_ui_chest_0_unlock_time_;
+ seconds_t root_ui_chest_1_unlock_time_;
+ seconds_t root_ui_chest_2_unlock_time_;
+ seconds_t root_ui_chest_3_unlock_time_;
+ seconds_t root_ui_chest_0_ad_allow_time_;
+ seconds_t root_ui_chest_1_ad_allow_time_;
+ seconds_t root_ui_chest_2_ad_allow_time_;
+ seconds_t root_ui_chest_3_ad_allow_time_;
+
uint32_t next_scan_query_id_{};
int scan_socket_{-1};
int host_protocol_version_{-1};
@@ -301,6 +315,9 @@ class ClassicAppMode : public base::AppMode {
int public_party_max_size_{8};
int public_party_player_count_{0};
int public_party_max_player_count_{8};
+ int root_ui_tickets_meter_value_;
+ int root_ui_tokens_meter_value_;
+ int root_ui_league_rank_value_;
float debug_speed_mult_{1.0f};
float replay_speed_mult_{1.0f};
std::set admin_public_ids_;
@@ -308,9 +325,6 @@ class ClassicAppMode : public base::AppMode {
std::string public_party_name_;
std::string public_party_min_league_;
std::string public_party_stats_url_;
- std::string root_ui_tickets_meter_text_;
- std::string root_ui_tokens_meter_text_;
- std::string root_ui_league_rank_text_;
std::string root_ui_league_type_;
std::string root_ui_achievement_percent_text_;
std::string root_ui_level_text_;
diff --git a/src/ballistica/classic/support/stress_test.cc b/src/ballistica/classic/support/stress_test.cc
index 5441578d..e49b29b4 100644
--- a/src/ballistica/classic/support/stress_test.cc
+++ b/src/ballistica/classic/support/stress_test.cc
@@ -53,7 +53,7 @@ void StressTest::ProcessInputs(int player_count) {
assert(g_base->InLogicThread());
assert(player_count >= 0);
- millisecs_t time = g_core->GetAppTimeMillisecs();
+ millisecs_t time = g_core->AppTimeMillisecs();
// FIXME: If we don't check for stress_test_last_leave_time_ we totally
// confuse the game.. need to be able to survive that.
diff --git a/src/ballistica/core/core.cc b/src/ballistica/core/core.cc
index d8b41e5d..55b3764f 100644
--- a/src/ballistica/core/core.cc
+++ b/src/ballistica/core/core.cc
@@ -72,7 +72,7 @@ auto CoreFeatureSet::Import(const CoreConfig* config) -> CoreFeatureSet* {
}
void CoreFeatureSet::DoImport_(const CoreConfig& config) {
- millisecs_t start_millisecs = CorePlatform::GetCurrentMillisecs();
+ millisecs_t start_millisecs = CorePlatform::TimeMonotonicMillisecs();
assert(g_core == nullptr);
g_core = new CoreFeatureSet(config);
@@ -87,7 +87,7 @@ CoreFeatureSet::CoreFeatureSet(CoreConfig config)
python{new CorePython()},
platform{CorePlatform::Create()},
core_config_{std::move(config)},
- last_app_time_measure_microsecs_{CorePlatform::GetCurrentMicrosecs()},
+ last_app_time_measure_microsecs_{CorePlatform::TimeMonotonicMicrosecs()},
vr_mode_{config.vr_mode} {
// We're a singleton. If there's already one of us, something's wrong.
assert(g_core == nullptr);
@@ -313,7 +313,7 @@ auto CoreFeatureSet::SoftImportBase() -> BaseSoftInterface* {
// // We include time-since-start as part of the message here.
// char buffer[128];
// snprintf(buffer, sizeof(buffer), "%s @ %.3fs.", msg,
-// g_core->GetAppTimeSeconds() + offset_seconds);
+// g_core->AppTimeSeconds() + offset_seconds);
// Log(LogName::kBaLifecycle, LogLevel::kDebug, buffer);
// } else {
// Log(LogName::kBaLifecycle, LogLevel::kDebug, msg);
@@ -355,23 +355,23 @@ static void WaitThenDie(millisecs_t wait, const std::string& action) {
FatalError("Timed out waiting for " + action + ".");
}
-auto CoreFeatureSet::GetAppTimeMillisecs() -> millisecs_t {
+auto CoreFeatureSet::AppTimeMillisecs() -> millisecs_t {
UpdateAppTime_();
return app_time_microsecs_ / 1000;
}
-auto CoreFeatureSet::GetAppTimeMicrosecs() -> microsecs_t {
+auto CoreFeatureSet::AppTimeMicrosecs() -> microsecs_t {
UpdateAppTime_();
return app_time_microsecs_;
}
-auto CoreFeatureSet::GetAppTimeSeconds() -> seconds_t {
+auto CoreFeatureSet::AppTimeSeconds() -> seconds_t {
UpdateAppTime_();
return static_cast(app_time_microsecs_) / 1000000;
}
void CoreFeatureSet::UpdateAppTime_() {
- microsecs_t t = CorePlatform::GetCurrentMicrosecs();
+ microsecs_t t = CorePlatform::TimeMonotonicMicrosecs();
// If we're at a different time than our last query, do our funky math.
if (t != last_app_time_measure_microsecs_) {
diff --git a/src/ballistica/core/core.h b/src/ballistica/core/core.h
index 7ba9e3f2..c66d81d5 100644
--- a/src/ballistica/core/core.h
+++ b/src/ballistica/core/core.h
@@ -73,21 +73,21 @@ class CoreFeatureSet {
/// App-time is basically the total time that the engine has been actively
/// running. (The 'App' here is a slight misnomer). It will stop
/// progressing while the app is suspended and will never go backwards.
- auto GetAppTimeMillisecs() -> millisecs_t;
+ auto AppTimeMillisecs() -> millisecs_t;
/// Return current app-time in microseconds.
///
/// App-time is basically the total time that the engine has been actively
/// running. (The 'App' here is a slight misnomer). It will stop
/// progressing while the app is suspended and will never go backwards.
- auto GetAppTimeMicrosecs() -> microsecs_t;
+ auto AppTimeMicrosecs() -> microsecs_t;
/// Return current app-time in seconds.
///
/// App-time is basically the total time that the engine has been actively
/// running. (The 'App' here is a slight misnomer). It will stop
/// progressing while the app is suspended and will never go backwards.
- auto GetAppTimeSeconds() -> seconds_t;
+ auto AppTimeSeconds() -> seconds_t;
/// Are we in the 'main' thread? The thread that first inited Core is
/// considered the 'main' thread; on most platforms it is the one where
diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc
index abb58fe7..d58de8a6 100644
--- a/src/ballistica/core/platform/core_platform.cc
+++ b/src/ballistica/core/platform/core_platform.cc
@@ -116,7 +116,8 @@ void CorePlatform::LowLevelDebugLog(const std::string& msg) {
HandleLowLevelDebugLog(msg);
}
-CorePlatform::CorePlatform() : start_time_millisecs_(GetCurrentMillisecs()) {}
+CorePlatform::CorePlatform()
+ : start_time_millisecs_(TimeMonotonicMillisecs()) {}
void CorePlatform::PostInit() {
// Hmm; we seem to get some funky invalid utf8 out of
@@ -960,8 +961,8 @@ auto CorePlatform::SetSocketNonBlocking(int sd) -> bool {
#endif
}
-auto CorePlatform::GetTicks() const -> millisecs_t {
- return GetCurrentMillisecs() - start_time_millisecs_;
+auto CorePlatform::TimeSinceLaunchMillisecs() const -> millisecs_t {
+ return TimeMonotonicMillisecs() - start_time_millisecs_;
}
auto CorePlatform::GetPlatformName() -> std::string {
@@ -1072,27 +1073,27 @@ void CorePlatform::SetDebugKey(const std::string& key,
void CorePlatform::HandleLowLevelDebugLog(const std::string& msg) {}
-auto CorePlatform::GetCurrentMillisecs() -> millisecs_t {
+auto CorePlatform::TimeMonotonicMillisecs() -> millisecs_t {
return std::chrono::time_point_cast(
std::chrono::steady_clock::now())
.time_since_epoch()
.count();
}
-auto CorePlatform::GetCurrentMicrosecs() -> millisecs_t {
+auto CorePlatform::TimeMonotonicMicrosecs() -> millisecs_t {
return std::chrono::time_point_cast(
std::chrono::steady_clock::now())
.time_since_epoch()
.count();
}
-auto CorePlatform::GetSecondsSinceEpoch() -> double {
+auto CorePlatform::TimeSinceEpochSeconds() -> double {
return std::chrono::duration(
std::chrono::system_clock::now().time_since_epoch())
.count();
}
-auto CorePlatform::GetCurrentWholeSeconds() -> int64_t {
+auto CorePlatform::TimeMonotonicWholeSeconds() -> int64_t {
return std::chrono::time_point_cast(
std::chrono::steady_clock::now())
.time_since_epoch()
diff --git a/src/ballistica/core/platform/core_platform.h b/src/ballistica/core/platform/core_platform.h
index a1b2f5f5..51b8b1d0 100644
--- a/src/ballistica/core/platform/core_platform.h
+++ b/src/ballistica/core/platform/core_platform.h
@@ -346,31 +346,31 @@ class CorePlatform {
/// monotonic. For most purposes, AppTime values are preferable since
/// their progression pauses during app suspension and they are 100%
/// guaranteed to not go backwards.
- auto GetTicks() const -> millisecs_t;
+ auto TimeSinceLaunchMillisecs() const -> millisecs_t;
/// Return a raw current milliseconds value. It *should* be monotonic. It
/// is relative to an undefined start point; only use it for time
/// differences. Generally the AppTime values are preferable since their
/// progression pauses during app suspension and they are 100% guaranteed
/// to not go backwards.
- static auto GetCurrentMillisecs() -> millisecs_t;
+ static auto TimeMonotonicMillisecs() -> millisecs_t;
/// Return a raw current microseconds value. It *should* be monotonic. It
/// is relative to an undefined start point; only use it for time
/// differences. Generally the AppTime values are preferable since their
/// progression pauses during app suspension and they are 100% guaranteed
/// to not go backwards.
- static auto GetCurrentMicrosecs() -> microsecs_t;
+ static auto TimeMonotonicMicrosecs() -> microsecs_t;
/// Return a raw current seconds integer value. It *should* be monotonic.
/// It is relative to an undefined start point; only use it for time
/// differences. Generally the AppTime values are preferable since their
/// progression pauses during app suspension and they are 100% guaranteed
/// to not go backwards.
- static auto GetCurrentWholeSeconds() -> int64_t;
+ static auto TimeMonotonicWholeSeconds() -> int64_t;
/// Return seconds since the epoch; same as Python's time.time().
- static auto GetSecondsSinceEpoch() -> double;
+ static auto TimeSinceEpochSeconds() -> double;
static void SleepSeconds(seconds_t duration);
static void SleepMillisecs(millisecs_t duration);
diff --git a/src/ballistica/core/support/core_config.h b/src/ballistica/core/support/core_config.h
index 1ccfef09..d7b93030 100644
--- a/src/ballistica/core/support/core_config.h
+++ b/src/ballistica/core/support/core_config.h
@@ -10,8 +10,8 @@
namespace ballistica::core {
-/// Collection of low level options for a run of the engine; passed
-/// when initing the core feature-set.
+/// A collection of low level options for a run of the engine; passed when
+/// initing the core feature-set.
class CoreConfig {
public:
static auto ForArgsAndEnvVars(int argc, char** argv) -> CoreConfig;
diff --git a/src/ballistica/scene_v1/connection/connection.cc b/src/ballistica/scene_v1/connection/connection.cc
index 54863869..8de0859c 100644
--- a/src/ballistica/scene_v1/connection/connection.cc
+++ b/src/ballistica/scene_v1/connection/connection.cc
@@ -34,7 +34,7 @@ const int kPingMeasureInterval = 2000;
Connection::Connection() {
// NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
- creation_time_ = last_average_update_time_ = g_core->GetAppTimeMillisecs();
+ creation_time_ = last_average_update_time_ = g_core->AppTimeMillisecs();
}
void Connection::ProcessWaitingMessages() {
@@ -181,13 +181,13 @@ void Connection::HandleGamePacket(const std::vector& data) {
"Error: got invalid BA_SCENEPACKET_KEEPALIVE packet.");
return;
}
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
HandleResends(real_time, data, 1);
break;
}
case BA_SCENEPACKET_MESSAGE: {
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
// Expect 1 byte type, 2 byte num, 3 byte acks, at least 1 byte payload.
if (data.size() < 7) {
@@ -211,7 +211,7 @@ void Connection::HandleGamePacket(const std::vector& data) {
ReliableMessageIn& msg(in_messages_[num]);
msg.data.resize(data.size() - 6);
memcpy(&(msg.data[0]), &(data[6]), msg.data.size());
- msg.arrival_time = g_core->GetAppTimeMillisecs();
+ msg.arrival_time = g_core->AppTimeMillisecs();
// Now run all in-order packets we've got.
ProcessWaitingMessages();
@@ -308,7 +308,7 @@ void Connection::SendReliableMessage(const std::vector& data) {
assert(out_messages_.find(num) == out_messages_.end());
ReliableMessageOut& msg(out_messages_[num]);
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
msg.data = data;
msg.first_send_time = msg.last_send_time = real_time;
@@ -341,7 +341,7 @@ void Connection::SendUnreliableMessage(const std::vector& data) {
}
uint16_t num = next_out_unreliable_message_num_++;
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
// Add our header/acks and go ahead and send this one out.
// 1 byte for type, 2 for packet-num, 2 for unreliable packet-num, 3 for acks.
@@ -367,7 +367,7 @@ void Connection::SendJMessage(cJSON* val) {
}
void Connection::Update() {
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
// Update our averages once per second.
while (real_time - last_average_update_time_ > 1000) {
diff --git a/src/ballistica/scene_v1/connection/connection_to_client.cc b/src/ballistica/scene_v1/connection/connection_to_client.cc
index 64682c83..85dfda96 100644
--- a/src/ballistica/scene_v1/connection/connection_to_client.cc
+++ b/src/ballistica/scene_v1/connection/connection_to_client.cc
@@ -95,7 +95,7 @@ ConnectionToClient::~ConnectionToClient() {
void ConnectionToClient::Update() {
Connection::Update(); // Handles common stuff.
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
// If we're waiting for handshake response still, keep sending out handshake
// attempts.
@@ -246,7 +246,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) {
// Don't allow fresh clients to start kick votes for a while.
next_kick_vote_allow_time_ =
- g_core->GetAppTimeMillisecs() + kNewClientKickVoteDelay;
+ g_core->AppTimeMillisecs() + kNewClientKickVoteDelay;
// At this point we have their name, so lets announce their arrival.
if (appmode->ShouldAnnouncePartyJoinsAndLeaves()) {
@@ -263,7 +263,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) {
// Also mark the time for flashing the 'someone just joined your
// party' message in the corner.
appmode->set_last_connection_to_client_join_time(
- g_core->GetAppTimeMillisecs());
+ g_core->AppTimeMillisecs());
// Added midway through protocol 29:
// We now send a json dict of info about ourself first thing. This
@@ -338,8 +338,7 @@ void ConnectionToClient::SendScreenMessage(const std::string& s, float r,
// Older clients don't support the screen-message message, so in that case
// we just send it as a chat-message from .
if (build_number() < 14248) {
- std::string value =
- g_base->assets->CompileResourceString(s, "sendScreenMessage");
+ std::string value = g_base->assets->CompileResourceString(s);
std::string our_spec_string =
PlayerSpec::GetDummyPlayerSpec("").GetSpecString();
std::vector msg_out(1 + 1 + our_spec_string.size() + value.size());
@@ -500,7 +499,7 @@ void ConnectionToClient::HandleMessagePacket(
case BA_MESSAGE_CHAT: {
// We got a chat message from a client.
- millisecs_t now = g_core->GetAppTimeMillisecs();
+ millisecs_t now = g_core->AppTimeMillisecs();
// Ignore this if they're chat blocked.
if (now >= chat_block_time_) {
@@ -636,7 +635,7 @@ void ConnectionToClient::HandleMessagePacket(
}
case BA_MESSAGE_REMOVE_REMOTE_PLAYER: {
- last_remove_player_time_ = g_core->GetAppTimeMillisecs();
+ last_remove_player_time_ = g_core->AppTimeMillisecs();
if (buffer.size() != 2) {
g_core->Log(LogName::kBaNetworking, LogLevel::kError,
"Error: invalid remove-remote-player packet");
@@ -693,7 +692,7 @@ void ConnectionToClient::HandleMessagePacket(
// master-server info for this client, delay their join (we'll
// eventually give up and just give them a blank slate).
if (still_waiting_for_auth
- && (g_core->GetAppTimeMillisecs() - creation_time() < 10000)) {
+ && (g_core->AppTimeMillisecs() - creation_time() < 10000)) {
SendScreenMessage(
"{\"v\":\"${A}...\",\"s\":[[\"${A}\",{\"r\":"
"\"loadingTryAgainText\",\"f\":\"loadingText\"}]]}",
diff --git a/src/ballistica/scene_v1/connection/connection_to_host.cc b/src/ballistica/scene_v1/connection/connection_to_host.cc
index 877e4b7d..462e981d 100644
--- a/src/ballistica/scene_v1/connection/connection_to_host.cc
+++ b/src/ballistica/scene_v1/connection/connection_to_host.cc
@@ -58,7 +58,7 @@ ConnectionToHost::~ConnectionToHost() {
}
void ConnectionToHost::Update() {
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
// Send out null messages occasionally for ping measurement purposes.
// Note that we currently only do this from the client since we might not
diff --git a/src/ballistica/scene_v1/dynamics/dynamics.cc b/src/ballistica/scene_v1/dynamics/dynamics.cc
index b545aea6..96f4bc77 100644
--- a/src/ballistica/scene_v1/dynamics/dynamics.cc
+++ b/src/ballistica/scene_v1/dynamics/dynamics.cc
@@ -514,7 +514,7 @@ void Dynamics::ProcessCollision_() {
void Dynamics::Process() {
in_process_ = true;
// Update this once so we can recycle results.
- real_time_ = g_core->GetAppTimeMillisecs();
+ real_time_ = g_core->AppTimeMillisecs();
ProcessCollision_();
dWorldQuickStep(ode_world_, kGameStepSeconds);
dJointGroupEmpty(ode_contact_group_);
diff --git a/src/ballistica/scene_v1/node/globals_node.cc b/src/ballistica/scene_v1/node/globals_node.cc
index a167c1de..40a549cd 100644
--- a/src/ballistica/scene_v1/node/globals_node.cc
+++ b/src/ballistica/scene_v1/node/globals_node.cc
@@ -27,7 +27,7 @@ class GlobalsNodeType : public NodeType {
public:
#define BA_NODE_TYPE_CLASS GlobalsNode
BA_NODE_CREATE_CALL(CreateGlobals);
- BA_INT64_ATTR_READONLY(real_time, GetAppTimeMillisecs);
+ BA_INT64_ATTR_READONLY(real_time, AppTimeMillisecs);
BA_INT64_ATTR_READONLY(time, GetTime);
BA_INT64_ATTR_READONLY(step, GetStep);
BA_FLOAT_ATTR(debris_friction, debris_friction, SetDebrisFriction);
@@ -206,7 +206,7 @@ auto GlobalsNode::IsCurrentGlobals() const -> bool {
&& scene->globals_node() == this);
}
-auto GlobalsNode::GetAppTimeMillisecs() -> millisecs_t {
+auto GlobalsNode::AppTimeMillisecs() -> millisecs_t {
// Pull this from our scene so we return consistent values throughout a step.
return scene()->last_step_real_time();
}
diff --git a/src/ballistica/scene_v1/node/globals_node.h b/src/ballistica/scene_v1/node/globals_node.h
index b1d487e8..e0e486bd 100644
--- a/src/ballistica/scene_v1/node/globals_node.h
+++ b/src/ballistica/scene_v1/node/globals_node.h
@@ -17,7 +17,7 @@ class GlobalsNode : public Node {
~GlobalsNode() override;
void SetAsForeground();
auto IsCurrentGlobals() const -> bool;
- auto GetAppTimeMillisecs() -> millisecs_t;
+ auto AppTimeMillisecs() -> millisecs_t;
auto GetTime() -> millisecs_t;
auto GetStep() -> int64_t;
auto debris_friction() const -> float { return debris_friction_; }
diff --git a/src/ballistica/scene_v1/node/session_globals_node.cc b/src/ballistica/scene_v1/node/session_globals_node.cc
index 7479632b..f577ef61 100644
--- a/src/ballistica/scene_v1/node/session_globals_node.cc
+++ b/src/ballistica/scene_v1/node/session_globals_node.cc
@@ -12,7 +12,7 @@ class SessionGlobalsNodeType : public NodeType {
public:
#define BA_NODE_TYPE_CLASS SessionGlobalsNode
BA_NODE_CREATE_CALL(CreateSessionGlobals);
- BA_INT64_ATTR_READONLY(real_time, GetAppTimeMillisecs);
+ BA_INT64_ATTR_READONLY(real_time, AppTimeMillisecs);
BA_INT64_ATTR_READONLY(time, GetTime);
BA_INT64_ATTR_READONLY(step, GetStep);
#undef BA_NODE_TYPE_CLASS
@@ -38,7 +38,7 @@ SessionGlobalsNode::SessionGlobalsNode(Scene* scene) : Node(scene, node_type) {
SessionGlobalsNode::~SessionGlobalsNode() = default;
-auto SessionGlobalsNode::GetAppTimeMillisecs() -> millisecs_t {
+auto SessionGlobalsNode::AppTimeMillisecs() -> millisecs_t {
// Pull this from our scene so we return consistent values throughout a step.
return scene()->last_step_real_time();
}
diff --git a/src/ballistica/scene_v1/node/session_globals_node.h b/src/ballistica/scene_v1/node/session_globals_node.h
index dcc07552..01ae47b3 100644
--- a/src/ballistica/scene_v1/node/session_globals_node.h
+++ b/src/ballistica/scene_v1/node/session_globals_node.h
@@ -12,7 +12,7 @@ class SessionGlobalsNode : public Node {
static auto InitType() -> NodeType*;
explicit SessionGlobalsNode(Scene* scene);
~SessionGlobalsNode() override;
- auto GetAppTimeMillisecs() -> millisecs_t;
+ auto AppTimeMillisecs() -> millisecs_t;
auto GetTime() -> millisecs_t;
auto GetStep() -> int64_t;
};
diff --git a/src/ballistica/scene_v1/node/sound_node.cc b/src/ballistica/scene_v1/node/sound_node.cc
index 2d26f8a8..32ce4a1b 100644
--- a/src/ballistica/scene_v1/node/sound_node.cc
+++ b/src/ballistica/scene_v1/node/sound_node.cc
@@ -142,7 +142,7 @@ void SoundNode::Step() {
}
}
if (positional_ && position_dirty_ && playing_) {
- millisecs_t t = g_core->GetAppTimeMillisecs();
+ millisecs_t t = g_core->AppTimeMillisecs();
if (t - last_position_update_time_ > 100) {
base::AudioSource* s = g_base->audio->SourceBeginExisting(play_id_, 107);
if (s) {
diff --git a/src/ballistica/scene_v1/node/spaz_node.cc b/src/ballistica/scene_v1/node/spaz_node.cc
index b9956ddc..3aee29f2 100644
--- a/src/ballistica/scene_v1/node/spaz_node.cc
+++ b/src/ballistica/scene_v1/node/spaz_node.cc
@@ -1797,7 +1797,7 @@ void SpazNode::DoFlyPress() {
// Keep from doing too many sparkles.
static millisecs_t last_sparkle_time = 0;
- millisecs_t t = g_core->GetAppTimeMillisecs();
+ millisecs_t t = g_core->AppTimeMillisecs();
if (t - last_sparkle_time > 200) {
last_sparkle_time = t;
auto* s = g_base->audio->SourceBeginNew();
diff --git a/src/ballistica/scene_v1/node/terrain_node.cc b/src/ballistica/scene_v1/node/terrain_node.cc
index 2d1d4e7a..4aec612c 100644
--- a/src/ballistica/scene_v1/node/terrain_node.cc
+++ b/src/ballistica/scene_v1/node/terrain_node.cc
@@ -104,7 +104,7 @@ TerrainNode::~TerrainNode() {
// without our reference.
if (collision_mesh_.exists()) {
collision_mesh_->collision_mesh_data()->set_last_used_time(
- g_core->GetAppTimeMillisecs());
+ g_core->AppTimeMillisecs());
}
}
@@ -123,7 +123,7 @@ void TerrainNode::set_collision_mesh(SceneCollisionMesh* val) {
// if we had an old one, mark its last-used time so caching works properly..
if (collision_mesh_.exists()) {
collision_mesh_->collision_mesh_data()->set_last_used_time(
- g_core->GetAppTimeMillisecs());
+ g_core->AppTimeMillisecs());
}
collision_mesh_ = val;
diff --git a/src/ballistica/scene_v1/node/text_node.cc b/src/ballistica/scene_v1/node/text_node.cc
index 9c6e932c..6f3d3fe2 100644
--- a/src/ballistica/scene_v1/node/text_node.cc
+++ b/src/ballistica/scene_v1/node/text_node.cc
@@ -117,8 +117,7 @@ void TextNode::SetText(const std::string& val) {
if (do_format_check) {
bool valid;
- g_base->assets->CompileResourceString(val, "setText format check",
- &valid);
+ g_base->assets->CompileResourceString(val, &valid);
if (!valid) {
BA_LOG_ONCE(
LogName::kBa, LogLevel::kError,
@@ -354,8 +353,7 @@ void TextNode::Draw(base::FrameDef* frame_def) {
// Apply subs/resources to get our actual text if need be.
if (text_translation_dirty_) {
- text_translated_ =
- g_base->assets->CompileResourceString(text_raw_, "TextNode::OnDraw");
+ text_translated_ = g_base->assets->CompileResourceString(text_raw_);
text_translation_dirty_ = false;
text_group_dirty_ = true;
text_width_dirty_ = true;
diff --git a/src/ballistica/scene_v1/node/time_display_node.cc b/src/ballistica/scene_v1/node/time_display_node.cc
index 947609d5..b04229da 100644
--- a/src/ballistica/scene_v1/node/time_display_node.cc
+++ b/src/ballistica/scene_v1/node/time_display_node.cc
@@ -50,12 +50,12 @@ TimeDisplayNode::~TimeDisplayNode() = default;
auto TimeDisplayNode::GetOutput() -> std::string {
assert(g_base->InLogicThread());
if (translations_dirty_) {
- time_suffix_hours_ = g_base->assets->CompileResourceString(
- R"({"r":"timeSuffixHoursText"})", "tda");
+ time_suffix_hours_ =
+ g_base->assets->CompileResourceString(R"({"r":"timeSuffixHoursText"})");
time_suffix_minutes_ = g_base->assets->CompileResourceString(
- R"({"r":"timeSuffixMinutesText"})", "tdb");
+ R"({"r":"timeSuffixMinutesText"})");
time_suffix_seconds_ = g_base->assets->CompileResourceString(
- R"({"r":"timeSuffixSecondsText"})", "tdc");
+ R"({"r":"timeSuffixSecondsText"})");
translations_dirty_ = false;
output_dirty_ = true;
}
diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc b/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc
index cd34041c..c6780dc6 100644
--- a/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc
+++ b/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc
@@ -115,7 +115,7 @@ auto PythonClassSceneDataAsset::GetValue(PythonClassSceneDataAsset* self)
// haha really need to rename this class.
base::DataAsset* datadata = data->data_data();
datadata->Load();
- datadata->set_last_used_time(g_core->GetAppTimeMillisecs());
+ datadata->set_last_used_time(g_core->AppTimeMillisecs());
PyObject* obj = datadata->object().get();
assert(obj);
Py_INCREF(obj);
diff --git a/src/ballistica/scene_v1/support/client_session.cc b/src/ballistica/scene_v1/support/client_session.cc
index a63a4ef3..49961fd5 100644
--- a/src/ballistica/scene_v1/support/client_session.cc
+++ b/src/ballistica/scene_v1/support/client_session.cc
@@ -1015,7 +1015,7 @@ void ClientSession::HandleSessionMessage(const std::vector& buffer) {
// let's also use this opportunity to graph our command-buffer size
// for network debugging... if (NetGraph *graph =
// g_graphics->GetClientSessionStepBufferGraph()) {
- // graph->addSample(GetAppTimeMillisecs(), steps_on_list_);
+ // graph->addSample(AppTimeMillisecs(), steps_on_list_);
// }
break;
diff --git a/src/ballistica/scene_v1/support/client_session_net.cc b/src/ballistica/scene_v1/support/client_session_net.cc
index ce3d34e2..a1b0d675 100644
--- a/src/ballistica/scene_v1/support/client_session_net.cc
+++ b/src/ballistica/scene_v1/support/client_session_net.cc
@@ -52,7 +52,7 @@ void ClientSessionNet::OnCommandBufferUnderrun() {
// We currently don't do anything here; we want to just power
// through hitches and keep aiming for our target time.
// (though perhaps we could take note here for analytics purposes).
- // printf("Underrun at %d\n", GetAppTimeMillisecs());
+ // printf("Underrun at %d\n", AppTimeMillisecs());
// fflush(stdout);
}
@@ -100,7 +100,7 @@ void ClientSessionNet::UpdateBuffering() {
+ (1.0f - smoothing)
* static_cast(bucket.max_delay_from_projection);
}
- auto now = g_core->GetAppTimeMillisecs();
+ auto now = g_core->AppTimeMillisecs();
// We want target-base-time to wind up at our projected time minus some
// safety offset to account for buffering fluctuations.
@@ -160,7 +160,7 @@ void ClientSessionNet::OnReset(bool rewind) {
}
void ClientSessionNet::OnBaseTimeStepAdded(int step) {
- auto now = g_core->GetAppTimeMillisecs();
+ auto now = g_core->AppTimeMillisecs();
millisecs_t new_base_time_received = base_time_received_ + step;
diff --git a/src/ballistica/scene_v1/support/host_session.cc b/src/ballistica/scene_v1/support/host_session.cc
index 25340fa2..9f00e258 100644
--- a/src/ballistica/scene_v1/support/host_session.cc
+++ b/src/ballistica/scene_v1/support/host_session.cc
@@ -27,7 +27,7 @@
namespace ballistica::scene_v1 {
HostSession::HostSession(PyObject* session_type_obj)
- : last_kick_idle_players_decrement_time_(g_core->GetAppTimeMillisecs()) {
+ : last_kick_idle_players_decrement_time_(g_core->AppTimeMillisecs()) {
assert(g_base->logic);
assert(g_base->InLogicThread());
assert(session_type_obj != nullptr);
@@ -327,7 +327,7 @@ void HostSession::SetKickIdlePlayers(bool enable) {
// If this has changed, reset our disconnect-time reporting.
assert(g_base->InLogicThread());
if (enable != kick_idle_players_) {
- last_kick_idle_players_decrement_time_ = g_core->GetAppTimeMillisecs();
+ last_kick_idle_players_decrement_time_ = g_core->AppTimeMillisecs();
}
kick_idle_players_ = enable;
}
@@ -456,7 +456,7 @@ void HostSession::DecrementPlayerTimeOuts(millisecs_t millisecs) {
}
void HostSession::ProcessPlayerTimeOuts() {
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
if (foreground_host_activity_.exists()
&& foreground_host_activity_->game_speed() > 0.0
@@ -487,7 +487,7 @@ void HostSession::StepScene() {
void HostSession::Update(int time_advance_millisecs, double time_advance) {
assert(g_base->InLogicThread());
- millisecs_t update_time_start = core::CorePlatform::GetCurrentMillisecs();
+ millisecs_t update_time_start = core::CorePlatform::TimeMonotonicMillisecs();
// HACK: we used to do a bunch of fudging to try and advance time by
// exactly 16 milliseconds per frame which would give us a clean 2 sim
@@ -546,7 +546,7 @@ void HostSession::Update(int time_advance_millisecs, double time_advance) {
// slow down if we're overloaded and have a better chance at maintaining
// a reasonable frame-rate/etc.
auto elapsed =
- core::CorePlatform::GetCurrentMillisecs() - update_time_start;
+ core::CorePlatform::TimeMonotonicMillisecs() - update_time_start;
if (elapsed >= 1000 / 30) {
too_slow = true;
break;
diff --git a/src/ballistica/scene_v1/support/player.cc b/src/ballistica/scene_v1/support/player.cc
index 2db8aa63..43295163 100644
--- a/src/ballistica/scene_v1/support/player.cc
+++ b/src/ballistica/scene_v1/support/player.cc
@@ -19,7 +19,7 @@ namespace ballistica::scene_v1 {
Player::Player(int id_in, HostSession* host_session)
: id_(id_in),
- creation_time_(g_core->GetAppTimeMillisecs()),
+ creation_time_(g_core->AppTimeMillisecs()),
host_session_(host_session) {
assert(host_session);
assert(g_base->InLogicThread());
@@ -40,7 +40,7 @@ Player::~Player() {
}
auto Player::GetAge() const -> millisecs_t {
- return g_core->GetAppTimeMillisecs() - creation_time_;
+ return g_core->AppTimeMillisecs() - creation_time_;
}
auto Player::GetName(bool full, bool icon) const -> std::string {
diff --git a/src/ballistica/scene_v1/support/scene.cc b/src/ballistica/scene_v1/support/scene.cc
index 0293ad79..a545af55 100644
--- a/src/ballistica/scene_v1/support/scene.cc
+++ b/src/ballistica/scene_v1/support/scene.cc
@@ -40,7 +40,7 @@ void Scene::SetMapBounds(float xmin, float ymin, float zmin, float xmax,
Scene::Scene(millisecs_t start_time)
: time_(start_time),
stepnum_(start_time / kGameStepMilliseconds),
- last_step_real_time_(g_core->GetAppTimeMillisecs()) {
+ last_step_real_time_(g_core->AppTimeMillisecs()) {
dynamics_ = Object::New(this);
// Reset world bounds to default.
@@ -145,7 +145,7 @@ void Scene::Step() {
// Step all our nodes.
{
in_step_ = true;
- last_step_real_time_ = g_core->GetAppTimeMillisecs();
+ last_step_real_time_ = g_core->AppTimeMillisecs();
for (auto&& i : nodes_) {
Node* node = i.get();
node->Step();
diff --git a/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc b/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc
index 298c1d7f..3d16d34f 100644
--- a/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc
+++ b/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc
@@ -201,7 +201,7 @@ void SceneV1InputDeviceDelegate::ShipBufferIfFull() {
ConnectionToHost* hc = remote_player_.get();
// Ship the buffer once it gets big enough or once enough time has passed.
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
size_t size = remote_input_commands_buffer_.size();
if (size > 2
diff --git a/src/ballistica/scene_v1/support/session_stream.cc b/src/ballistica/scene_v1/support/session_stream.cc
index cc6780dd..b196f388 100644
--- a/src/ballistica/scene_v1/support/session_stream.cc
+++ b/src/ballistica/scene_v1/support/session_stream.cc
@@ -374,7 +374,7 @@ void SessionStream::ShipSessionCommandsMessage() {
AddMessageToReplay(out_message_);
}
out_message_.clear();
- last_send_time_ = g_core->GetAppTimeMillisecs();
+ last_send_time_ = g_core->AppTimeMillisecs();
}
void SessionStream::AddMessageToReplay(const std::vector& message) {
@@ -441,7 +441,7 @@ void SessionStream::EndCommand(bool is_time_set) {
if (host_session_) {
auto* appmode = classic::ClassicAppMode::GetSingleton();
// Now if its been long enough *AND* this is a time-step command, send.
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
millisecs_t diff = real_time - last_send_time_;
if (is_time_set && diff >= app_mode_->buffer_time()) {
ShipSessionCommandsMessage();
diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc
index 82c917e0..ca606c13 100644
--- a/src/ballistica/shared/ballistica.cc
+++ b/src/ballistica/shared/ballistica.cc
@@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
namespace ballistica {
// These are set automatically via script; don't modify them here.
-const int kEngineBuildNumber = 22155;
+const int kEngineBuildNumber = 22178;
const char* kEngineVersion = "1.7.37";
const int kEngineApiVersion = 9;
@@ -53,7 +53,7 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
core::BaseSoftInterface* l_base{};
try {
- auto time1 = core::CorePlatform::GetCurrentMillisecs();
+ auto time1 = core::CorePlatform::TimeMonotonicMillisecs();
// Even at the absolute start of execution we should be able to
// reasonably log errors. Set env var BA_CRASH_TEST=1 to test this.
@@ -68,7 +68,7 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// import it first thing even if we don't explicitly use it.
l_core = core::CoreFeatureSet::Import(&core_config);
- auto time2 = core::CorePlatform::GetCurrentMillisecs();
+ auto time2 = core::CorePlatform::TimeMonotonicMillisecs();
// If a command was passed, simply run it and exit. We want to act
// simply as a Python interpreter in that case; we don't do any
@@ -98,7 +98,7 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// those modules get loaded from in the first place.
l_core->python->MonolithicModeBaEnvConfigure();
- auto time3 = core::CorePlatform::GetCurrentMillisecs();
+ auto time3 = core::CorePlatform::TimeMonotonicMillisecs();
// We need the base feature-set to run a full app but we don't have a hard
// dependency to it. Let's see if it's available.
@@ -107,7 +107,7 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
FatalError("Base module unavailable; can't run app.");
}
- auto time4 = core::CorePlatform::GetCurrentMillisecs();
+ auto time4 = core::CorePlatform::TimeMonotonicMillisecs();
// -------------------------------------------------------------------------
// Phase 2: "The pieces are moving."
@@ -126,7 +126,7 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// environment do that part).
// Make noise if it takes us too long to get to this point.
- auto time5 = core::CorePlatform::GetCurrentMillisecs();
+ auto time5 = core::CorePlatform::TimeMonotonicMillisecs();
auto total_duration = time5 - time1;
if (total_duration > 5000) {
auto core_import_duration = time2 - time1;
diff --git a/src/ballistica/shared/foundation/event_loop.cc b/src/ballistica/shared/foundation/event_loop.cc
index 52a564a6..5bdb4454 100644
--- a/src/ballistica/shared/foundation/event_loop.cc
+++ b/src/ballistica/shared/foundation/event_loop.cc
@@ -206,7 +206,7 @@ void EventLoop::WaitForNextEvent_(bool single_cycle) {
// If we've got active timers, wait for messages with a timeout so we can
// run the next timer payload.
if (!suspended_ && timers_.ActiveTimerCount() > 0) {
- microsecs_t apptime = g_core->GetAppTimeMicrosecs();
+ microsecs_t apptime = g_core->AppTimeMicrosecs();
microsecs_t wait_time = timers_.TimeToNextExpire(apptime);
if (wait_time > 0) {
std::unique_lock lock(thread_message_mutex_);
@@ -306,7 +306,7 @@ void EventLoop::Run_(bool single_cycle) {
}
if (!suspended_) {
- timers_.Run(g_core->GetAppTimeMicrosecs());
+ timers_.Run(g_core->AppTimeMicrosecs());
RunPendingRunnables_();
}
@@ -584,7 +584,7 @@ auto EventLoop::NewTimer(microsecs_t length, bool repeat, Runnable* runnable)
assert(g_core);
assert(ThreadIsCurrent());
assert(Object::IsValidManagedObject(runnable));
- return timers_.NewTimer(g_core->GetAppTimeMicrosecs(), length, 0,
+ return timers_.NewTimer(g_core->AppTimeMicrosecs(), length, 0,
repeat ? -1 : 0, runnable);
}
@@ -718,7 +718,7 @@ void EventLoop::AcquireGIL_() {
assert(g_base_soft && g_base_soft->InLogicThread());
auto debug_timing{g_core->core_config().debug_timing};
millisecs_t startmillisecs{
- debug_timing ? core::CorePlatform::GetCurrentMillisecs() : 0};
+ debug_timing ? core::CorePlatform::TimeMonotonicMillisecs() : 0};
if (py_thread_state_) {
PyEval_RestoreThread(py_thread_state_);
@@ -726,7 +726,8 @@ void EventLoop::AcquireGIL_() {
}
if (debug_timing) {
- auto duration{core::CorePlatform::GetCurrentMillisecs() - startmillisecs};
+ auto duration{core::CorePlatform::TimeMonotonicMillisecs()
+ - startmillisecs};
if (duration > (1000 / 120)) {
g_core->Log(LogName::kBa, LogLevel::kInfo,
"GIL acquire took too long (" + std::to_string(duration)
diff --git a/src/ballistica/shared/foundation/fatal_error.cc b/src/ballistica/shared/foundation/fatal_error.cc
index 8cfe5d4a..beaa578f 100644
--- a/src/ballistica/shared/foundation/fatal_error.cc
+++ b/src/ballistica/shared/foundation/fatal_error.cc
@@ -122,7 +122,7 @@ void FatalError::ReportFatalError(const std::string& message,
Logging::V1CloudLog(logmsg);
Logging::EmitLog("root", LogLevel::kCritical,
- core::CorePlatform::GetSecondsSinceEpoch(), logmsg);
+ core::CorePlatform::TimeSinceEpochSeconds(), logmsg);
fprintf(stderr, "%s\n", logmsg.c_str());
std::string prefix = "FATAL-ERROR-LOG:";
@@ -182,9 +182,9 @@ void FatalError::DoBlockingFatalErrorDialog(const std::string& message) {
// There's a chance that it can't (if threads are suspended, if it is
// blocked on a synchronous call to another thread, etc.) so if we don't
// see something happening soon, just give up on showing a dialog.
- auto starttime = core::CorePlatform::GetCurrentMillisecs();
+ auto starttime = core::CorePlatform::TimeMonotonicMillisecs();
while (!started) {
- if (core::CorePlatform::GetCurrentMillisecs() - starttime > 3000) {
+ if (core::CorePlatform::TimeMonotonicMillisecs() - starttime > 3000) {
return;
}
core::CorePlatform::SleepMillisecs(10);
@@ -211,7 +211,7 @@ auto FatalError::HandleFatalError(bool exit_cleanly,
if (!in_top_level_exception_handler) {
if (exit_cleanly) {
Logging::EmitLog("root", LogLevel::kCritical,
- core::CorePlatform::GetSecondsSinceEpoch(),
+ core::CorePlatform::TimeSinceEpochSeconds(),
"Calling exit(1)...");
// Inform anyone who cares that the engine is going down NOW.
@@ -223,7 +223,7 @@ auto FatalError::HandleFatalError(bool exit_cleanly,
exit(1);
} else {
Logging::EmitLog("root", LogLevel::kCritical,
- core::CorePlatform::GetSecondsSinceEpoch(),
+ core::CorePlatform::TimeSinceEpochSeconds(),
"Calling abort()...");
abort();
}
diff --git a/src/ballistica/shared/foundation/macros.cc b/src/ballistica/shared/foundation/macros.cc
index b69a2635..f168de73 100644
--- a/src/ballistica/shared/foundation/macros.cc
+++ b/src/ballistica/shared/foundation/macros.cc
@@ -25,7 +25,7 @@ void MacroFunctionTimerEnd(core::CoreFeatureSet* corefs, millisecs_t starttime,
return;
}
assert(corefs);
- millisecs_t endtime = corefs->platform->GetTicks();
+ millisecs_t endtime = corefs->platform->TimeSinceLaunchMillisecs();
if (endtime - starttime > time) {
core::g_core->Log(LogName::kBa, LogLevel::kWarning,
std::to_string(endtime - starttime)
@@ -42,7 +42,7 @@ void MacroFunctionTimerEndThread(core::CoreFeatureSet* corefs,
return;
}
assert(corefs);
- millisecs_t endtime = corefs->platform->GetTicks();
+ millisecs_t endtime = corefs->platform->TimeSinceLaunchMillisecs();
if (endtime - starttime > time) {
g_core->Log(LogName::kBa, LogLevel::kWarning,
std::to_string(endtime - starttime) + " milliseconds spent by "
@@ -60,7 +60,7 @@ void MacroFunctionTimerEndEx(core::CoreFeatureSet* corefs,
return;
}
assert(corefs);
- millisecs_t endtime = corefs->platform->GetTicks();
+ millisecs_t endtime = corefs->platform->TimeSinceLaunchMillisecs();
if (endtime - starttime > time) {
g_core->Log(LogName::kBa, LogLevel::kWarning,
std::to_string(endtime - starttime) + " milliseconds spent in "
@@ -78,7 +78,7 @@ void MacroFunctionTimerEndThreadEx(core::CoreFeatureSet* corefs,
return;
}
assert(corefs);
- millisecs_t endtime = corefs->platform->GetTicks();
+ millisecs_t endtime = corefs->platform->TimeSinceLaunchMillisecs();
if (endtime - starttime > time) {
g_core->Log(LogName::kBa, LogLevel::kWarning,
std::to_string(endtime - starttime) + " milliseconds spent by "
@@ -96,7 +96,7 @@ void MacroTimeCheckEnd(core::CoreFeatureSet* corefs, millisecs_t starttime,
return;
}
assert(corefs);
- millisecs_t e = corefs->platform->GetTicks();
+ millisecs_t e = corefs->platform->TimeSinceLaunchMillisecs();
if (e - starttime > time) {
g_core->Log(LogName::kBa, LogLevel::kWarning,
std::string(name) + " took " + std::to_string(e - starttime)
diff --git a/src/ballistica/shared/foundation/macros.h b/src/ballistica/shared/foundation/macros.h
index 100c4cce..528686d8 100644
--- a/src/ballistica/shared/foundation/macros.h
+++ b/src/ballistica/shared/foundation/macros.h
@@ -43,7 +43,7 @@
// FIXME: Turn these into C++ classes.
#if BA_DEBUG_BUILD
#define BA_DEBUG_FUNCTION_TIMER_BEGIN() \
- millisecs_t _dfts = g_core->platform->GetTicks()
+ millisecs_t _dfts = g_core->platform->TimeSinceLaunchMillisecs()
#define BA_DEBUG_FUNCTION_TIMER_END(time) \
::ballistica::MacroFunctionTimerEnd(g_core, _dfts, time, __PRETTY_FUNCTION__)
#define BA_DEBUG_FUNCTION_TIMER_END_THREAD(time) \
@@ -55,7 +55,7 @@
::ballistica::MacroFunctionTimerEndThreadEx(g_core, _dfts, time, \
__PRETTY_FUNCTION__, what)
#define BA_DEBUG_TIME_CHECK_BEGIN(name) \
- millisecs_t name##_ts = g_core->platform->GetTicks()
+ millisecs_t name##_ts = g_core->platform->TimeSinceLaunchMillisecs()
#define BA_DEBUG_TIME_CHECK_END(name, time) \
::ballistica::MacroTimeCheckEnd(g_core, name##_ts, time, #name, __FILE__, \
__LINE__)
diff --git a/src/ballistica/shared/foundation/object.cc b/src/ballistica/shared/foundation/object.cc
index 03dee019..1d129891 100644
--- a/src/ballistica/shared/foundation/object.cc
+++ b/src/ballistica/shared/foundation/object.cc
@@ -24,7 +24,7 @@ Object::Object() {
#if BA_DEBUG_BUILD
// Mark when we were born.
assert(g_core);
- object_birth_time_ = g_core->GetAppTimeMillisecs();
+ object_birth_time_ = g_core->AppTimeMillisecs();
// Add ourself to the global object list.
{
@@ -126,7 +126,7 @@ void Object::LsObjects() {
{
std::scoped_lock lock(g_core->object_list_mutex);
s = std::to_string(g_core->object_count) + " Objects at time "
- + std::to_string(g_core->GetAppTimeMillisecs()) + ";";
+ + std::to_string(g_core->AppTimeMillisecs()) + ";";
if (explicit_bool(true)) {
std::unordered_map obj_map;
diff --git a/src/ballistica/shared/python/python_ref.cc b/src/ballistica/shared/python/python_ref.cc
index 7a368ca2..a324f32c 100644
--- a/src/ballistica/shared/python/python_ref.cc
+++ b/src/ballistica/shared/python/python_ref.cc
@@ -4,6 +4,7 @@
#include
#include
+#include
#include "ballistica/core/core.h"
#include "ballistica/core/support/base_soft.h"
@@ -251,6 +252,7 @@ auto PythonRef::GetAttr(const char* name) const -> PythonRef {
auto PythonRef::DictGetItem(const char* name) const -> PythonRef {
assert(Python::HaveGIL());
ThrowIfUnset();
+ assert(PyDict_Check(obj_)); // Caller's job to ensure this.
PyObject* key = PyUnicode_FromString(name);
PyObject* out = PyDict_GetItemWithError(obj_, key);
Py_DECREF(key);
@@ -270,6 +272,26 @@ auto PythonRef::DictGetItem(const char* name) const -> PythonRef {
return {};
}
+auto PythonRef::DictItems() const
+ -> std::vector> {
+ assert(Python::HaveGIL());
+ ThrowIfUnset();
+
+ assert(PyDict_Check(obj_)); // Caller's job to ensure this.
+
+ Py_ssize_t pos{};
+ PyObject *key, *value;
+ std::vector> out;
+ out.resize(PyDict_Size(obj_));
+ size_t i = 0;
+ while (PyDict_Next(obj_, &pos, &key, &value)) {
+ out[i].first.Acquire(key);
+ out[i].second.Acquire(value);
+ i++;
+ }
+ return out;
+}
+
auto PythonRef::NewRef() const -> PyObject* {
assert(Python::HaveGIL());
ThrowIfUnset();
diff --git a/src/ballistica/shared/python/python_ref.h b/src/ballistica/shared/python/python_ref.h
index fea453f2..cbeb5c83 100644
--- a/src/ballistica/shared/python/python_ref.h
+++ b/src/ballistica/shared/python/python_ref.h
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
#include "ballistica/shared/ballistica.h" // IWYU pragma: keep.
@@ -153,6 +154,9 @@ class PythonRef {
/// Throws Exception if an error occurs.
auto DictGetItem(const char* name) const -> PythonRef;
+ /// Return all items in a dict as C++ structures.
+ auto DictItems() const -> std::vector>;
+
/// The equivalent of calling Python str() on the contained PyObject, and
/// gracefully handles invalid refs. To throw exceptions on invalid refs,
/// use ValueAsString();
diff --git a/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc b/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc
index 96fff73b..90c823b0 100644
--- a/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc
+++ b/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc
@@ -124,7 +124,7 @@ PyTypeObject PythonClassUISound::type_obj;
PyMethodDef PythonClassUISound::tp_methods[] = {
{"play", (PyCFunction)PythonClassUISound::Play,
METH_VARARGS | METH_KEYWORDS,
- "play() -> None\n"
+ "play(volume: float = 1.0) -> None\n"
"\n"
"Play the sound locally.\n"
""},
diff --git a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc
index e10084fd..9b083e7b 100644
--- a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc
+++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc
@@ -22,6 +22,7 @@
#include "ballistica/ui_v1/widget/root_widget.h"
#include "ballistica/ui_v1/widget/row_widget.h"
#include "ballistica/ui_v1/widget/scroll_widget.h"
+#include "ballistica/ui_v1/widget/spinner_widget.h"
namespace ballistica::ui_v1 {
@@ -901,6 +902,98 @@ static PyMethodDef PyImageWidgetDef = {
"are applied to the Widget.",
};
+// ----------------------------- imagewidget -----------------------------------
+
+static auto PySpinnerWidget(PyObject* self, PyObject* args, PyObject* keywds)
+ -> PyObject* {
+ BA_PYTHON_TRY;
+ PyObject* edit_obj{Py_None};
+ PyObject* parent_obj{Py_None};
+ ContainerWidget* parent_widget{};
+ PyObject* size_obj{Py_None};
+ PyObject* pos_obj{Py_None};
+ PyObject* visible_obj{Py_None};
+
+ static const char* kwlist[] = {"edit", "parent", "size",
+ "position", "visible", nullptr};
+ if (!PyArg_ParseTupleAndKeywords(
+ args, keywds, "|OOOOO", const_cast(kwlist), &edit_obj,
+ &parent_obj, &size_obj, &pos_obj, &visible_obj))
+ return nullptr;
+
+ if (!g_base->CurrentContext().IsEmpty()) {
+ throw Exception("UI functions must be called with no context set.");
+ }
+
+ // Gather up any user code triggered by this stuff and run it at the end
+ // before we return.
+ base::UI::OperationContext ui_op_context;
+
+ // Grab the edited widget or create a new one.
+ Object::Ref b;
+ if (edit_obj != Py_None) {
+ b = dynamic_cast(UIV1Python::GetPyWidget(edit_obj));
+ if (!b.exists())
+ throw Exception("Invalid or nonexistent widget.",
+ PyExcType::kWidgetNotFound);
+ } else {
+ parent_widget = parent_obj == Py_None
+ ? g_ui_v1->screen_root_widget()
+ : dynamic_cast(
+ UIV1Python::GetPyWidget(parent_obj));
+ if (parent_widget == nullptr) {
+ throw Exception("Parent widget nonexistent or not a container.",
+ PyExcType::kWidgetNotFound);
+ }
+ b = Object::New();
+ }
+ if (size_obj != Py_None) {
+ auto size{Python::GetPyFloat(size_obj)};
+ b->set_size(size);
+ }
+ if (pos_obj != Py_None) {
+ Point2D p = Python::GetPyPoint2D(pos_obj);
+ b->set_translate(p.x, p.y);
+ }
+ if (visible_obj != Py_None) {
+ b->set_visible(Python::GetPyBool(visible_obj));
+ }
+
+ // If making a new widget, add it at the end.
+ if (edit_obj == Py_None) {
+ g_ui_v1->AddWidget(b.get(), parent_widget);
+ }
+
+ // Run any calls built up by UI callbacks.
+ ui_op_context.Finish();
+
+ return b->NewPyRef();
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PySpinnerWidgetDef = {
+ "spinnerwidget", // name
+ (PyCFunction)PySpinnerWidget, // method
+ METH_VARARGS | METH_KEYWORDS, // flags
+
+ "spinnerwidget(*,\n"
+ " edit: bauiv1.Widget | None = None,\n"
+ " parent: bauiv1.Widget | None = None,\n"
+ " size: float | None = None,\n"
+ " position: Sequence[float] | None = None,\n"
+ " visible: bool | None = None,\n"
+ ")\n"
+ " -> bauiv1.Widget\n"
+ "\n"
+ "Create or edit a spinner widget.\n"
+ "\n"
+ "Category: **User Interface Functions**\n"
+ "\n"
+ "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n"
+ "a new one is created and returned. Arguments that are not set to None\n"
+ "are applied to the Widget.",
+};
+
// ----------------------------- columnwidget ----------------------------------
static auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds)
@@ -1580,6 +1673,7 @@ static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds)
PyObject* parent_obj{Py_None};
PyObject* edit_obj{Py_None};
PyObject* center_small_content_obj{Py_None};
+ PyObject* center_small_content_horizontally_obj{Py_None};
ContainerWidget* parent_widget{};
PyObject* color_obj{Py_None};
PyObject* highlight_obj{Py_None};
@@ -1599,6 +1693,7 @@ static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds)
"capture_arrows",
"on_select_call",
"center_small_content",
+ "center_small_content_horizontally",
"color",
"highlight",
"border_opacity",
@@ -1610,13 +1705,13 @@ static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds)
nullptr};
if (!PyArg_ParseTupleAndKeywords(
- args, keywds, "|OOOOOOOOOOOOOOOOO", const_cast(kwlist),
+ args, keywds, "|OOOOOOOOOOOOOOOOOO", const_cast(kwlist),
&edit_obj, &parent_obj, &size_obj, &pos_obj, &background_obj,
&selected_child_obj, &capture_arrows_obj, &on_select_call_obj,
- ¢er_small_content_obj, &color_obj, &highlight_obj,
- &border_opacity_obj, &simple_culling_v_obj,
- &selection_loops_to_parent_obj, &claims_left_right_obj,
- &claims_up_down_obj, &autoselect_obj))
+ ¢er_small_content_obj, ¢er_small_content_horizontally_obj,
+ &color_obj, &highlight_obj, &border_opacity_obj,
+ &simple_culling_v_obj, &selection_loops_to_parent_obj,
+ &claims_left_right_obj, &claims_up_down_obj, &autoselect_obj))
return nullptr;
if (!g_base->CurrentContext().IsEmpty()) {
@@ -1670,6 +1765,10 @@ static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds)
widget->set_center_small_content(
Python::GetPyBool(center_small_content_obj));
}
+ if (center_small_content_horizontally_obj != Py_None) {
+ widget->set_center_small_content_horizontally(
+ Python::GetPyBool(center_small_content_horizontally_obj));
+ }
if (color_obj != Py_None) {
std::vector c = Python::GetPyFloats(color_obj);
if (c.size() != 3) {
@@ -1731,6 +1830,7 @@ static PyMethodDef PyScrollWidgetDef = {
" capture_arrows: bool = False,\n"
" on_select_call: Callable | None = None,\n"
" center_small_content: bool | None = None,\n"
+ " center_small_content_horizontally: bool | None = None,\n"
" color: Sequence[float] | None = None,\n"
" highlight: bool | None = None,\n"
" border_opacity: float | None = None,\n"
@@ -2101,7 +2201,7 @@ static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds)
// we should probably extend TextWidget to handle this internally, but
// punting on that for now.
widget->set_description(g_base->assets->CompileResourceString(
- g_base->python->GetPyLString(description_obj), "textwidget set desc"));
+ g_base->python->GetPyLString(description_obj)));
}
if (autoselect_obj != Py_None) {
widget->set_auto_select(Python::GetPyBool(autoselect_obj));
@@ -2661,18 +2761,80 @@ static PyMethodDef PyOnScreenChangeDef = {
"(internal)",
};
+// ------------------------ root_ui_pause_updates ------------------------------
+
+static auto PyRootUIPauseUpdates(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+ BA_PRECONDITION(g_base->InLogicThread());
+
+ auto* root_widget = g_ui_v1->root_widget();
+ BA_PRECONDITION(root_widget);
+ root_widget->PauseUpdates();
+
+ Py_RETURN_NONE;
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyRootUIPauseUpdatesDef = {
+ "root_ui_pause_updates", // name
+ (PyCFunction)PyRootUIPauseUpdates, // method
+ METH_NOARGS, // flags
+
+ "root_ui_pause_updates() -> None\n"
+ "\n"
+ "Temporarily pause updates to the root ui for animation purposes.",
+};
+
+// ------------------------ root_ui_resume_updates -----------------------------
+
+static auto PyRootUIResumeUpdates(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+ BA_PRECONDITION(g_base->InLogicThread());
+
+ auto* root_widget = g_ui_v1->root_widget();
+ BA_PRECONDITION(root_widget);
+ root_widget->ResumeUpdates();
+
+ Py_RETURN_NONE;
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyRootUIResumeUpdatesDef = {
+ "root_ui_resume_updates", // name
+ (PyCFunction)PyRootUIResumeUpdates, // method
+ METH_NOARGS, // flags
+
+ "root_ui_resume_updates() -> None\n"
+ "\n"
+ "Temporarily resume updates to the root ui for animation purposes.",
+};
+
// -----------------------------------------------------------------------------
auto PythonMethodsUIV1::GetMethods() -> std::vector {
- return {
- PyRootUIBackPressDef, PyGetSpecialWidgetDef, PySetPartyWindowOpenDef,
- PyButtonWidgetDef, PyCheckBoxWidgetDef, PyImageWidgetDef,
- PyColumnWidgetDef, PyContainerWidgetDef, PyRowWidgetDef,
- PyScrollWidgetDef, PyHScrollWidgetDef, PyTextWidgetDef,
- PyWidgetDef, PyUIBoundsDef, PyGetSoundDef,
- PyGetTextureDef, PyGetQRCodeTextureDef, PyGetMeshDef,
- PyIsAvailableDef, PyOnScreenChangeDef,
- };
+ return {PyRootUIBackPressDef,
+ PyGetSpecialWidgetDef,
+ PySetPartyWindowOpenDef,
+ PyButtonWidgetDef,
+ PyCheckBoxWidgetDef,
+ PyImageWidgetDef,
+ PySpinnerWidgetDef,
+ PyColumnWidgetDef,
+ PyContainerWidgetDef,
+ PyRowWidgetDef,
+ PyScrollWidgetDef,
+ PyHScrollWidgetDef,
+ PyTextWidgetDef,
+ PyWidgetDef,
+ PyUIBoundsDef,
+ PyGetSoundDef,
+ PyGetTextureDef,
+ PyGetQRCodeTextureDef,
+ PyGetMeshDef,
+ PyIsAvailableDef,
+ PyOnScreenChangeDef,
+ PyRootUIPauseUpdatesDef,
+ PyRootUIResumeUpdatesDef};
}
#pragma clang diagnostic pop
diff --git a/src/ballistica/ui_v1/ui_v1.cc b/src/ballistica/ui_v1/ui_v1.cc
index f6b2c517..c1a61545 100644
--- a/src/ballistica/ui_v1/ui_v1.cc
+++ b/src/ballistica/ui_v1/ui_v1.cc
@@ -278,7 +278,6 @@ void UIV1FeatureSet::ConfirmQuit(QuitType quit_type) {
}
UIV1FeatureSet::UILock::UILock(bool write) {
- assert(g_base->ui);
assert(g_base->InLogicThread());
if (write && g_ui_v1->ui_lock_count_ != 0) {
diff --git a/src/ballistica/ui_v1/ui_v1.h b/src/ballistica/ui_v1/ui_v1.h
index 13c86b5c..73d7b354 100644
--- a/src/ballistica/ui_v1/ui_v1.h
+++ b/src/ballistica/ui_v1/ui_v1.h
@@ -143,11 +143,8 @@ class UIV1FeatureSet : public FeatureSetNativeComponent,
Object::Ref root_widget_;
int ui_lock_count_{};
int language_state_{};
- // int party_icon_number_{};
bool always_use_internal_on_screen_keyboard_{};
bool party_window_open_{};
- // bool account_signed_in_{};
- // std::string account_name_{};
};
} // namespace ballistica::ui_v1
diff --git a/src/ballistica/ui_v1/widget/check_box_widget.cc b/src/ballistica/ui_v1/widget/check_box_widget.cc
index dd4c1696..d569feaa 100644
--- a/src/ballistica/ui_v1/widget/check_box_widget.cc
+++ b/src/ballistica/ui_v1/widget/check_box_widget.cc
@@ -46,7 +46,7 @@ void CheckBoxWidget::SetHeight(float height_in) {
}
void CheckBoxWidget::Draw(base::RenderPass* pass, bool draw_transparent) {
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ millisecs_t real_time = g_core->AppTimeMillisecs();
have_drawn_ = true;
float l = 0.0f;
@@ -236,7 +236,7 @@ void CheckBoxWidget::SetValue(bool value) {
// Don't animate if we're setting initial values.
if (checked_ != value && have_drawn_) {
- last_change_time_ = g_core->GetAppTimeMillisecs();
+ last_change_time_ = g_core->AppTimeMillisecs();
}
checked_ = value;
}
@@ -245,7 +245,7 @@ void CheckBoxWidget::Activate() {
g_base->audio->SafePlaySysSound(base::SysSoundID::kSwish3);
checked_ = !checked_;
check_dirty_ = true;
- last_change_time_ = g_core->GetAppTimeMillisecs();
+ last_change_time_ = g_core->AppTimeMillisecs();
if (auto* call = on_value_change_call_.get()) {
PythonRef args(Py_BuildValue("(O)", checked_ ? Py_True : Py_False),
PythonRef::kSteal);
diff --git a/src/ballistica/ui_v1/widget/root_widget.cc b/src/ballistica/ui_v1/widget/root_widget.cc
index 2277528a..d93cf209 100644
--- a/src/ballistica/ui_v1/widget/root_widget.cc
+++ b/src/ballistica/ui_v1/widget/root_widget.cc
@@ -3,6 +3,7 @@
#include "ballistica/ui_v1/widget/root_widget.h"
#include
+#include
#include
#include
@@ -10,9 +11,12 @@
#include "ballistica/base/assets/assets.h"
#include "ballistica/base/graphics/renderer/render_pass.h"
#include "ballistica/base/graphics/support/frame_def.h"
+#include "ballistica/base/support/classic_soft.h"
#include "ballistica/base/support/context.h"
#include "ballistica/shared/buildconfig/buildconfig_common.h"
#include "ballistica/shared/foundation/inline.h"
+#include "ballistica/shared/foundation/types.h"
+#include "ballistica/shared/generic/utils.h"
#include "ballistica/ui_v1/python/ui_v1_python.h"
#include "ballistica/ui_v1/widget/button_widget.h"
#include "ballistica/ui_v1/widget/image_widget.h"
@@ -849,10 +853,35 @@ void RootWidget::Setup() {
chest_3_lock_icon_ = AddImage_(imgd);
}
+ // TV icons.
+ {
+ ImageDef_ imgd;
+ imgd.x = -34.0f;
+ imgd.y = -27.0f;
+ imgd.width = 32.0f;
+ imgd.height = 32.0f;
+ imgd.img = "tv";
+ imgd.depth_min = 0.3f;
+ imgd.color_r = 1.5f;
+ imgd.color_g = 1.0f;
+ imgd.color_b = 2.0f;
+
+ imgd.button = chest_0_button_;
+ chest_0_tv_icon_ = AddImage_(imgd);
+
+ imgd.button = chest_1_button_;
+ chest_1_tv_icon_ = AddImage_(imgd);
+
+ imgd.button = chest_2_button_;
+ chest_2_tv_icon_ = AddImage_(imgd);
+
+ imgd.button = chest_3_button_;
+ chest_3_tv_icon_ = AddImage_(imgd);
+ }
+
// Lock times.
{
TextDef_ td;
- // td.width = 0.0f;
td.text = "3h 2m";
td.x = 0.0f;
td.y = 55.0f;
@@ -895,6 +924,11 @@ void RootWidget::Setup() {
b.disable_offset_scale = 1.5f;
b.pre_buffer = 20.0f;
b.allow_in_game = false;
+
+ // This is a very big icon that can interfere with clicking stuff near
+ // it, so suck target area in a bit.
+ b.target_extra_left = -20.0f;
+ b.target_extra_right = -20.0f;
inventory_button_ = AddButton_(b);
bottom_right_buttons_.push_back(inventory_button_);
}
@@ -925,13 +959,23 @@ void RootWidget::Setup() {
void RootWidget::Draw(base::RenderPass* pass, bool transparent) {
// Opaque pass gets drawn first; use that as an opportunity to step up our
// motion.
-
if (!transparent) {
- millisecs_t current_time = pass->frame_def()->display_time_millisecs();
- millisecs_t time_diff =
- std::min(millisecs_t{100}, current_time - update_time_);
+ seconds_t current_time = pass->frame_def()->display_time();
+ seconds_t time_diff = std::min(seconds_t{0.1}, current_time - update_time_);
+
+ // millisecs_t current_time = pass->frame_def()->display_time_millisecs();
+ // millisecs_t time_diff =
+ // std::min(millisecs_t{100}, current_time - update_time_);
+
+ StepChildWidgets_(time_diff);
+ StepChests_();
+
+ if (update_pause_count_ != 0) {
+ // update_pause_time_ +=
+ } else {
+ update_pause_time_ = 0.0;
+ }
- StepChildWidgets_(static_cast(time_diff));
update_time_ = current_time;
}
ContainerWidget::Draw(pass, transparent);
@@ -977,6 +1021,7 @@ auto RootWidget::AddButton_(const ButtonDef_& def) -> RootWidget::Button_* {
} else {
b.widget->SetUpWidget(screen_stack_widget_);
}
+
// We wanna prevent anyone from redirecting these to point to outside
// widgets since we'll probably outlive those outside widgets.
b.widget->set_neighbors_locked(true);
@@ -1067,11 +1112,23 @@ void RootWidget::UpdateForFocusedWindow_(Widget* widget) {
MarkForUpdate();
}
-void RootWidget::StepChildWidgets_(float dt) {
+void RootWidget::StepChests_() {
+ // Aim to run this once per second.
+ auto now = g_core->AppTimeSeconds();
+ if (now - last_chests_step_time_ < 1.0) {
+ return;
+ }
+ last_chests_step_time_ = now;
+ UpdateChests_();
+}
+
+void RootWidget::StepChildWidgets_(seconds_t dt) {
// Hitches tend to break our math and cause buttons to overshoot on their
// transitions in and then back up. So let's limit our max dt to about
// what ~30fps would give us.
- dt = std::min(dt, 1000.0f / 30.0f);
+ dt = std::min(dt, 1.0 / 30.0);
+
+ float dt_ms = dt * 1000.0;
if (!child_widgets_dirty_) {
return;
@@ -1110,7 +1167,7 @@ void RootWidget::StepChildWidgets_(float dt) {
float xpos = 0.0f;
for (auto* btn : top_left_buttons_) {
auto enabled = btn->enabled;
- float bwidthhalf = btn->width * 0.5;
+ float bwidthhalf = btn->width * 0.5f;
if (enabled) {
xpos += bwidthhalf + btn->pre_buffer;
}
@@ -1190,8 +1247,8 @@ void RootWidget::StepChildWidgets_(float dt) {
}
// Now push our smooth value towards our target value.
- b.x_smoothed += (b.x_target - b.x_smoothed) * 0.015f * dt;
- b.y_smoothed += (b.y_target - b.y_smoothed) * 0.015f * dt;
+ b.x_smoothed += (b.x_target - b.x_smoothed) * 0.015f * dt_ms;
+ b.y_smoothed += (b.y_target - b.y_smoothed) * 0.015f * dt_ms;
// Snap in place once we reach the target; otherwise note that we need
// to keep going.
@@ -1296,7 +1353,7 @@ void RootWidget::UpdateLayout() {
// Run an immediate step to update things; (avoids jumpy positions if
// resizing game window))
- StepChildWidgets_(0.0f);
+ StepChildWidgets_(0.0);
}
void RootWidget::OnUIScaleChange() { MarkForUpdate(); }
@@ -1419,12 +1476,12 @@ void RootWidget::SetSquadSizeLabel(int val) {
}
}
-void RootWidget::SetTicketsMeterText(const std::string& val) {
+void RootWidget::SetTicketsMeterValue(int val) {
assert(tickets_meter_text_);
- tickets_meter_text_->widget->SetText(val);
+ tickets_meter_text_->widget->SetText(val >= 0 ? std::to_string(val) : "");
}
-void RootWidget::SetTokensMeterText(const std::string& val, bool gold_pass) {
+void RootWidget::SetTokensMeterValue(int val, bool gold_pass) {
assert(tokens_meter_text_);
assert(get_tokens_button_);
gold_pass_ = gold_pass;
@@ -1436,7 +1493,7 @@ void RootWidget::SetTokensMeterText(const std::string& val, bool gold_pass) {
g_buildconfig.enable_os_font_rendering() ? "\xE2\x88\x9E" : "inf");
} else {
get_tokens_button_->force_hide = false;
- tokens_meter_text_->widget->SetText(val);
+ tokens_meter_text_->widget->SetText(val >= 0 ? std::to_string(val) : "");
}
UpdateTokensMeterTextColor_();
// May need to animate in/out.
@@ -1452,9 +1509,10 @@ void RootWidget::UpdateTokensMeterTextColor_() {
}
}
-void RootWidget::SetLeagueRankText(const std::string& val) {
+void RootWidget::SetLeagueRankValue(int val) {
assert(league_rank_text_);
- league_rank_text_->widget->SetText(val);
+ league_rank_text_->widget->SetText(val > 0 ? ("#" + std::to_string(val))
+ : "");
}
void RootWidget::SetLeagueType(const std::string& val) {
@@ -1553,68 +1611,169 @@ void RootWidget::SetHaveLiveValues(bool have_live_values) {
chest_backing_->widget->set_opacity(have_live_values ? 1.0f : 0.5f);
}
-void RootWidget::SetChests(const std::string& chest_0_appearance,
- const std::string& chest_1_appearance,
- const std::string& chest_2_appearance,
- const std::string& chest_3_appearance) {
+void RootWidget::SetChests(
+ const std::string& chest_0_appearance,
+ const std::string& chest_1_appearance,
+ const std::string& chest_2_appearance,
+ const std::string& chest_3_appearance, seconds_t chest_0_unlock_time,
+ seconds_t chest_1_unlock_time, seconds_t chest_2_unlock_time,
+ seconds_t chest_3_unlock_time, seconds_t chest_0_ad_allow_time,
+ seconds_t chest_1_ad_allow_time, seconds_t chest_2_ad_allow_time,
+ seconds_t chest_3_ad_allow_time) {
chest_0_appearance_ = chest_0_appearance;
chest_1_appearance_ = chest_1_appearance;
chest_2_appearance_ = chest_2_appearance;
chest_3_appearance_ = chest_3_appearance;
+ chest_0_unlock_time_ = chest_0_unlock_time;
+ chest_1_unlock_time_ = chest_1_unlock_time;
+ chest_2_unlock_time_ = chest_2_unlock_time;
+ chest_3_unlock_time_ = chest_3_unlock_time;
+ chest_0_ad_allow_time_ = chest_0_ad_allow_time;
+ chest_1_ad_allow_time_ = chest_1_ad_allow_time;
+ chest_2_ad_allow_time_ = chest_2_ad_allow_time;
+ chest_3_ad_allow_time_ = chest_3_ad_allow_time;
UpdateChests_();
}
+void RootWidget::OnLanguageChange() {
+ ContainerWidget::OnLanguageChange();
+ translations_dirty_ = true;
+}
+
void RootWidget::UpdateChests_() {
- std::vector> slots =
- // NOLINTNEXTLINE (clang-format's formatting here upsets cpplint).
+ // Make sure we've got the latest translated strings for open times.
+ if (translations_dirty_) {
+ time_suffix_hours_ =
+ g_base->assets->CompileResourceString(R"({"r":"timeSuffixHoursText"})");
+ time_suffix_minutes_ = g_base->assets->CompileResourceString(
+ R"({"r":"timeSuffixMinutesText"})");
+ time_suffix_seconds_ = g_base->assets->CompileResourceString(
+ R"({"r":"timeSuffixSecondsText"})");
+ translations_dirty_ = false;
+ }
+
+ std::vector>
+ slots =
+ // NOLINTNEXTLINE (clang-format's formatting here upsets cpplint).
{
{chest_0_appearance_, chest_0_button_, chest_0_lock_icon_,
- chest_0_time_text_},
+ chest_0_tv_icon_, chest_0_time_text_, chest_0_unlock_time_,
+ chest_0_ad_allow_time_},
{chest_1_appearance_, chest_1_button_, chest_1_lock_icon_,
- chest_1_time_text_},
+ chest_1_tv_icon_, chest_1_time_text_, chest_1_unlock_time_,
+ chest_1_ad_allow_time_},
{chest_2_appearance_, chest_2_button_, chest_2_lock_icon_,
- chest_2_time_text_},
+ chest_2_tv_icon_, chest_2_time_text_, chest_2_unlock_time_,
+ chest_2_ad_allow_time_},
{chest_3_appearance_, chest_3_button_, chest_3_lock_icon_,
- chest_3_time_text_},
+ chest_3_tv_icon_, chest_3_time_text_, chest_3_unlock_time_,
+ chest_3_ad_allow_time_},
};
// We drop the backing/slots down a bit if we have no chests.
auto have_chests{false};
- for (const auto& [appearance, b, l, t] : slots) {
+
+ // clang-format off
+ for (const auto& [appearance,
+ b,
+ l,
+ tv,
+ t,
+ ut,
+ aat] : slots) {
+ // clang-format on
+
if (appearance != "") {
have_chests = true;
}
}
- for (const auto& [appearance, b, l, t] : slots) {
- assert(b);
- assert(l);
+ auto now{g_base->TimeSinceEpochCloudSeconds()};
+
+ // clang-format off
+ for (const auto& [appearance,
+ btn,
+ lock_img,
+ tv_img,
+ txt,
+ unlocktm,
+ adallowtm] : slots) {
+ // clang-format on
+
+ assert(btn);
+ assert(lock_img);
Object::Ref tex;
if (appearance == "") {
// Empty slot.
- b->widget->set_color(0.473f, 0.44f, 0.583f);
- b->width = b->height = 80.0f;
- b->y = have_chests ? 44.0f : -2.0f;
+ btn->widget->set_color(0.473f, 0.44f, 0.583f);
+ btn->width = btn->height = 80.0f;
+ btn->y = have_chests ? 44.0f : -2.0f;
{
base::Assets::AssetListLock lock;
tex = g_base->assets->GetTexture("chestIconEmpty");
}
- l->visible = false;
- t->visible = false;
+ lock_img->visible = false;
+ tv_img->visible = false;
+ txt->visible = false;
+
+ btn->widget->SetTintTexture(nullptr);
+ btn->widget->set_tint_color(1.0f, 1.0f, 1.0f);
+ btn->widget->set_tint2_color(1.0f, 1.0f, 1.0f);
+
} else {
+ Object::Ref textint;
+
// Chest in slot.
have_chests = true;
- b->widget->set_color(1.0f, 1.0f, 1.0f);
- b->width = b->height = 110.0f;
- b->y = 44.0f;
+ btn->width = btn->height = 110.0f;
+ btn->y = 44.0f;
+ std::string chest_tex_closed;
+ std::string chest_tex_closed_tint;
+ Vector3f chest_color;
+ Vector3f chest_tint;
+ Vector3f chest_tint2;
+ if (auto* classic = g_base->classic()) {
+ classic->GetClassicChestDisplayInfo(
+ appearance, &chest_tex_closed, &chest_tex_closed_tint, &chest_color,
+ &chest_tint, &chest_tint2);
+ } else {
+ chest_tex_closed = "chestIcon";
+ chest_tex_closed_tint = "white";
+ chest_color = Vector3f{1.0f, 1.0f, 1.0f};
+ chest_tint = Vector3f{1.0f, 1.0f, 1.0f};
+ chest_tint2 = Vector3f{1.0f, 1.0f, 1.0f};
+ }
{
base::Assets::AssetListLock lock;
- tex = g_base->assets->GetTexture("chestIcon");
+ tex = g_base->assets->GetTexture(chest_tex_closed);
+ textint = g_base->assets->GetTexture(chest_tex_closed_tint);
+ }
+ btn->widget->set_color(chest_color.x, chest_color.y, chest_color.z);
+ btn->widget->SetTintTexture(textint.get());
+ btn->widget->set_tint_color(chest_tint.x, chest_tint.y, chest_tint.z);
+ btn->widget->set_tint2_color(chest_tint2.x, chest_tint2.y, chest_tint2.z);
+
+ auto to_unlock{gold_pass_ ? 0
+ : static_cast(std::ceil(unlocktm - now))};
+
+ if (to_unlock > 0) {
+ // Show the ad-available tag IF the ad provides an allow-ad time AND
+ // that time has passed AND we've got an ad ready to go.
+ auto allow_ad{adallowtm > 0.0 && adallowtm <= now
+ && g_core->have_incentivized_ad};
+
+ lock_img->visible = true;
+ txt->visible = true;
+ tv_img->visible = allow_ad;
+ txt->widget->SetText(GetTimeStr_(to_unlock));
+ } else {
+ lock_img->visible = false;
+ tv_img->visible = false;
+ txt->visible = false;
}
- l->visible = true;
- t->visible = true;
}
- b->widget->SetTexture(tex.get());
+ btn->widget->SetTexture(tex.get());
}
assert(chest_backing_);
@@ -1623,6 +1782,80 @@ void RootWidget::UpdateChests_() {
child_widgets_dirty_ = true;
}
+auto RootWidget::GetTimeStr_(seconds_t diff) -> std::string {
+ // NOTE: Adapted from time_display_node.cc. Not sure if it would make
+ // sense to share this code somewhere?..
+ std::string output;
+ auto show_sub_seconds{false};
+
+ auto t{static_cast(diff * 1000.0)};
+ bool is_negative = false;
+ if (t < 0) {
+ t = -t;
+ is_negative = true;
+ }
+
+ // Hours
+ int h = static_cast_check_fit(((t / 1000) / (60 * 60)));
+ if (h != 0) {
+ std::string s = time_suffix_hours_;
+ char buffer[100];
+ snprintf(buffer, sizeof(buffer), "%d", h);
+ Utils::StringReplaceOne(&s, "${COUNT}", buffer);
+ if (!output.empty()) {
+ output += " ";
+ }
+ output += s;
+ }
+
+ // Minutes.
+ int m = static_cast_check_fit(((t / 1000) / 60) % 60);
+ if (m != 0) {
+ std::string s = time_suffix_minutes_;
+ char buffer[100];
+ snprintf(buffer, sizeof(buffer), "%d", m);
+ Utils::StringReplaceOne(&s, "${COUNT}", buffer);
+ if (!output.empty()) {
+ output += " ";
+ }
+ output += s;
+ }
+
+ // Only show seconds when within a few minutes.
+ if (m < 2) {
+ if (show_sub_seconds) {
+ float sec = fmod(static_cast(t) / 1000.0f, 60.0f);
+ if (sec >= 0.005f || output.empty()) {
+ std::string s = time_suffix_seconds_;
+ char buffer[100];
+ snprintf(buffer, sizeof(buffer), "%.2f", sec);
+ Utils::StringReplaceOne(&s, "${COUNT}", buffer);
+ if (!output.empty()) {
+ output += " ";
+ }
+ output += s;
+ }
+ } else {
+ // Seconds (integer).
+ int sec = static_cast_check_fit(t / 1000 % 60);
+ if (sec != 0 || output.empty()) {
+ std::string s = time_suffix_seconds_;
+ char buffer[100];
+ snprintf(buffer, sizeof(buffer), "%d", sec);
+ Utils::StringReplaceOne(&s, "${COUNT}", buffer);
+ if (!output.empty()) {
+ output += " ";
+ }
+ output += s;
+ }
+ }
+ }
+ if (is_negative) {
+ output = "-" + output;
+ }
+ return output;
+}
+
void RootWidget::SetInboxCountText(const std::string& val) {
assert(inbox_count_text_);
@@ -1638,4 +1871,18 @@ void RootWidget::SetInboxCountText(const std::string& val) {
}
}
+void RootWidget::PauseUpdates() {
+ assert(g_base->InLogicThread());
+ // TODO(ericf): wire this up.
+ // printf("HELLO PAUSING\n");
+ update_pause_count_ += 1;
+}
+
+void RootWidget::ResumeUpdates() {
+ assert(g_base->InLogicThread());
+ // TODO(ericf): wire this up.
+ // printf("HELLO RESUMING\n");
+ update_pause_count_ -= 1;
+}
+
} // namespace ballistica::ui_v1
diff --git a/src/ballistica/ui_v1/widget/root_widget.h b/src/ballistica/ui_v1/widget/root_widget.h
index 80c5266d..7296695e 100644
--- a/src/ballistica/ui_v1/widget/root_widget.h
+++ b/src/ballistica/ui_v1/widget/root_widget.h
@@ -35,13 +35,14 @@ class RootWidget : public ContainerWidget {
/// Called when UIScale or screen dimensions change.
void OnUIScaleChange();
+ void OnLanguageChange() override;
void UpdateLayout() override;
void SetSquadSizeLabel(int val);
void SetAccountState(bool signed_in, const std::string& name);
- void SetTicketsMeterText(const std::string& val);
- void SetTokensMeterText(const std::string& val, bool gold_pass);
- void SetLeagueRankText(const std::string& val);
+ void SetTicketsMeterValue(int val);
+ void SetTokensMeterValue(int val, bool gold_pass);
+ void SetLeagueRankValue(int val);
void SetLeagueType(const std::string& val);
void SetAchievementPercentText(const std::string& val);
void SetLevelText(const std::string& val);
@@ -50,11 +51,26 @@ class RootWidget : public ContainerWidget {
void SetChests(const std::string& chest_0_appearance,
const std::string& chest_1_appearance,
const std::string& chest_2_appearance,
- const std::string& chest_3_appearance);
+ const std::string& chest_3_appearance,
+ seconds_t chest_0_unlock_time, seconds_t chest_1_unlock_time,
+ seconds_t chest_2_unlock_time, seconds_t chest_3_unlock_time,
+ seconds_t chest_0_ad_allow_time,
+ seconds_t chest_1_ad_allow_time,
+ seconds_t chest_2_ad_allow_time,
+ seconds_t chest_3_ad_allow_time);
void SetHaveLiveValues(bool have_live_values);
auto bottom_left_height() const { return bottom_left_height_; }
+ /// Temporarily pause updates to things such as
+ /// ticket/token meters so they can be applied at a
+ /// set time or animated.
+ void PauseUpdates();
+
+ /// Resume updates to things such as ticket/token
+ /// meters. Snaps to the latest values.
+ void ResumeUpdates();
+
private:
struct ButtonDef_;
struct Button_;
@@ -65,13 +81,15 @@ class RootWidget : public ContainerWidget {
enum class MeterType_ { kLevel, kTrophy, kTickets, kTokens };
enum class VAlign_ { kTop, kCenter, kBottom };
+ auto GetTimeStr_(seconds_t diff) -> std::string;
void UpdateChests_();
void UpdateTokensMeterText_();
void UpdateForFocusedWindow_(Widget* widget);
auto AddButton_(const ButtonDef_& def) -> Button_*;
auto AddText_(const TextDef_& def) -> Text_*;
auto AddImage_(const ImageDef_& def) -> Image_*;
- void StepChildWidgets_(float dt);
+ void StepChildWidgets_(seconds_t dt);
+ void StepChests_();
void AddMeter_(MeterType_ type, float h_align, float r, float g, float b,
bool plus, const std::string& s);
void UpdateTokensMeterTextColor_();
@@ -80,6 +98,9 @@ class RootWidget : public ContainerWidget {
std::string chest_1_appearance_;
std::string chest_2_appearance_;
std::string chest_3_appearance_;
+ std::string time_suffix_hours_;
+ std::string time_suffix_minutes_;
+ std::string time_suffix_seconds_;
std::list buttons_;
std::list texts_;
std::list images_;
@@ -116,6 +137,10 @@ class RootWidget : public ContainerWidget {
Image_* chest_1_lock_icon_{};
Image_* chest_2_lock_icon_{};
Image_* chest_3_lock_icon_{};
+ Image_* chest_0_tv_icon_{};
+ Image_* chest_1_tv_icon_{};
+ Image_* chest_2_tv_icon_{};
+ Image_* chest_3_tv_icon_{};
Text_* squad_size_text_{};
Text_* account_name_text_{};
Text_* tickets_meter_text_{};
@@ -129,14 +154,26 @@ class RootWidget : public ContainerWidget {
Text_* chest_1_time_text_{};
Text_* chest_2_time_text_{};
Text_* chest_3_time_text_{};
+ seconds_t chest_0_unlock_time_{-1.0};
+ seconds_t chest_1_unlock_time_{-1.0};
+ seconds_t chest_2_unlock_time_{-1.0};
+ seconds_t chest_3_unlock_time_{-1.0};
+ seconds_t chest_0_ad_allow_time_{-1.0};
+ seconds_t chest_1_ad_allow_time_{-1.0};
+ seconds_t chest_2_ad_allow_time_{-1.0};
+ seconds_t chest_3_ad_allow_time_{-1.0};
+ seconds_t last_chests_step_time_{-1.0f};
+ seconds_t update_pause_time_{};
+ seconds_t update_time_{};
float base_scale_{1.0f};
float bottom_left_height_{};
- millisecs_t update_time_{};
+ int update_pause_count_{};
ToolbarVisibility toolbar_visibility_{ToolbarVisibility::kInGame};
bool child_widgets_dirty_{true};
bool in_main_menu_{};
bool gold_pass_{};
bool have_live_values_{};
+ bool translations_dirty_{true};
};
} // namespace ballistica::ui_v1
diff --git a/src/ballistica/ui_v1/widget/scroll_widget.cc b/src/ballistica/ui_v1/widget/scroll_widget.cc
index 9a4b64d6..dc8dfc9d 100644
--- a/src/ballistica/ui_v1/widget/scroll_widget.cc
+++ b/src/ballistica/ui_v1/widget/scroll_widget.cc
@@ -201,7 +201,7 @@ auto ScrollWidget::HandleMessage(const base::WidgetMessage& m) -> bool {
avg_scroll_speed_v_ =
smoothing * avg_scroll_speed_v_ + (1.0f - smoothing) * 0.0f;
}
- last_sub_widget_h_scroll_claim_time_ = g_core->GetAppTimeMillisecs();
+ last_sub_widget_h_scroll_claim_time_ = g_core->AppTimeMillisecs();
}
pass = false;
break;
@@ -227,7 +227,7 @@ auto ScrollWidget::HandleMessage(const base::WidgetMessage& m) -> bool {
// ignore vertical scrolling (should probably make this less fuzzy).
bool ignore_regular_scrolling = false;
bool child_claimed_h_scroll_recently =
- (g_core->GetAppTimeMillisecs() - last_sub_widget_h_scroll_claim_time_
+ (g_core->AppTimeMillisecs() - last_sub_widget_h_scroll_claim_time_
< 100);
if (child_claimed_h_scroll_recently
&& std::abs(avg_scroll_speed_h_) > std::abs(avg_scroll_speed_v_))
@@ -569,9 +569,21 @@ void ScrollWidget::UpdateLayout() {
amount_visible_ = 0;
return;
}
- float child_h = (**i).GetHeight();
- child_max_offset_ = child_h - (height() - 2 * (border_height_ + V_MARGIN));
- amount_visible_ = (height() - 2 * (border_height_ + V_MARGIN)) / child_h;
+
+ float extra_border_x{4.0}; // Whee arbitrary hard coded values.
+ float xoffs;
+ if (center_small_content_horizontally_) {
+ float our_width{width()};
+ float child_width = (**i).GetWidth();
+ xoffs = (our_width - child_width) * 0.5 - border_width_ - extra_border_x;
+ } else {
+ xoffs = extra_border_x + border_width_;
+ }
+
+ float child_height = (**i).GetHeight();
+ child_max_offset_ =
+ child_height - (height() - 2 * (border_height_ + V_MARGIN));
+ amount_visible_ = (height() - 2 * (border_height_ + V_MARGIN)) / child_height;
if (amount_visible_ > 1) {
amount_visible_ = 1;
if (center_small_content_) {
@@ -585,8 +597,9 @@ void ScrollWidget::UpdateLayout() {
if (mouse_held_thumb_) {
if (child_offset_v_
- > child_h - (height() - 2 * (border_height_ + V_MARGIN))) {
- child_offset_v_ = child_h - (height() - 2 * (border_height_ + V_MARGIN));
+ > child_height - (height() - 2 * (border_height_ + V_MARGIN))) {
+ child_offset_v_ =
+ child_height - (height() - 2 * (border_height_ + V_MARGIN));
inertia_scroll_rate_ = 0;
}
if (child_offset_v_ < 0) {
@@ -594,9 +607,9 @@ void ScrollWidget::UpdateLayout() {
inertia_scroll_rate_ = 0;
}
}
- (**i).set_translate(4 + border_width_, height() - (border_height_ + V_MARGIN)
- + child_offset_v_smoothed_
- - child_h + center_offset_y_);
+ (**i).set_translate(xoffs, height() - (border_height_ + V_MARGIN)
+ + child_offset_v_smoothed_ - child_height
+ + center_offset_y_);
thumb_dirty_ = true;
}
diff --git a/src/ballistica/ui_v1/widget/scroll_widget.h b/src/ballistica/ui_v1/widget/scroll_widget.h
index cfee14dd..812f0ea6 100644
--- a/src/ballistica/ui_v1/widget/scroll_widget.h
+++ b/src/ballistica/ui_v1/widget/scroll_widget.h
@@ -32,6 +32,10 @@ class ScrollWidget : public ContainerWidget {
center_small_content_ = val;
MarkForUpdate();
}
+ auto set_center_small_content_horizontally(bool val) {
+ center_small_content_horizontally_ = val;
+ MarkForUpdate();
+ }
void OnTouchDelayTimerExpired();
auto set_color(float r, float g, float b) {
color_red_ = r;
@@ -106,6 +110,7 @@ class ScrollWidget : public ContainerWidget {
bool glow_dirty_{true};
bool thumb_dirty_{true};
bool center_small_content_{};
+ bool center_small_content_horizontally_{};
bool touch_held_{};
bool highlight_{true};
bool capture_arrows_{false};
diff --git a/src/ballistica/ui_v1/widget/spinner_widget.cc b/src/ballistica/ui_v1/widget/spinner_widget.cc
new file mode 100644
index 00000000..e851f0e4
--- /dev/null
+++ b/src/ballistica/ui_v1/widget/spinner_widget.cc
@@ -0,0 +1,58 @@
+// Released under the MIT License. See LICENSE for details.
+
+#include "ballistica/ui_v1/widget/spinner_widget.h"
+
+#include
+#include
+
+#include "ballistica/base/assets/assets.h"
+#include "ballistica/base/base.h"
+#include "ballistica/base/graphics/component/simple_component.h"
+
+namespace ballistica::ui_v1 {
+
+SpinnerWidget::SpinnerWidget() {}
+SpinnerWidget::~SpinnerWidget() = default;
+
+auto SpinnerWidget::GetWidth() -> float { return size_; }
+auto SpinnerWidget::GetHeight() -> float { return size_; }
+
+void SpinnerWidget::Draw(base::RenderPass* pass, bool draw_transparent) {
+ seconds_t current_time = pass->frame_def()->display_time();
+
+ // We only draw in transparent pass.
+ if (!draw_transparent) {
+ return;
+ }
+
+ // Fade presence in any time we're visible and out any time we're not.
+ if (visible_) {
+ presence_ = std::min(
+ 1.0, presence_ + pass->frame_def()->display_time_elapsed() * 1.0);
+ } else {
+ presence_ = std::max(
+ 0.0, presence_ - pass->frame_def()->display_time_elapsed() * 2.0);
+ // Also don't draw anything in this case.
+ return;
+ }
+
+ auto alpha{std::max(0.0, std::min(1.0, presence_ * 2.0 - 1.0))};
+
+ base::SimpleComponent c(pass);
+ c.SetTransparent(true);
+ c.SetColor(1.0f, 1.0f, 1.0f, alpha);
+ c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kSpinner));
+ {
+ auto xf = c.ScopedTransform();
+ c.Scale(size_, size_, 1.0f);
+ c.Rotate(-360.0f * std::fmod(current_time * 2.0, 1.0), 0.0f, 0.0f, 1.0f);
+ c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1));
+ }
+ c.Submit();
+}
+
+auto SpinnerWidget::HandleMessage(const base::WidgetMessage& m) -> bool {
+ return false;
+}
+
+} // namespace ballistica::ui_v1
diff --git a/src/ballistica/ui_v1/widget/spinner_widget.h b/src/ballistica/ui_v1/widget/spinner_widget.h
new file mode 100644
index 00000000..ab724dc3
--- /dev/null
+++ b/src/ballistica/ui_v1/widget/spinner_widget.h
@@ -0,0 +1,36 @@
+// Released under the MIT License. See LICENSE for details.
+
+#ifndef BALLISTICA_UI_V1_WIDGET_SPINNER_WIDGET_H_
+#define BALLISTICA_UI_V1_WIDGET_SPINNER_WIDGET_H_
+
+#include
+
+#include "ballistica/ui_v1/widget/widget.h"
+
+namespace ballistica::ui_v1 {
+
+class SpinnerWidget : public Widget {
+ public:
+ SpinnerWidget();
+ ~SpinnerWidget() override;
+ void Draw(base::RenderPass* pass, bool transparent) override;
+ auto HandleMessage(const base::WidgetMessage& m) -> bool override;
+ void set_size(float size) { size_ = size; }
+
+ /// Setting the visibility attr on a spinner will cause it to fade in
+ /// gradually when made visible. Setting visible-in-container will not
+ /// have this effect.
+ void set_visible(bool val) { visible_ = val; }
+ auto GetWidth() -> float override;
+ auto GetHeight() -> float override;
+ auto GetWidgetTypeName() -> std::string override { return "spinner"; }
+
+ private:
+ float size_{32.0f};
+ float presence_{};
+ bool visible_{true};
+};
+
+} // namespace ballistica::ui_v1
+
+#endif // BALLISTICA_UI_V1_WIDGET_SPINNER_WIDGET_H_
diff --git a/src/ballistica/ui_v1/widget/text_widget.cc b/src/ballistica/ui_v1/widget/text_widget.cc
index c4a3fde0..f96733e2 100644
--- a/src/ballistica/ui_v1/widget/text_widget.cc
+++ b/src/ballistica/ui_v1/widget/text_widget.cc
@@ -547,8 +547,7 @@ void TextWidget::SetText(const std::string& text_in_raw) {
if (do_format_check) {
bool valid;
- g_base->assets->CompileResourceString(
- text_in_raw, "TextWidget::set_text format check", &valid);
+ g_base->assets->CompileResourceString(text_in_raw, &valid);
if (!valid) {
BA_LOG_ONCE(LogName::kBa, LogLevel::kError,
"Invalid resource string: '" + text_in_raw + "'");
@@ -951,8 +950,7 @@ void TextWidget::UpdateTranslation_() {
if (editable()) {
text_translated_ = text_raw_;
} else {
- text_translated_ = g_base->assets->CompileResourceString(
- text_raw_, "TextWidget::UpdateTranslation");
+ text_translated_ = g_base->assets->CompileResourceString(text_raw_);
}
text_translation_dirty_ = false;
text_group_dirty_ = true;
diff --git a/src/meta/baclassicmeta/pyembed/binding_classic.py b/src/meta/baclassicmeta/pyembed/binding_classic.py
index 04af0f5b..61c67878 100644
--- a/src/meta/baclassicmeta/pyembed/binding_classic.py
+++ b/src/meta/baclassicmeta/pyembed/binding_classic.py
@@ -6,9 +6,15 @@ from __future__ import annotations
from baclassic._music import do_play_music
from baclassic._input import get_input_device_mapped_value
+from baclassic._chest import (
+ CHEST_APPEARANCE_DISPLAY_INFOS,
+ CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
+)
# The C++ layer looks for this variable:
values = [
do_play_music, # kDoPlayMusicCall
get_input_device_mapped_value, # kGetInputDeviceMappedValueCall
+ CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, # kChestAppearanceDisplayInfoDefault
+ CHEST_APPEARANCE_DISPLAY_INFOS, # kChestAppearanceDisplayInfos
]
diff --git a/tests/test_efro/test_dataclassio.py b/tests/test_efro/test_dataclassio.py
index a24e5617..6cc0f056 100644
--- a/tests/test_efro/test_dataclassio.py
+++ b/tests/test_efro/test_dataclassio.py
@@ -1129,6 +1129,74 @@ def test_soft_default() -> None:
assert dataclass_from_dict(_TestClassE8, todict) == orig
+def test_enum_fallback() -> None:
+ """Test enum_fallback IOAttr values."""
+ # pylint: disable=missing-class-docstring
+ # pylint: disable=unused-variable
+
+ @ioprepped
+ @dataclass
+ class TestClass:
+
+ class TestEnum1(Enum):
+ VAL1 = 'val1'
+ VAL2 = 'val2'
+ VAL3 = 'val3'
+
+ class TestEnum2(Enum):
+ VAL1 = 'val1'
+ VAL2 = 'val2'
+ VAL3 = 'val3'
+
+ enum1val: Annotated[TestEnum1, IOAttrs('e1')]
+ enum2val: Annotated[
+ TestEnum2, IOAttrs('e2', enum_fallback=TestEnum2.VAL1)
+ ]
+
+ # All valid values; should work.
+ _obj = dataclass_from_dict(TestClass, {'e1': 'val1', 'e2': 'val1'})
+
+ # Bad Enum1 value; should fail since there's no fallback.
+ with pytest.raises(ValueError):
+ _obj = dataclass_from_dict(TestClass, {'e1': 'val4', 'e2': 'val1'})
+
+ # Bad Enum2 value; the attr provides a fallback but still should
+ # fail since we didn't explicitly specify lossy loading.
+ with pytest.raises(ValueError):
+ obj = dataclass_from_dict(TestClass, {'e1': 'val1', 'e2': 'val4'})
+
+ # Bad Enum2 value; should successfully substitute our fallback value
+ # since we specify lossy loading.
+ obj_w_fb = dataclass_from_dict(
+ TestClass, {'e1': 'val1', 'e2': 'val4'}, lossy=True
+ )
+ assert obj_w_fb.enum2val is obj_w_fb.TestEnum2.VAL1
+
+ # Allowing fallbacks means data might be lost on any load, so we
+ # disallow writes for such data to be safe.
+ with pytest.raises(ValueError):
+ dataclass_to_dict(obj_w_fb)
+
+ # Using wrong type as enum_fallback should fail.
+ with pytest.raises(TypeError):
+
+ @ioprepped
+ @dataclass
+ class TestClass2:
+
+ class TestEnum1(Enum):
+ VAL1 = 'val1'
+ VAL2 = 'val2'
+
+ class TestEnum2(Enum):
+ VAL1 = 'val1'
+ VAL2 = 'val2'
+
+ enum1val: Annotated[
+ TestEnum1, IOAttrs('e1', enum_fallback=TestEnum2.VAL1)
+ ]
+
+
class MTTestTypeID(Enum):
"""IDs for our multi-type class."""
@@ -1460,56 +1528,295 @@ def test_multi_type_2() -> None:
val3 = dataclass_from_dict(MTTest2Base, indict3)
-def test_enum_fallback() -> None:
- """Test enum_fallback IOAttr values."""
- # pylint: disable=missing-class-docstring
- # pylint: disable=unused-variable
+# Define 2 variations of Test3 - an 'old' and 'new' one - to simulate
+# older/newer versions of the same schema.
+class MTTest3OldTypeID(Enum):
+ """IDs for our multi-type class."""
- @ioprepped
- @dataclass
- class TestClass:
+ CLASS_1 = 'm1'
+ CLASS_2 = 'm2'
- class TestEnum1(Enum):
- VAL1 = 'val1'
- VAL2 = 'val2'
- VAL3 = 'val3'
- class TestEnum2(Enum):
- VAL1 = 'val1'
- VAL2 = 'val2'
- VAL3 = 'val3'
+class MTTest3OldBase(IOMultiType[MTTest3OldTypeID]):
+ """Our multi-type class.
- enum1val: Annotated[TestEnum1, IOAttrs('e1')]
- enum2val: Annotated[
- TestEnum2, IOAttrs('e2', enum_fallback=TestEnum2.VAL1)
- ]
+ These top level multi-type classes are special parent classes
+ that know about all of their child classes and how to serialize
+ & deserialize them using explicit type ids. We can then use the
+ parent class in annotations and dataclassio will do the right thing.
+ Useful for stuff like Message classes where we may want to store a
+ bunch of different types of them into one place.
+ """
- # All valid values; should work.
- _obj = dataclass_from_dict(TestClass, {'e1': 'val1', 'e2': 'val1'})
+ @override
+ @classmethod
+ def get_type(cls, type_id: MTTest3OldTypeID) -> type[MTTest3OldBase]:
+ """Return the subclass for each of our type-ids."""
- # Bad Enum1 value; should fail since there's no fallback.
+ # This uses assert_never() to ensure we cover all cases in the
+ # enum. Though this is less efficient than looking up by dict
+ # would be. If we had lots of values we could also support lazy
+ # loading by importing classes only when their value is being
+ # requested.
+ val: type[MTTest3OldBase]
+ if type_id is MTTest3OldTypeID.CLASS_1:
+ val = MTTest3OldClass1
+ elif type_id is MTTest3OldTypeID.CLASS_2:
+ val = MTTest3OldClass2
+ else:
+ assert_never(type_id)
+ return val
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> MTTest3OldTypeID:
+ """Provide the type-id for this subclass."""
+ # If we wanted, we could just maintain a static mapping of
+ # types-to-ids here, but there are benefits to letting each
+ # child class speak for itself. Namely that we can do
+ # lazy-loading and don't need to have all types present here.
+
+ # So we'll let all our child classes override this.
+ raise NotImplementedError()
+
+ @override
+ @classmethod
+ def get_unknown_type_fallback(cls) -> MTTest3OldBase | None:
+ # Define a fallback here that can be returned in cases of
+ # unrecognized types (though only if 'lossy' is enabled for the
+ # load).
+ return MTTest3OldClass1(ival=42)
+
+
+@ioprepped
+@dataclass
+class MTTest3OldClass1(MTTest3OldBase):
+ """A test child-class for use with our multi-type class."""
+
+ ival: int
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> MTTest3OldTypeID:
+ return MTTest3OldTypeID.CLASS_1
+
+
+@ioprepped
+@dataclass
+class MTTest3OldClass2(MTTest3OldBase):
+ """Another test child-class for use with our multi-type class."""
+
+ sval: str
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> MTTest3OldTypeID:
+ return MTTest3OldTypeID.CLASS_2
+
+
+@ioprepped
+@dataclass
+class MTTest3OldWrapper:
+ """Testing something *containing* a test class instance."""
+
+ child: MTTest3OldBase
+
+
+@ioprepped
+@dataclass
+class MTTest3OldListWrapper:
+ """Testing something *containing* a test class instance."""
+
+ children: list[MTTest3OldBase]
+
+
+class MTTest3NewTypeID(Enum):
+ """IDs for our multi-type class."""
+
+ CLASS_1 = 'm1'
+ CLASS_2 = 'm2'
+ CLASS_3 = 'm3'
+
+
+class MTTest3NewBase(IOMultiType[MTTest3NewTypeID]):
+ """Our multi-type class.
+
+ These top level multi-type classes are special parent classes
+ that know about all of their child classes and how to serialize
+ & deserialize them using explicit type ids. We can then use the
+ parent class in annotations and dataclassio will do the right thing.
+ Useful for stuff like Message classes where we may want to store a
+ bunch of different types of them into one place.
+ """
+
+ @override
+ @classmethod
+ def get_type(cls, type_id: MTTest3NewTypeID) -> type[MTTest3NewBase]:
+ """Return the subclass for each of our type-ids."""
+
+ # This uses assert_never() to ensure we cover all cases in the
+ # enum. Though this is less efficient than looking up by dict
+ # would be. If we had lots of values we could also support lazy
+ # loading by importing classes only when their value is being
+ # requested.
+ val: type[MTTest3NewBase]
+ if type_id is MTTest3NewTypeID.CLASS_1:
+ val = MTTest3NewClass1
+ elif type_id is MTTest3NewTypeID.CLASS_2:
+ val = MTTest3NewClass2
+ elif type_id is MTTest3NewTypeID.CLASS_3:
+ val = MTTest3NewClass3
+ else:
+ assert_never(type_id)
+ return val
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> MTTest3NewTypeID:
+ """Provide the type-id for this subclass."""
+ # If we wanted, we could just maintain a static mapping of
+ # types-to-ids here, but there are benefits to letting each
+ # child class speak for itself. Namely that we can do
+ # lazy-loading and don't need to have all types present here.
+
+ # So we'll let all our child classes override this.
+ raise NotImplementedError()
+
+ @override
+ @classmethod
+ def get_unknown_type_fallback(cls) -> MTTest3NewBase | None:
+ # Define a fallback here that can be returned in cases of
+ # unrecognized types (though only if 'lossy' is enabled for the
+ # load).
+ return MTTest3NewClass1(ival=43)
+
+
+@ioprepped
+@dataclass
+class MTTest3NewClass1(MTTest3NewBase):
+ """A test child-class for use with our multi-type class."""
+
+ ival: int
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> MTTest3NewTypeID:
+ return MTTest3NewTypeID.CLASS_1
+
+
+@ioprepped
+@dataclass
+class MTTest3NewClass2(MTTest3NewBase):
+ """Another test child-class for use with our multi-type class."""
+
+ sval: str
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> MTTest3NewTypeID:
+ return MTTest3NewTypeID.CLASS_2
+
+
+@ioprepped
+@dataclass
+class MTTest3NewClass3(MTTest3NewBase):
+ """Another test child-class for use with our multi-type class."""
+
+ bval: bool
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> MTTest3NewTypeID:
+ return MTTest3NewTypeID.CLASS_3
+
+
+@ioprepped
+@dataclass
+class MTTest3NewWrapper:
+ """Testing something *containing* a test class instance."""
+
+ child: MTTest3NewBase
+
+
+@ioprepped
+@dataclass
+class MTTest3NewListWrapper:
+ """Testing something *containing* a test class instance."""
+
+ children: list[MTTest3NewBase]
+
+
+def test_multi_type_3() -> None:
+ """Test IOMultiType stuff."""
+
+ # Define some data using our 'newer' schema and it should load using
+ # our 'older' one.
+ data2 = dataclass_to_dict(MTTest3NewClass2(sval='foof'))
+ obj2 = dataclass_from_dict(MTTest3OldBase, data2)
+ assert isinstance(obj2, MTTest3OldClass2)
+
+ # However, this won't work with class 3 which only exists in the
+ # 'newer' schema. So this should fail.
+ data3 = dataclass_to_dict(MTTest3NewClass3(bval=True))
with pytest.raises(ValueError):
- _obj = dataclass_from_dict(TestClass, {'e1': 'val4', 'e2': 'val1'})
+ obj3 = dataclass_from_dict(MTTest3OldBase, data3)
- # Bad Enum2 value; should substitute our fallback value.
- obj = dataclass_from_dict(TestClass, {'e1': 'val1', 'e2': 'val4'})
- assert obj.enum2val is obj.TestEnum2.VAL1
+ # Running in lossy mode should succeed, however, since we define a
+ # fallback call on our multitype. The fallback should give us a
+ # particular MTTestClass1.
+ obj3 = dataclass_from_dict(MTTest3OldBase, data3, lossy=True)
+ assert obj3 == MTTest3OldClass1(ival=42)
- # Using wrong type as enum_fallback should fail.
- with pytest.raises(TypeError):
+ # ----------------------------------------------------------------
+ # Now do the same tests with a dataclass *containing* one of these
+ # dataclasses (since this goes through a different code path).
+ # ----------------------------------------------------------------
- @ioprepped
- @dataclass
- class TestClass2:
+ # Define some data using our 'newer' schema and it should load using
+ # our 'older' one.
+ wdata2 = dataclass_to_dict(
+ MTTest3NewWrapper(child=MTTest3NewClass2(sval='foof'))
+ )
+ wobj2 = dataclass_from_dict(MTTest3OldWrapper, wdata2)
+ assert isinstance(wobj2, MTTest3OldWrapper)
+ assert isinstance(wobj2.child, MTTest3OldClass2)
- class TestEnum1(Enum):
- VAL1 = 'val1'
- VAL2 = 'val2'
+ # However, this won't work with class 3 which only exists in the
+ # 'newer' schema. So this should fail.
+ wdata3 = dataclass_to_dict(MTTest3NewWrapper(MTTest3NewClass3(bval=True)))
+ with pytest.raises(ValueError):
+ wobj3 = dataclass_from_dict(MTTest3OldWrapper, wdata3)
- class TestEnum2(Enum):
- VAL1 = 'val1'
- VAL2 = 'val2'
+ # Running in lossy mode should succeed, however, since we define a
+ # fallback call on our multitype. The fallback should give us a
+ # particular MTTestClass1.
+ wobj3 = dataclass_from_dict(MTTest3OldWrapper, wdata3, lossy=True)
+ assert wobj3 == MTTest3OldWrapper(child=MTTest3OldClass1(ival=42))
- enum1val: Annotated[
- TestEnum1, IOAttrs('e1', enum_fallback=TestEnum2.VAL1)
- ]
+ # ----------------------------------------------------------------
+ # Once more with a dataclass containing a *sequence* of these, which
+ # is a slightly different code path again.
+ # ----------------------------------------------------------------
+
+ # Define some data using our 'newer' schema and it should load using
+ # our 'older' one.
+ wldata2 = dataclass_to_dict(
+ MTTest3NewListWrapper(children=[MTTest3NewClass2(sval='foof')])
+ )
+ wlobj2 = dataclass_from_dict(MTTest3OldListWrapper, wldata2)
+ assert isinstance(wlobj2, MTTest3OldListWrapper)
+ assert isinstance(wlobj2.children[0], MTTest3OldClass2)
+
+ # However, this won't work with class 3 which only exists in the
+ # 'newer' schema. So this should fail.
+ wldata3 = dataclass_to_dict(
+ MTTest3NewListWrapper([MTTest3NewClass3(bval=True)])
+ )
+ with pytest.raises(ValueError):
+ wlobj3 = dataclass_from_dict(MTTest3OldListWrapper, wldata3)
+
+ # Running in lossy mode should succeed, however, since we define a
+ # fallback call on our multitype. The fallback should give us a
+ # particular MTTestClass1.
+ wlobj3 = dataclass_from_dict(MTTest3OldListWrapper, wldata3, lossy=True)
+ assert wlobj3 == MTTest3OldListWrapper(children=[MTTest3OldClass1(ival=42)])
diff --git a/tools/bacommon/bs.py b/tools/bacommon/bs.py
new file mode 100644
index 00000000..7d673b80
--- /dev/null
+++ b/tools/bacommon/bs.py
@@ -0,0 +1,733 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""BombSquad specific bits."""
+
+from __future__ import annotations
+
+import datetime
+from enum import Enum
+from dataclasses import dataclass, field
+from typing import Annotated, override, assert_never
+
+from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
+from efro.message import Message, Response
+
+
+@ioprepped
+@dataclass
+class PrivatePartyMessage(Message):
+ """Message asking about info we need for private-party UI."""
+
+ need_datacode: Annotated[bool, IOAttrs('d')]
+
+ @override
+ @classmethod
+ def get_response_types(cls) -> list[type[Response] | None]:
+ return [PrivatePartyResponse]
+
+
+@ioprepped
+@dataclass
+class PrivatePartyResponse(Response):
+ """Here's that private party UI info you asked for, boss."""
+
+ success: Annotated[bool, IOAttrs('s')]
+ tokens: Annotated[int, IOAttrs('t')]
+ gold_pass: Annotated[bool, IOAttrs('g')]
+ datacode: Annotated[str | None, IOAttrs('d')]
+
+
+class ClassicChestAppearance(Enum):
+ """Appearances bombsquad classic chests can have."""
+
+ UNKNOWN = 'u'
+ DEFAULT = 'd'
+ L1 = 'l1'
+ L2 = 'l2'
+ L3 = 'l3'
+ L4 = 'l4'
+ L5 = 'l5'
+ L6 = 'l6'
+
+
+@ioprepped
+@dataclass
+class ClassicAccountLiveData:
+ """Live account data fed to the client in the bs classic app mode."""
+
+ @dataclass
+ class Chest:
+ """A lovely chest."""
+
+ appearance: Annotated[
+ ClassicChestAppearance,
+ IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
+ ]
+ unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
+ ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]
+
+ class LeagueType(Enum):
+ """Type of league we are in."""
+
+ BRONZE = 'b'
+ SILVER = 's'
+ GOLD = 'g'
+ DIAMOND = 'd'
+
+ tickets: Annotated[int, IOAttrs('ti')]
+
+ tokens: Annotated[int, IOAttrs('to')]
+ gold_pass: Annotated[bool, IOAttrs('g')]
+
+ achievements: Annotated[int, IOAttrs('a')]
+ achievements_total: Annotated[int, IOAttrs('at')]
+
+ league_type: Annotated[LeagueType | None, IOAttrs('lt')]
+ league_num: Annotated[int | None, IOAttrs('ln')]
+ league_rank: Annotated[int | None, IOAttrs('lr')]
+
+ level: Annotated[int, IOAttrs('lv')]
+ xp: Annotated[int, IOAttrs('xp')]
+ xpmax: Annotated[int, IOAttrs('xpm')]
+
+ inbox_count: Annotated[int, IOAttrs('ibc')]
+ inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
+
+ chests: Annotated[dict[str, Chest], IOAttrs('c')]
+
+
+class DisplayItemTypeID(Enum):
+ """Type ID for each of our subclasses."""
+
+ UNKNOWN = 'u'
+ TICKETS = 't'
+ TOKENS = 'k'
+
+
+class DisplayItem(IOMultiType[DisplayItemTypeID]):
+ """Some amount of something that can be shown or described.
+
+ Used to depict chest contents or other rewards or prices.
+ """
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> DisplayItemTypeID:
+ # Require child classes to supply this themselves. If we did a
+ # full type registry/lookup here it would require us to import
+ # everything and would prevent lazy loading.
+ raise NotImplementedError()
+
+ @override
+ @classmethod
+ def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
+ """Return the subclass for each of our type-ids."""
+ # pylint: disable=cyclic-import
+ out: type[DisplayItem]
+
+ t = DisplayItemTypeID
+ if type_id is t.UNKNOWN:
+ out = UnknownDisplayItem
+ elif type_id is t.TICKETS:
+ out = TicketsDisplayItem
+ elif type_id is t.TOKENS:
+ out = TokensDisplayItem
+ else:
+ # Important to make sure we provide all types.
+ assert_never(type_id)
+ return out
+
+ def get_description(self) -> tuple[str, list[tuple[str, str]]]:
+ """Return a string description and subs for the item.
+
+ These decriptions are baked into the DisplayItemWrapper and
+ should be accessed from there by the client. This should only be
+ called on the server side when doing said baking.
+ """
+ raise NotImplementedError()
+
+ # Implement fallbacks so client can digest item lists even if they
+ # contain unrecognized stuff. DisplayItemWrapper contains basic
+ # baked down info that they can still use in such cases.
+ @override
+ @classmethod
+ def get_unknown_type_fallback(cls) -> DisplayItem:
+ return UnknownDisplayItem()
+
+
+@ioprepped
+@dataclass
+class UnknownDisplayItem(DisplayItem):
+ """Something we don't know how to display."""
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> DisplayItemTypeID:
+ return DisplayItemTypeID.UNKNOWN
+
+ @override
+ def get_description(self) -> tuple[str, list[tuple[str, str]]]:
+ import logging
+
+ # Make noise but don't break.
+ logging.exception(
+ 'UnknownDisplayItem.get_description() should never be called.'
+ ' Always access descriptions on the DisplayItemWrapper.'
+ )
+ return 'Unknown', []
+
+
+@ioprepped
+@dataclass
+class TicketsDisplayItem(DisplayItem):
+ """Some amount of tickets."""
+
+ count: Annotated[int, IOAttrs('c')]
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> DisplayItemTypeID:
+ return DisplayItemTypeID.TICKETS
+
+ @override
+ def get_description(self) -> tuple[str, list[tuple[str, str]]]:
+ return '${C} Tickets', [('${C}', str(self.count))]
+
+
+@ioprepped
+@dataclass
+class TokensDisplayItem(DisplayItem):
+ """Some amount of tokens."""
+
+ count: Annotated[int, IOAttrs('c')]
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> DisplayItemTypeID:
+ return DisplayItemTypeID.TOKENS
+
+ @override
+ def get_description(self) -> tuple[str, list[tuple[str, str]]]:
+ return '${C} Tokens', [('${C}', str(self.count))]
+
+
+@ioprepped
+@dataclass
+class DisplayItemWrapper:
+ """Wraps a DisplayItem and common info."""
+
+ item: Annotated[DisplayItem, IOAttrs('i')]
+ description: Annotated[str, IOAttrs('d')]
+ description_subs: Annotated[list[str] | None, IOAttrs('s')]
+
+ @classmethod
+ def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
+ """Convenience method to wrap a DisplayItem."""
+ desc, subs = item.get_description()
+ # Flatten subs to single list.
+ flat_subs = [item for pair in subs for item in pair]
+ return DisplayItemWrapper(item, desc, flat_subs)
+
+
+@ioprepped
+@dataclass
+class ChestInfoMessage(Message):
+ """Request info about a chest."""
+
+ chest_id: Annotated[str, IOAttrs('i')]
+
+ @override
+ @classmethod
+ def get_response_types(cls) -> list[type[Response] | None]:
+ return [ChestInfoResponse]
+
+
+@ioprepped
+@dataclass
+class ChestInfoResponse(Response):
+ """Here's that chest info you asked for, boss."""
+
+ @dataclass
+ class Chest:
+ """A lovely chest."""
+
+ @dataclass
+ class PrizeSet:
+ """A possible set of prizes for this chest."""
+
+ weight: Annotated[float, IOAttrs('w')]
+ contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
+
+ appearance: Annotated[
+ ClassicChestAppearance,
+ IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
+ ]
+
+ # How much to unlock *now*.
+ unlock_tokens: Annotated[int, IOAttrs('tk')]
+
+ # When unlocks on its own.
+ unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
+
+ # Possible prizes we contain.
+ prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
+
+ # Are ads allowed now?
+ ad_allow: Annotated[bool, IOAttrs('aa')]
+
+ chest: Annotated[Chest | None, IOAttrs('c')]
+ user_tokens: Annotated[int | None, IOAttrs('t')]
+
+
+@ioprepped
+@dataclass
+class ChestActionMessage(Message):
+ """Request action about a chest."""
+
+ class Action(Enum):
+ """Types of actions we can request."""
+
+ # Unlocking (for free or with tokens).
+ UNLOCK = 'u'
+
+ # Watched an ad to reduce wait.
+ AD = 'ad'
+
+ action: Annotated[Action, IOAttrs('a')]
+
+ # Tokens we are paying (only applies to unlock).
+ token_payment: Annotated[int, IOAttrs('t')]
+
+ chest_id: Annotated[str, IOAttrs('i')]
+
+ @override
+ @classmethod
+ def get_response_types(cls) -> list[type[Response] | None]:
+ return [ChestActionResponse]
+
+
+@ioprepped
+@dataclass
+class ChestActionResponse(Response):
+ """Here's the results of that action you asked for, boss."""
+
+ # Tokens that were actually charged.
+ tokens_charged: Annotated[int, IOAttrs('t')] = 0
+
+ # If present, signifies the chest has been opened and we should show
+ # the user this stuff that was in it.
+ contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None
+
+ # If contents are present, which of the chest's prize-sets they
+ # represent.
+ prizeindex: Annotated[int, IOAttrs('i')] = 0
+
+ # Printable error if something goes wrong.
+ error: Annotated[str | None, IOAttrs('e')] = None
+
+ # Printable warning. Shown in orange with an error sound. Does not
+ # mean the action failed; only that there's something to tell the
+ # users such as 'It looks like you are faking ad views; stop it or
+ # you won't have ad options anymore.'
+ warning: Annotated[str | None, IOAttrs('w')] = None
+
+ # Printable success message. Shown in green with a cash-register
+ # sound. Can be used for things like successful wait reductions via
+ # ad views.
+ success_msg: Annotated[str | None, IOAttrs('s')] = None
+
+
+class ClientUITypeID(Enum):
+ """Type ID for each of our subclasses."""
+
+ UNKNOWN = 'u'
+ BASIC = 'b'
+
+
+class ClientUI(IOMultiType[ClientUITypeID]):
+ """Defines some user interface on the client."""
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> ClientUITypeID:
+ # Require child classes to supply this themselves. If we did a
+ # full type registry/lookup here it would require us to import
+ # everything and would prevent lazy loading.
+ raise NotImplementedError()
+
+ @override
+ @classmethod
+ def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
+ """Return the subclass for each of our type-ids."""
+ # pylint: disable=cyclic-import
+ out: type[ClientUI]
+
+ t = ClientUITypeID
+ if type_id is t.UNKNOWN:
+ out = UnknownClientUI
+ elif type_id is t.BASIC:
+ out = BasicClientUI
+ else:
+ # Important to make sure we provide all types.
+ assert_never(type_id)
+ return out
+
+ @override
+ @classmethod
+ def get_unknown_type_fallback(cls) -> ClientUI:
+ # If we encounter some future message type we don't know
+ # anything about, drop in a placeholder.
+ return UnknownClientUI()
+
+
+@ioprepped
+@dataclass
+class UnknownClientUI(ClientUI):
+ """Fallback type for unrecognized entries."""
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> ClientUITypeID:
+ return ClientUITypeID.UNKNOWN
+
+
+class BasicClientUIComponentTypeID(Enum):
+ """Type ID for each of our subclasses."""
+
+ UNKNOWN = 'u'
+ TEXT = 't'
+
+
+class BasicClientUIComponent(IOMultiType[BasicClientUIComponentTypeID]):
+ """Top level class for our multitype."""
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> BasicClientUIComponentTypeID:
+ # Require child classes to supply this themselves. If we did a
+ # full type registry/lookup here it would require us to import
+ # everything and would prevent lazy loading.
+ raise NotImplementedError()
+
+ @override
+ @classmethod
+ def get_type(
+ cls, type_id: BasicClientUIComponentTypeID
+ ) -> type[BasicClientUIComponent]:
+ """Return the subclass for each of our type-ids."""
+ # pylint: disable=cyclic-import
+
+ t = BasicClientUIComponentTypeID
+ if type_id is t.UNKNOWN:
+ return BasicClientUIComponentUnknown
+ if type_id is t.TEXT:
+ return BasicClientUIComponentText
+ # if type_id is t.SCREEN_MESSAGE:
+ # return BasicClientUIComponentScreenMessage
+
+ # Important to make sure we provide all types.
+ assert_never(type_id)
+
+ @override
+ @classmethod
+ def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
+ # If we encounter some future message type we don't know
+ # anything about, drop in a placeholder.
+ return BasicClientUIComponentUnknown()
+
+
+@ioprepped
+@dataclass
+class BasicClientUIComponentUnknown(BasicClientUIComponent):
+ """An unknown basic client component type.
+
+ In practice these should never show up since the master-server
+ generates these on the fly for the client and so should not send
+ clients one they can't digest.
+ """
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> BasicClientUIComponentTypeID:
+ return BasicClientUIComponentTypeID.UNKNOWN
+
+
+@ioprepped
+@dataclass
+class BasicClientUIComponentText(BasicClientUIComponent):
+ """Show some text in the inbox message."""
+
+ text: Annotated[str, IOAttrs('t')]
+ subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
+ default_factory=list
+ )
+ scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0
+ color: Annotated[
+ tuple[float, float, float, float], IOAttrs('c', store_default=False)
+ ] = (
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ )
+ spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
+ spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> BasicClientUIComponentTypeID:
+ return BasicClientUIComponentTypeID.TEXT
+
+
+@ioprepped
+@dataclass
+class BasicClientUI(ClientUI):
+ """A basic UI for the client."""
+
+ class ButtonLabel(Enum):
+ """Distinct button labels we support."""
+
+ UNKNOWN = 'u'
+ OK = 'o'
+ APPLY = 'a'
+ CANCEL = 'c'
+ ACCEPT = 'ac'
+ DECLINE = 'dn'
+ IGNORE = 'ig'
+ CLAIM = 'cl'
+ DISCARD = 'd'
+
+ class InteractionStyle(Enum):
+ """Overall interaction styles we support."""
+
+ UNKNOWN = 'u'
+ BUTTON_POSITIVE = 'p'
+ BUTTON_POSITIVE_NEGATIVE = 'pn'
+
+ components: Annotated[list[BasicClientUIComponent], IOAttrs('s')]
+
+ interaction_style: Annotated[
+ InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN)
+ ] = InteractionStyle.BUTTON_POSITIVE
+
+ button_label_positive: Annotated[
+ ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN)
+ ] = ButtonLabel.OK
+
+ button_label_negative: Annotated[
+ ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN)
+ ] = ButtonLabel.CANCEL
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> ClientUITypeID:
+ return ClientUITypeID.BASIC
+
+ def contains_unknown_elements(self) -> bool:
+ """Whether something within us is an unknown type or enum."""
+ return (
+ self.interaction_style is self.InteractionStyle.UNKNOWN
+ or self.button_label_positive is self.ButtonLabel.UNKNOWN
+ or self.button_label_negative is self.ButtonLabel.UNKNOWN
+ or any(
+ c.get_type_id() is BasicClientUIComponentTypeID.UNKNOWN
+ for c in self.components
+ )
+ )
+
+
+@ioprepped
+@dataclass
+class ClientUIWrapper:
+ """Wrapper for a ClientUI and its common data."""
+
+ id: Annotated[str, IOAttrs('i')]
+ createtime: Annotated[datetime.datetime, IOAttrs('c')]
+ ui: Annotated[ClientUI, IOAttrs('e')]
+
+
+@ioprepped
+@dataclass
+class InboxRequestMessage(Message):
+ """Message requesting our inbox."""
+
+ @override
+ @classmethod
+ def get_response_types(cls) -> list[type[Response] | None]:
+ return [InboxRequestResponse]
+
+
+@ioprepped
+@dataclass
+class InboxRequestResponse(Response):
+ """Here's that inbox contents you asked for, boss."""
+
+ wrappers: Annotated[list[ClientUIWrapper], IOAttrs('w')]
+
+ # Printable error if something goes wrong.
+ error: Annotated[str | None, IOAttrs('e')] = None
+
+
+class ClientUIAction(Enum):
+ """Types of actions we can run."""
+
+ BUTTON_PRESS_POSITIVE = 'p'
+ BUTTON_PRESS_NEGATIVE = 'n'
+
+
+class ClientEffectTypeID(Enum):
+ """Type ID for each of our subclasses."""
+
+ UNKNOWN = 'u'
+ SCREEN_MESSAGE = 'm'
+ SOUND = 's'
+ DELAY = 'd'
+
+
+class ClientEffect(IOMultiType[ClientEffectTypeID]):
+ """Something that can happen on the client.
+
+ This can include screen messages, sounds, visual effects, etc.
+ """
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> ClientEffectTypeID:
+ # Require child classes to supply this themselves. If we did a
+ # full type registry/lookup here it would require us to import
+ # everything and would prevent lazy loading.
+ raise NotImplementedError()
+
+ @override
+ @classmethod
+ def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
+ """Return the subclass for each of our type-ids."""
+ # pylint: disable=cyclic-import
+
+ t = ClientEffectTypeID
+ if type_id is t.UNKNOWN:
+ return ClientEffectUnknown
+ if type_id is t.SCREEN_MESSAGE:
+ return ClientEffectScreenMessage
+ if type_id is t.SOUND:
+ return ClientEffectSound
+ if type_id is t.DELAY:
+ return ClientEffectDelay
+
+ # Important to make sure we provide all types.
+ assert_never(type_id)
+
+ @override
+ @classmethod
+ def get_unknown_type_fallback(cls) -> ClientEffect:
+ # If we encounter some future message type we don't know
+ # anything about, drop in a placeholder.
+ return ClientEffectUnknown()
+
+
+@ioprepped
+@dataclass
+class ClientEffectUnknown(ClientEffect):
+ """Fallback substitute for types we don't recognize."""
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> ClientEffectTypeID:
+ return ClientEffectTypeID.UNKNOWN
+
+
+@ioprepped
+@dataclass
+class ClientEffectScreenMessage(ClientEffect):
+ """Display a screen-message."""
+
+ message: Annotated[str, IOAttrs('m')]
+
+ # Note: Firestore can't store arrays of arrays so we flatten it to a
+ # single dimension.
+ subs: Annotated[list[str], IOAttrs('s')]
+ color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0)
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> ClientEffectTypeID:
+ return ClientEffectTypeID.SCREEN_MESSAGE
+
+
+@ioprepped
+@dataclass
+class ClientEffectSound(ClientEffect):
+ """Play a sound."""
+
+ class Sound(Enum):
+ """Sounds that can be made alongside the message."""
+
+ UNKNOWN = 'u'
+ CASH_REGISTER = 'c'
+ ERROR = 'e'
+ POWER_DOWN = 'p'
+ GUN_COCKING = 'g'
+
+ sound: Annotated[Sound, IOAttrs('s', enum_fallback=Sound.UNKNOWN)]
+ volume: Annotated[float, IOAttrs('v')] = 1.0
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> ClientEffectTypeID:
+ return ClientEffectTypeID.SOUND
+
+
+@ioprepped
+@dataclass
+class ClientEffectDelay(ClientEffect):
+ """Delay effect processing."""
+
+ seconds: Annotated[float, IOAttrs('s')]
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> ClientEffectTypeID:
+ return ClientEffectTypeID.DELAY
+
+
+@ioprepped
+@dataclass
+class ClientUIActionMessage(Message):
+ """Do something to a client ui."""
+
+ id: Annotated[str, IOAttrs('i')]
+ action: Annotated[ClientUIAction, IOAttrs('a')]
+
+ @override
+ @classmethod
+ def get_response_types(cls) -> list[type[Response] | None]:
+ return [ClientUIActionResponse]
+
+
+@ioprepped
+@dataclass
+class ClientUIActionResponse(Response):
+ """Did something to that inbox entry, boss."""
+
+ class ErrorType(Enum):
+ """Types of errors that may have occurred."""
+
+ # Probably a future error type we don't recognize.
+ UNKNOWN = 'u'
+
+ # Something went wrong on the server, but specifics are not
+ # relevant.
+ INTERNAL = 'i'
+
+ # The entry expired on the server. In various cases such as 'ok'
+ # buttons this can generally be ignored.
+ EXPIRED = 'e'
+
+ error_type: Annotated[
+ ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN)
+ ]
+
+ # User facing error message in the case of errors.
+ error_message: Annotated[str | None, IOAttrs('em')]
+
+ effects: Annotated[list[ClientEffect], IOAttrs('fx')]
diff --git a/tools/bacommon/cloud.py b/tools/bacommon/cloud.py
index fbd28701..0d8aaf2e 100644
--- a/tools/bacommon/cloud.py
+++ b/tools/bacommon/cloud.py
@@ -4,7 +4,6 @@
from __future__ import annotations
-import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Annotated, override
@@ -301,240 +300,3 @@ class StoreQueryResponse(Response):
available_purchases: Annotated[list[Purchase], IOAttrs('p')]
token_info_url: Annotated[str, IOAttrs('tiu')]
-
-
-@ioprepped
-@dataclass
-class BSPrivatePartyMessage(Message):
- """Message asking about info we need for private-party UI."""
-
- need_datacode: Annotated[bool, IOAttrs('d')]
-
- @override
- @classmethod
- def get_response_types(cls) -> list[type[Response] | None]:
- return [BSPrivatePartyResponse]
-
-
-@ioprepped
-@dataclass
-class BSPrivatePartyResponse(Response):
- """Here's that private party UI info you asked for, boss."""
-
- success: Annotated[bool, IOAttrs('s')]
- tokens: Annotated[int, IOAttrs('t')]
- gold_pass: Annotated[bool, IOAttrs('g')]
- datacode: Annotated[str | None, IOAttrs('d')]
-
-
-class BSClassicChestAppearance(Enum):
- """Appearances bombsquad classic chests can have."""
-
- UNKNOWN = 'u'
- DEFAULT = 'd'
-
-
-@ioprepped
-@dataclass
-class BSClassicAccountLiveData:
- """Account related data kept up to date live for classic app mode."""
-
- @dataclass
- class Chest:
- """A lovely chest."""
-
- appearance: Annotated[
- BSClassicChestAppearance,
- IOAttrs('a', enum_fallback=BSClassicChestAppearance.UNKNOWN),
- ]
- unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
- ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]
-
- class LeagueType(Enum):
- """Type of league we are in."""
-
- BRONZE = 'b'
- SILVER = 's'
- GOLD = 'g'
- DIAMOND = 'd'
-
- tickets: Annotated[int, IOAttrs('ti')]
-
- tokens: Annotated[int, IOAttrs('to')]
- gold_pass: Annotated[bool, IOAttrs('g')]
-
- achievements: Annotated[int, IOAttrs('a')]
- achievements_total: Annotated[int, IOAttrs('at')]
-
- league_type: Annotated[LeagueType | None, IOAttrs('lt')]
- league_num: Annotated[int | None, IOAttrs('ln')]
- league_rank: Annotated[int | None, IOAttrs('lr')]
-
- level: Annotated[int, IOAttrs('lv')]
- xp: Annotated[int, IOAttrs('xp')]
- xpmax: Annotated[int, IOAttrs('xpm')]
-
- inbox_count: Annotated[int, IOAttrs('ibc')]
- inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
-
- chests: Annotated[dict[str, Chest], IOAttrs('c')]
-
-
-class BSInboxEntryType(Enum):
- """Types of entries that can be in an inbox."""
-
- UNKNOWN = 'u' # Entry types we don't support will be this.
- SIMPLE = 's'
- CLAIM = 'c'
- CLAIM_DISCARD = 'cd'
-
-
-@ioprepped
-@dataclass
-class BSInboxEntry:
- """Single message in an inbox."""
-
- type: Annotated[
- BSInboxEntryType, IOAttrs('t', enum_fallback=BSInboxEntryType.UNKNOWN)
- ]
- id: Annotated[str, IOAttrs('i')]
- createtime: Annotated[datetime.datetime, IOAttrs('c')]
-
- # If clients don't support format_version of a message they will
- # display 'app needs to be updated to show this'.
- format_version: Annotated[int, IOAttrs('f', soft_default=1)]
-
- # These have soft defaults so can be removed in the future if desired.
- message: Annotated[str, IOAttrs('m', soft_default='(invalid message)')]
- subs: Annotated[list[str], IOAttrs('s', soft_default_factory=list)]
-
-
-@ioprepped
-@dataclass
-class BSInboxRequestMessage(Message):
- """Message requesting our inbox."""
-
- @override
- @classmethod
- def get_response_types(cls) -> list[type[Response] | None]:
- return [BSInboxRequestResponse]
-
-
-@ioprepped
-@dataclass
-class BSInboxRequestResponse(Response):
- """Here's that inbox contents you asked for, boss."""
-
- entries: Annotated[list[BSInboxEntry], IOAttrs('m')]
-
- # Printable error if something goes wrong.
- error: Annotated[str | None, IOAttrs('e')] = None
-
-
-@ioprepped
-@dataclass
-class BSChestInfoMessage(Message):
- """Request info about a chest."""
-
- chest_id: Annotated[str, IOAttrs('i')]
-
- @override
- @classmethod
- def get_response_types(cls) -> list[type[Response] | None]:
- return [BSChestInfoResponse]
-
-
-@ioprepped
-@dataclass
-class BSChestInfoResponse(Response):
- """Here's that inbox contents you asked for, boss."""
-
- @dataclass
- class Chest:
- """A lovely chest."""
-
- appearance: Annotated[
- BSClassicChestAppearance,
- IOAttrs('a', enum_fallback=BSClassicChestAppearance.UNKNOWN),
- ]
-
- # How much to unlock *now*.
- unlock_tokens: Annotated[int, IOAttrs('tk')]
-
- # When unlocks on its own.
- unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
-
- # Are ads allowed now?
- ad_allow: Annotated[bool, IOAttrs('aa')]
-
- chest: Annotated[Chest | None, IOAttrs('c')]
-
-
-@ioprepped
-@dataclass
-class BSChestActionMessage(Message):
- """Request action about a chest."""
-
- class Action(Enum):
- """Types of actions we can request."""
-
- # Unlocking (for free or with tokens).
- UNLOCK = 'u'
-
- # Watched an ad to reduce wait.
- AD = 'ad'
-
- action: Annotated[Action, IOAttrs('a')]
-
- # Tokens we are paying (only applies to unlock).
- token_payment: Annotated[int, IOAttrs('t')]
-
- chest_id: Annotated[str, IOAttrs('i')]
-
- @override
- @classmethod
- def get_response_types(cls) -> list[type[Response] | None]:
- return [BSChestActionResponse]
-
-
-@ioprepped
-@dataclass
-class BSChestActionResponse(Response):
- """Here's the results of that action you asked for, boss."""
-
- # If present, signifies the chest has been opened and we should show
- # the user this stuff that was in it.
- contents: Annotated[list[str] | None, IOAttrs('c')] = None
-
- # Printable error if something goes wrong.
- error: Annotated[str | None, IOAttrs('e')] = None
-
-
-class BSInboxEntryProcessType(Enum):
- """Types of processing we can ask for."""
-
- POSITIVE = 'p'
- NEGATIVE = 'n'
-
-
-@ioprepped
-@dataclass
-class BSInboxEntryProcessMessage(Message):
- """Do something to an inbox entry."""
-
- id: Annotated[str, IOAttrs('i')]
- process_type: Annotated[BSInboxEntryProcessType, IOAttrs('t')]
-
- @override
- @classmethod
- def get_response_types(cls) -> list[type[Response] | None]:
- return [BSInboxEntryProcessResponse]
-
-
-@ioprepped
-@dataclass
-class BSInboxEntryProcessResponse(Response):
- """Did something to that inbox entry, boss."""
-
- # Printable error if something goes wrong.
- error: Annotated[str | None, IOAttrs('e')] = None
diff --git a/tools/efro/dataclassio/_api.py b/tools/efro/dataclassio/_api.py
index 5e7aee34..d494c6d7 100644
--- a/tools/efro/dataclassio/_api.py
+++ b/tools/efro/dataclassio/_api.py
@@ -104,14 +104,15 @@ def dataclass_from_dict(
coerce_to_float: bool = True,
allow_unknown_attrs: bool = True,
discard_unknown_attrs: bool = False,
+ lossy: bool = False,
) -> T:
"""Given a dict, return a dataclass of a given type.
The dict must be formatted to match the specified codec (generally
json-friendly object types). This means that sequence values such as
tuples or sets should be passed as lists, enums should be passed as
- their associated values, nested dataclasses should be passed as dicts,
- etc.
+ their associated values, nested dataclasses should be passed as
+ dicts, etc.
All values are checked to ensure their types/values are valid.
@@ -121,14 +122,22 @@ def dataclass_from_dict(
(as this would break the ability to do a lossless round-trip with
data).
- If coerce_to_float is True, int values passed for float typed fields
- will be converted to float values. Otherwise, a TypeError is raised.
+ If `coerce_to_float` is True, int values passed for float typed
+ fields will be converted to float values. Otherwise, a TypeError is
+ raised.
- If `allow_unknown_attrs` is False, AttributeErrors will be raised for
- attributes present in the dict but not on the data class. Otherwise,
- they will be preserved as part of the instance and included if it is
- exported back to a dict, unless `discard_unknown_attrs` is True, in
- which case they will simply be discarded.
+ If 'allow_unknown_attrs' is False, AttributeErrors will be raised
+ for attributes present in the dict but not on the data class.
+ Otherwise, they will be preserved as part of the instance and
+ included if it is exported back to a dict, unless
+ `discard_unknown_attrs` is True, in which case they will simply be
+ discarded.
+
+ If `lossy` is True, Enum attrs and IOMultiType types are allowed to
+ use any fallbacks defined for them. This can allow older schemas to
+ successfully load newer data, but this can fundamentally modify the
+ data, so the resulting object is flagged as 'lossy' and prevented
+ from being serialized back out by default.
"""
val = _Inputter(
cls,
@@ -136,6 +145,7 @@ def dataclass_from_dict(
coerce_to_float=coerce_to_float,
allow_unknown_attrs=allow_unknown_attrs,
discard_unknown_attrs=discard_unknown_attrs,
+ lossy=lossy,
).run(values)
assert isinstance(val, cls)
return val
@@ -144,9 +154,11 @@ def dataclass_from_dict(
def dataclass_from_json(
cls: type[T],
json_str: str,
+ *,
coerce_to_float: bool = True,
allow_unknown_attrs: bool = True,
discard_unknown_attrs: bool = False,
+ lossy: bool = False,
) -> T:
"""Return a dataclass instance given a json string.
@@ -159,6 +171,7 @@ def dataclass_from_json(
coerce_to_float=coerce_to_float,
allow_unknown_attrs=allow_unknown_attrs,
discard_unknown_attrs=discard_unknown_attrs,
+ lossy=lossy,
)
diff --git a/tools/efro/dataclassio/_base.py b/tools/efro/dataclassio/_base.py
index dc707377..191119dd 100644
--- a/tools/efro/dataclassio/_base.py
+++ b/tools/efro/dataclassio/_base.py
@@ -24,6 +24,11 @@ SIMPLE_TYPES = {int, bool, str, float, type(None)}
# present.
EXTRA_ATTRS_ATTR = '_DCIOEXATTRS'
+# Attr name for a bool attr for flagging data as lossy, which means it
+# may have been modified in some way during load and should generally not
+# be written back out.
+LOSSY_ATTR = '_DCIOLOSSY'
+
class Codec(Enum):
"""Specifies expected data format exported to or imported from."""
@@ -127,44 +132,68 @@ class IOMultiType(Generic[EnumT]):
The default is an obscure value so that it does not conflict
with members of individual type attrs, but in some cases one
- might prefer to serialize it to something simpler like 'type'
- by overriding this call. One just needs to make sure that no
+ might prefer to serialize it to something simpler like 'type' by
+ overriding this call. One just needs to make sure that no
encompassed types serialize anything to 'type' themself.
"""
return '_dciotype'
+ # NOTE: Currently (Jan 2025) mypy complains if overrides annotate
+ # return type of 'Self | None'. Substituting their own explicit type
+ # works though (see test_dataclassio).
+ @classmethod
+ def get_unknown_type_fallback(cls) -> Self | None:
+ """Return a fallback object in cases of unrecognized types.
+
+ This can allow newer data to remain readable in older
+ environments. Use caution with this option, however, as it
+ effectively modifies data.
+ """
+ return None
+
class IOAttrs:
"""For specifying io behavior in annotations.
'storagename', if passed, is the name used when storing to json/etc.
- 'store_default' can be set to False to avoid writing values when equal
- to the default value. Note that this requires the dataclass field
- to define a default or default_factory or for its IOAttrs to
- define a soft_default value.
+
+ 'store_default' can be set to False to avoid writing values when
+ equal to the default value. Note that this requires the
+ dataclass field to define a default or default_factory or for
+ its IOAttrs to define a soft_default value.
+
'whole_days', if True, requires datetime values to be exactly on day
boundaries (see efro.util.utc_today()).
- 'whole_hours', if True, requires datetime values to lie exactly on hour
- boundaries (see efro.util.utc_this_hour()).
- 'whole_minutes', if True, requires datetime values to lie exactly on minute
- boundaries (see efro.util.utc_this_minute()).
+
+ 'whole_hours', if True, requires datetime values to lie exactly on
+ hour boundaries (see efro.util.utc_this_hour()).
+
+ 'whole_minutes', if True, requires datetime values to lie exactly on
+ minute boundaries (see efro.util.utc_this_minute()).
+
'soft_default', if passed, injects a default value into dataclass
instantiation when the field is not present in the input data.
This allows dataclasses to add new non-optional fields while
- gracefully 'upgrading' old data. Note that when a soft_default is
- present it will take precedence over field defaults when determining
- whether to store a value for a field with store_default=False
- (since the soft_default value is what we'll get when reading that
- same data back in when the field is omitted).
+ gracefully 'upgrading' old data. Note that when a soft_default
+ is present it will take precedence over field defaults when
+ determining whether to store a value for a field with
+ store_default=False (since the soft_default value is what we'll
+ get when reading that same data back in when the field is
+ omitted).
+
'soft_default_factory' is similar to 'default_factory' in dataclass
- fields; it should be used instead of 'soft_default' for mutable types
- such as lists to prevent a single default object from unintentionally
- changing over time.
- 'enum_fallback', if provided, specifies an enum value to be substituted
- in the case of unrecognized enum values.
+ fields; it should be used instead of 'soft_default' for mutable
+ types such as lists to prevent a single default object from
+ unintentionally changing over time.
+
+ 'enum_fallback', if provided, specifies an enum value that can be
+ substituted in the case of unrecognized input values. This can
+ allow newer data to remain loadable in older environments. Note
+ that 'lossy' must be enabled in the top level load call for this
+ to apply, since it can fundamentally modify data.
"""
- # A sentinel object to detect if a parameter is supplied or not. Use
+ # A sentinel object to detect if a parameter is supplied or not. Use
# a class to give it a better repr.
class _MissingType:
pass
diff --git a/tools/efro/dataclassio/_inputter.py b/tools/efro/dataclassio/_inputter.py
index 9a975647..597d2f9a 100644
--- a/tools/efro/dataclassio/_inputter.py
+++ b/tools/efro/dataclassio/_inputter.py
@@ -20,6 +20,7 @@ from efro.dataclassio._base import (
Codec,
_parse_annotated,
EXTRA_ATTRS_ATTR,
+ LOSSY_ATTR,
_is_valid_for_codec,
_get_origin,
SIMPLE_TYPES,
@@ -46,6 +47,7 @@ class _Inputter:
coerce_to_float: bool,
allow_unknown_attrs: bool = True,
discard_unknown_attrs: bool = False,
+ lossy: bool = False,
):
self._cls = cls
self._codec = codec
@@ -53,6 +55,7 @@ class _Inputter:
self._allow_unknown_attrs = allow_unknown_attrs
self._discard_unknown_attrs = discard_unknown_attrs
self._soft_default_validator: _Outputter | None = None
+ self._lossy = lossy
if not allow_unknown_attrs and discard_unknown_attrs:
raise ValueError(
@@ -66,11 +69,12 @@ class _Inputter:
outcls: type[Any]
# If we're dealing with a multi-type subclass which is NOT a
- # dataclass, we must rely on its stored type to figure out
- # what type of dataclass we're going to. If we are a dataclass
- # then we already know what type we're going to so we can
- # survive without this, which is often necessary when reading
- # old data that doesn't have a type id attr yet.
+ # dataclass (generally a custom multitype base class), then we
+ # must rely on its stored type enum to figure out what type of
+ # dataclass we're going to create. If we *are* dealing with a
+ # dataclass then we already know what type we're going to so we
+ # can survive without this, which is often necessary when
+ # reading old data that doesn't have a type id attr yet.
if issubclass(self._cls, IOMultiType) and not dataclasses.is_dataclass(
self._cls
):
@@ -81,7 +85,40 @@ class _Inputter:
f' {values}.'
)
type_id_enum = self._cls.get_type_id_type()
- enum_val = type_id_enum(type_id_val)
+ try:
+ enum_val = type_id_enum(type_id_val)
+ except ValueError as exc:
+
+ # Check the fallback even if not in lossy mode, as we
+ # inform the user of its existence in errors in that
+ # case.
+ fallback = self._cls.get_unknown_type_fallback()
+
+ # Sanity check that fallback is correct type.
+ assert isinstance(fallback, self._cls | None)
+
+ # If we're in lossy mode, provide the fallback value.
+ if self._lossy:
+ if fallback is not None:
+ # Ok; they provided a fallback. Flag it as lossy
+ # to prevent it from being written back out by
+ # default, and return it.
+ setattr(fallback, LOSSY_ATTR, True)
+ return fallback
+ else:
+ # If we're *not* in lossy mode, inform the user if
+ # we *would* have succeeded if we were. This is
+ # useful for debugging these sorts of situations.
+ if fallback is not None:
+ raise ValueError(
+ 'Failed loading unrecognized multitype object.'
+ ' Note that the multitype provides a fallback'
+ ' and thus would succeed in lossy mode.'
+ ) from exc
+
+ # Otherwise the error stands as-is.
+ raise
+
outcls = self._cls.get_type(enum_val)
else:
outcls = self._cls
@@ -100,6 +137,17 @@ class _Inputter:
if is_ext:
out.did_input()
+ # If we're running in lossy mode, flag the object as such so we
+ # don't allow writing it back out and potentially accidentally
+ # losing data.
+ #
+ # FIXME - We are currently only flagging this at the top level,
+ # but this will not prevent sub-objects from being written out.
+ # Is that worth worrying about? Though perfect is the enemy of
+ # good I suppose.
+ if self._lossy:
+ setattr(out, LOSSY_ATTR, True)
+
return out
def _value_from_input(
@@ -183,22 +231,28 @@ class _Inputter:
# dataclass (all dataclasses inheriting from the multi-type
# should just be processed as dataclasses).
if issubclass(origin, IOMultiType):
- return self._dataclass_from_input(
- _get_multitype_type(anntype, fieldpath, value),
- fieldpath,
- value,
- )
+ return self._multitype_obj(anntype, fieldpath, value)
if issubclass(origin, Enum):
try:
return origin(value)
- except ValueError:
- # If a fallback enum was provided in ioattrs, return
- # that for unrecognized values.
+ except ValueError as exc:
+ # If a fallback enum was provided in ioattrs AND we're
+ # in lossy mode, return that for unrecognized values. If
+ # one was provided but we're *not* in lossy mode, note
+ # that we could have loaded it if lossy mode was
+ # enabled.
if ioattrs is not None and ioattrs.enum_fallback is not None:
+ # Sanity check; make sure fallback is valid.
assert type(ioattrs.enum_fallback) is origin
- return ioattrs.enum_fallback
- # Otherwise the error stands.
+ if self._lossy:
+ return ioattrs.enum_fallback
+ raise ValueError(
+ 'Failed to load Enum. Note that it has a fallback'
+ ' value and thus would succeed in lossy mode.'
+ ) from exc
+
+ # Otherwise the error stands as-is.
raise
if issubclass(origin, datetime.datetime):
@@ -243,16 +297,17 @@ class _Inputter:
"""Given a dict, instantiates a dataclass of the given type.
The dict must be in the json-friendly format as emitted from
- dataclass_to_dict. This means that sequence values such as tuples or
- sets should be passed as lists, enums should be passed as their
- associated values, and nested dataclasses should be passed as dicts.
+ dataclass_to_dict. This means that sequence values such as
+ tuples or sets should be passed as lists, enums should be passed
+ as their associated values, and nested dataclasses should be
+ passed as dicts.
"""
try:
return self._do_dataclass_from_input(cls, fieldpath, values)
except Exception as exc:
- # Extended data types can choose to sub default data in case
- # of failures (generally not a good idea but occasionally
- # useful).
+ # Extended data types can choose to substitute default data
+ # in case of failures (generally not a good idea but
+ # occasionally useful).
if issubclass(cls, IOExtendedData):
fallback = cls.handle_input_error(exc)
if fallback is None:
@@ -304,8 +359,8 @@ class _Inputter:
# However we do want to make sure the class we're loading
# doesn't itself use this same name, as this could lead to
# tricky breakage. We can't verify this for types at prep
- # time because IOMultiTypes are lazy-loaded, so this is
- # the best we can do.
+ # time because IOMultiTypes are lazy-loaded, so this is the
+ # best we can do.
if type_id_store_name in fields_by_name:
raise RuntimeError(
f"{cls} contains a '{type_id_store_name}' field"
@@ -574,12 +629,7 @@ class _Inputter:
# values to determine which type to load for each element.
if issubclass(childanntype, IOMultiType):
return seqtype(
- self._dataclass_from_input(
- _get_multitype_type(childanntype, fieldpath, i),
- fieldpath,
- i,
- )
- for i in value
+ self._multitype_obj(childanntype, fieldpath, i) for i in value
)
return seqtype(
@@ -587,6 +637,21 @@ class _Inputter:
for i in value
)
+ def _multitype_obj(self, anntype: Any, fieldpath: str, value: Any) -> Any:
+ try:
+ mttype = _get_multitype_type(anntype, fieldpath, value)
+ except ValueError:
+ if self._lossy:
+ out = anntype.get_unknown_type_fallback()
+ if out is not None:
+ # Ok; they provided a fallback. Make sure its of our
+ # expected type and return it.
+ assert isinstance(out, anntype)
+ return out
+ raise
+
+ return self._dataclass_from_input(mttype, fieldpath, value)
+
def _tuple_from_input(
self,
cls: type,
diff --git a/tools/efro/dataclassio/_outputter.py b/tools/efro/dataclassio/_outputter.py
index df6b72ab..e3404bda 100644
--- a/tools/efro/dataclassio/_outputter.py
+++ b/tools/efro/dataclassio/_outputter.py
@@ -21,6 +21,7 @@ from efro.dataclassio._base import (
Codec,
_parse_annotated,
EXTRA_ATTRS_ATTR,
+ LOSSY_ATTR,
_is_valid_for_codec,
_get_origin,
SIMPLE_TYPES,
@@ -61,6 +62,14 @@ class _Outputter:
# isinstance call below fails.
assert dataclasses.is_dataclass(self._obj)
+ # If this data has been flagged as lossy, don't allow outputting
+ # it. This hopefully helps avoid unintentional data
+ # modification/loss.
+ if getattr(obj, LOSSY_ATTR, False):
+ raise ValueError(
+ 'Object has been flagged as lossy; output is disallowed.'
+ )
+
# For special extended data types, call their 'will_output' callback.
# FIXME - should probably move this into _process_dataclass so it
# can work on nested values.
diff --git a/tools/efro/dataclassio/templatemultitype.py b/tools/efro/dataclassio/templatemultitype.py
new file mode 100644
index 00000000..9e918532
--- /dev/null
+++ b/tools/efro/dataclassio/templatemultitype.py
@@ -0,0 +1,63 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Template for an IOMultitype setup.
+
+To use this template, simply copy the contents of this module somewhere
+and then replace 'TemplateMultiType' with 'YourType'.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, assert_never, override
+
+from enum import Enum
+from dataclasses import dataclass
+
+from efro.dataclassio import ioprepped, IOMultiType
+
+if TYPE_CHECKING:
+ pass
+
+
+class TemplateMultiTypeTypeID(Enum):
+ """Type ID for each of our subclasses."""
+
+ TEST = 'test'
+
+
+class TemplateMultiType(IOMultiType[TemplateMultiTypeTypeID]):
+ """Top level class for our multitype."""
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> TemplateMultiTypeTypeID:
+ # Require child classes to supply this themselves. If we did a
+ # full type registry/lookup here it would require us to import
+ # everything and would prevent lazy loading.
+ raise NotImplementedError()
+
+ @override
+ @classmethod
+ def get_type(
+ cls, type_id: TemplateMultiTypeTypeID
+ ) -> type[TemplateMultiType]:
+ """Return the subclass for each of our type-ids."""
+ # pylint: disable=cyclic-import
+
+ t = TemplateMultiTypeTypeID
+ if type_id is t.TEST:
+ return Test
+
+ # Important to make sure we provide all types.
+ assert_never(type_id)
+
+
+@ioprepped
+@dataclass
+class Test(TemplateMultiType):
+ """Just a test."""
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> TemplateMultiTypeTypeID:
+ return TemplateMultiTypeTypeID.TEST
diff --git a/tools/efro/message/_protocol.py b/tools/efro/message/_protocol.py
index 1d18ea57..8f0c3c36 100644
--- a/tools/efro/message/_protocol.py
+++ b/tools/efro/message/_protocol.py
@@ -294,7 +294,14 @@ class MessageProtocol:
raise UnregisteredMessageIDError(
f'Got unregistered {opname} id of {m_id}.'
)
- return dataclass_from_dict(msgtype, msgdict)
+
+ # Explicitly allow any fallbacks we define for our enums and
+ # multitypes. This allows us to build message types that remain
+ # loadable even when containing unrecognized future
+ # enums/multitype data. Be aware that this flags the object as
+ # 'lossy' however which prevents it from being reserialized by
+ # default.
+ return dataclass_from_dict(msgtype, msgdict, lossy=True)
def _get_module_header(
self,
diff --git a/tools/efro/util.py b/tools/efro/util.py
index 16acc167..dfc85e35 100644
--- a/tools/efro/util.py
+++ b/tools/efro/util.py
@@ -6,6 +6,7 @@ from __future__ import annotations
import os
import time
+import random
import weakref
import functools
import datetime
@@ -75,8 +76,6 @@ def explicit_bool(val: bool) -> bool:
# pylint: disable=no-else-return
if TYPE_CHECKING:
# infer this!
- import random
-
return random.random() < 0.5
else:
return val
@@ -982,3 +981,15 @@ def extract_arg(
del args[argindex : argindex + 2]
return val
+
+
+def weighted_choice(*args: tuple[T, float]) -> T:
+ """Given object/weight pairs as args, returns a random object.
+
+ Intended as a shorthand way to call random.choices on a few explicit
+ options.
+ """
+ items: tuple[T]
+ weights: tuple[float]
+ items, weights = zip(*args)
+ return random.choices(items, weights=weights)[0]