diff --git a/.efrocachemap b/.efrocachemap
index 205b29dd..abb26d0d 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -421,9 +421,9 @@
"build/assets/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/74/be/fe45a8417e95b6a2233c51992a26",
"build/assets/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/48/ab/8cddfcde36a750856f3f81dd20c8",
"build/assets/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/2b/46/8aedfa8741090247f04eb9e6df55",
- "build/assets/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/b8/5e/c8766634397fb77ae3a407c05d63",
- "build/assets/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/6f/38/958616d8cb85916aa8b2bcd84f63",
- "build/assets/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/57/68/d03a19b9035cfae7cdc5377d889a",
+ "build/assets/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/85/81/9425f358eafcd41e91fb999612ba",
+ "build/assets/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/db/96/1f7fe0541a31880929e1c17ea957",
+ "build/assets/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/5e/37/3ddcfa6e1f771b74c02298a6599a",
"build/assets/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/b6/00/924583b899165757f412eef0dd01",
"build/assets/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/3f/e9/60a8f0ca529aa57b4f9cb7385abc",
"build/assets/ba_data/data/languages/croatian.json": "https://files.ballistica.net/cache/ba1/76/65/32c67af5bd0144c2d63cab0516fa",
@@ -432,30 +432,30 @@
"build/assets/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/22/b4/4a33bf81142ba2befad14eb5746e",
"build/assets/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/d1/6e/8899211693c20d3b00fc198f58c6",
"build/assets/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/0e/39/7cfa5f3fb8cef5f4a64f21cda880",
- "build/assets/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/cb/49/1739273c68c82cebca0aee16d6c9",
+ "build/assets/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/58/f3/63cfd8a3ccf0c904ab753d95789b",
"build/assets/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/51/89/e01389f8153497b56fbf0fa069c2",
"build/assets/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/54/97/54d2a530d825200c6126be56df5c",
"build/assets/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/23/6f/8547ba09722b7c7f5b8333986984",
"build/assets/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/a6/5d/78f912e9a89f98de004405167a6a",
"build/assets/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/88/ee/0cda537bab9ac827def5e236fe1a",
- "build/assets/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/a9/b5/10de2f3928d8c1f4887e0975743f",
+ "build/assets/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/79/6a/290a8c44a1e7635208c2ff5fdc6e",
"build/assets/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/58/3b/ae1ecc04375cee089a82359110b7",
- "build/assets/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/67/44/40ada7b8e76adceb2129d7668df6",
+ "build/assets/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/ce/99/027a5d1af17689b4311eafa5ff24",
"build/assets/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/bd/c1/3f8632adda5517059323d928f192",
"build/assets/ba_data/data/languages/malay.json": "https://files.ballistica.net/cache/ba1/83/25/62ce997fc70704b9234c95fb2e38",
- "build/assets/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/0e/46/0cb71876e02d361e11db64640831",
+ "build/assets/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/51/19/aec9cbb2f8d00f2afaccf5fd5410",
"build/assets/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/a6/a7/4a9a289fa1b97847c9a2578c112b",
"build/assets/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/99/b2/7c598c90fd522132af3536aef0ee",
"build/assets/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/ae/eb/dd54f65939c2facc6ac50c117826",
"build/assets/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/aa/99/f9f597787fe4e09c8ab53fe2e081",
"build/assets/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/d7/45/2dd72ac0e51680cb39b5ebaa1c69",
"build/assets/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/27/96/2d53dc3f7dd4e877cd40faafeeef",
- "build/assets/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/45/dd/ce6d9dd446293f5e0ae541f36943",
+ "build/assets/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/87/76/884442afd08b31364dc6a8b0cc16",
"build/assets/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/77/d6/71f10613291ebf9c71da66f18a18",
- "build/assets/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/c7/fc/5ed7bd686839ec1a867763248cf9",
+ "build/assets/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/b9/d4/b4e107456ea6420ee0f9d9d7a03e",
"build/assets/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/33/f6/3753c9af9a5b238d229a0bf23fbc",
"build/assets/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/0a/97/f1f948f6587ea7d40b639aba67ce",
- "build/assets/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/97/4a/399422e3061fdd82f66591283397",
+ "build/assets/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/f7/2e/b51abfbbb56e27866895d7e947d2",
"build/assets/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/b0/e3/d73ccf96c5fa490a54f090ee77a5",
"build/assets/ba_data/data/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/92/1c/d1e50f60fe3e101f246e172750ba",
"build/assets/ba_data/data/maps/big_g.json": "https://files.ballistica.net/cache/ba1/1d/d3/01d490643088a435ce75df971054",
@@ -4068,50 +4068,50 @@
"build/assets/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/2d/ef/5335207d41b21b9823f6805997f1",
"build/assets/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/b0/8a/55e2e77623fe657bea24f223a3ae",
"build/assets/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/86/5b/2af4d1e26a1a8073c89acb06e599",
- "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/6b/a1/5f560a97ab8641091343c6ee688b",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/50/dd/86cbb96aca3a339318b00574b2db",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/6b/b4/65070558df0a917c9a1aac8bb280",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/93/04/19410cb96b5c12fc2cd20dd9c099",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/36/69/25b4f3e931ff0add15a975383491",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/16/68/6011835e4db7927b26761847950b",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/11/bf/aa9df1fd5ae51e9b076a324d8e7a",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/8a/76/54da9b7ff4d79164d3f4dea2782b",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/4a/08/bf75de3244efe6fc342139a6da32",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/33/c7/d3534c1d605b5bcc4a541457cde9",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/0e/58/c7da77e4c0d031073e4db047e4f3",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/f5/be/f80777972954ebe6fd91b52a6533",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/e4/20/0f1e3a2e343e48dbe3c3ae8c6ab7",
- "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/23/46/72f453ea380bd5f04957886c2b57",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/f8/e3/b7bf2bdd4fe4879e8f95bc56073c",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/b5/e6/451f3cc73b5b79d21b19c2416d61",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/b2/0e/778420dfd1a6f81ed457a94f8f1d",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/00/76/0f4dd3bbf7a98f00221307535b4f",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/4d/fe/1d4e9c927e74f900766cb3d3c55f",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/82/99/e0a873f37e95674f2151ea99bf3d",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/b3/a2/5da0c4dc65f469e4a476e0395eb5",
- "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/a9/9a/adb83188f9c7d7b51dafd0f8b8a8",
- "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/b3/a2/5da0c4dc65f469e4a476e0395eb5",
- "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/a9/9a/adb83188f9c7d7b51dafd0f8b8a8",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/27/f4/269f5d37a8e3938c0acdab299833",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/7b/f3/f98278c9654a972baf65d5f04c12",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/27/f4/269f5d37a8e3938c0acdab299833",
- "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/7b/f3/f98278c9654a972baf65d5f04c12",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/c7/a2/40728a3ebfb3006c7a47b698214f",
- "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/84/19/a1bbbf42c50329f0cd1377d103bb",
- "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/c7/a2/40728a3ebfb3006c7a47b698214f",
- "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/84/19/a1bbbf42c50329f0cd1377d103bb",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/53/e4/455c68ee50813fe89e3002cf1fe8",
- "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/59/fe/a3e369f2db87a305641e74ae70ab",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/be/df/60063a6845e8654958f1a3e37867",
- "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/59/fe/a3e369f2db87a305641e74ae70ab",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/91/2f/362a643d543963de549d830fe604",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/2e/d4/67d6c0f9b372eb5cd92c9def6fc8",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/68/8b/d6049425f1069d256abdaf90004c",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/3e/82/f70e75696765ac05875cb5dd778c",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/38/ee/56658557aa2ecabd0d30eb01b68e",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/6b/4c/568766d02bbca174752488850737",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/3e/5b/1aa2252706188de69075eb7b3656",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/ab/a7/21bc5acc8a823bd7c7ec940e97bc",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/3a/79/8c3f90efa237a22e105a984ddc69",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/3f/fe/4c9f0926b60c6a4f5253fde8bb35",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/98/07/9edc3796de01efe4f2fc30d3fcc9",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/37/ba/ee706254c5a21f08b8d4c47bb6e2",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/1f/9a/33ed6f8e2b78af7b00b2727c6630",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/11/f3/0feed20f5d34c23d96b39e5702ae",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/4d/f8/c574339560c3f324d258d73dbe70",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/bc/b1/66680fead22bdecfcd4c8d825f8b",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/79/44/6cddf6fe194de66ab9576d1d958d",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/ef/1c/ddcdbedb47affd72d2f4c5164fd9",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/4a/ed/e7b293c3d484257e9c4a6c4b5ce1",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/c1/ed/4691901b692a59e598bd982f6d5b",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/02/74/f6091bf03a2de2690ac9de36391a",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/fa/8c/79e39ee8297c9946f3dfed5d47c4",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/f0/c4/0554dbc45078ce12e3ab1ab00667",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/2a/c3/4378d0290b122a345787b46ef23c",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/f1/6a/dcbae2252ce071fd07a350615351",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/c2/4b/04f9d9a58fa1d1cefc096cf84e57",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/61/8c/ec3c377382be6503836613fe0ae0",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/65/8f/25f37b8f6175fbe9274576e2eb4a",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/50/4e/39e4ff62ab5c2c3e72862c2c0881",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/9d/2b/44367bfb114e7b1148e4e100acd1",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/50/4e/39e4ff62ab5c2c3e72862c2c0881",
+ "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/9d/2b/44367bfb114e7b1148e4e100acd1",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/eb/1a/21761c11e160525903190cfe4ed3",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/a3/38/2c4abbc88434720311048185fdd1",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/eb/1a/21761c11e160525903190cfe4ed3",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/a3/38/2c4abbc88434720311048185fdd1",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/5f/47/5e5c2e910c783b15c6dcf0d78715",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/75/af/1ca3bec27e72101585254739189d",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/5f/47/5e5c2e910c783b15c6dcf0d78715",
+ "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/75/af/1ca3bec27e72101585254739189d",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/0b/6e/67ffc264f249cde0044637982bab",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/06/ab/699f575fabf3819dcf40b4c3125e",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/b3/27/a8e1a856695454d8b37a7d4f3604",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/06/ab/699f575fabf3819dcf40b4c3125e",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/b0/37/db1f330cc6d7d6cf987ff55b9ed4",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/b0/55/536be0beef09a49ecca957ca7318",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/d1/06/f341df353f2df7dbcbc80e2f3987",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/bd/f3/6f99da945c68fae7f860e69f86c6",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/ea/2d/ca8db3623be87b383486f905f41c",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/1e/fa/09a99039de3bf810e3ed13959416",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/cb/b0/f146f9d033d82a1d03496360001c",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/e1/5d/453846d79912b5a9a82cb5f8e2f5",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "https://files.ballistica.net/cache/ba1/f8/85/fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "https://files.ballistica.net/cache/ba1/f8/cd/3af311ac63147882590123b78318",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "https://files.ballistica.net/cache/ba1/ee/dd/ad968b176000e31c65be6206a2bc",
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index 6304dd3b..3e3ab804 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -125,6 +125,7 @@
appletvsimulator
appmode
appmodeselector
+ appmodule
appname
appnameupper
appnow
@@ -904,6 +905,7 @@
enumvalue
enval
envcfg
+ envconfig
envglobals
envhash
envname
@@ -1892,6 +1894,7 @@
namedarg
namel
namepre
+ namepretty
nametext
nameu
nameval
@@ -2432,6 +2435,7 @@
redist
redistributables
reenabled
+ reexpose
regionid
registerexecutionpolicyexception
registerwithlaunchservices
diff --git a/.idea/inspectionProfiles/Default.xml b/.idea/inspectionProfiles/Default.xml
index ff8542f6..f2590100 100644
--- a/.idea/inspectionProfiles/Default.xml
+++ b/.idea/inspectionProfiles/Default.xml
@@ -113,6 +113,8 @@
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b044686..a2e55376 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
-### 1.7.24 (build 21196, api 8, 2023-07-25)
+### 1.7.24 (build 21199, api 8, 2023-07-27)
+- Fixed an issue where respawn icons could disappear in epic mode (Thanks for
+ the heads-up Rikko!)
+- The `BA_ENABLE_IRONY_BUILD_DB` optional build env-var is now
+ `BA_ENABLE_COMPILE_COMMANDS_DB` since this same functionality can be used by
+ clangd or other tools. Originally I was using it for Irony for Emacs; hence
+ the old name.
- Due to the cleanup done in 1.7.20, it is now possible to build and run
Ballistica as a 'pure' Python app consisting of binary Python modules loaded
by a standard Python interpreter. This new build style is referred to as
diff --git a/Makefile b/Makefile
index 18de0bc9..cd5af619 100644
--- a/Makefile
+++ b/Makefile
@@ -28,11 +28,11 @@
help:
@tools/pcommand makefile_target_list Makefile
-# Set env-var BA_ENABLE_IRONY_BUILD_DB=1 to enable creating/updating a cmake
-# compile-commands database for use with irony for emacs (and possibly other
-# tools).
-ifeq ($(BA_ENABLE_IRONY_BUILD_DB),1)
- PREREQ_IRONY_BUILD_DB = .cache/irony/compile_commands.json
+# Set env-var BA_ENABLE_COMPILE_COMMANDS_DB=1 to enable creating/updating a
+# cmake compile-commands database for use with irony for emacs (and possibly
+# other tools).
+ifeq ($(BA_ENABLE_COMPILE_COMMANDS_DB),1)
+ PREREQ_COMPILE_COMMANDS_DB = .cache/compile_commands_db/compile_commands.json
endif
# Prereq targets that should be safe to run anytime; even if project-files
@@ -45,7 +45,7 @@ PREREQS_SAFE = .cache/checkenv .dir-locals.el .mypy.ini .pycheckers .pylintrc \
# fail if the CMakeList files don't match what's on disk. If such a target was
# included in PREREQS_SAFE it would try to build *before* project updates
# which would leave us stuck in a broken state.
-PREREQS_POST_UPDATE_ONLY = $(PREREQ_IRONY_BUILD_DB)
+PREREQS_POST_UPDATE_ONLY = $(PREREQ_COMPILE_COMMANDS_DB)
# Target that should be built before running most any other build.
# This installs tool config files, runs environment checks, etc.
@@ -1010,20 +1010,20 @@ CMAKE_BUILD_TYPE ?= Debug
# Build and run the cmake build.
cmake: cmake-build
- @cd build/cmake/$(CM_BT_LC)/staged && ./ballisticakit
+ cd build/cmake/$(CM_BT_LC)/staged && ./ballisticakit
# Build and run the cmake build under the gdb debugger.
# Sets up the ballistica environment to do things like abort() out to the
# debugger on errors instead of trying to cleanly exit.
cmake-gdb: cmake-build
- @cd build/cmake/$(CM_BT_LC)/staged && \
+ cd build/cmake/$(CM_BT_LC)/staged && \
BA_DEBUGGER_ATTACHED=1 gdb ./ballisticakit
# Build and run the cmake build under the lldb debugger.
# Sets up the ballistica environment to do things like abort() out to the
# debugger on errors instead of trying to cleanly exit.
cmake-lldb: cmake-build
- @cd build/cmake/$(CM_BT_LC)/staged && \
+ cd build/cmake/$(CM_BT_LC)/staged && \
BA_DEBUGGER_ATTACHED=1 lldb ./ballisticakit
# Build but don't run it.
@@ -1045,7 +1045,7 @@ cmake-clean:
rm -rf build/cmake/$(CM_BT_LC)
cmake-server: cmake-server-build
- @cd build/cmake/server-$(CM_BT_LC)/staged && ./ballisticakit_server
+ cd build/cmake/server-$(CM_BT_LC)/staged && ./ballisticakit_server
cmake-server-build: assets-server meta cmake-server-binary
@$(STAGE_BUILD) -cmakeserver -$(CM_BT_LC) \
@@ -1074,7 +1074,7 @@ cmake-modular-build: assets-cmake meta cmake-modular-binary
Modular build complete: BLU build/cmake/modular-$(CM_BT_LC)/staged
cmake-modular: cmake-modular-build
- @cd build/cmake/modular-$(CM_BT_LC)/staged && ./ballisticakit
+ cd build/cmake/modular-$(CM_BT_LC)/staged && ./ballisticakit
cmake-modular-binary: meta
@tools/pcommand cmake_prep_dir build/cmake/modular-$(CM_BT_LC)
@@ -1089,7 +1089,7 @@ cmake-modular-clean:
rm -rf build/cmake/modular-$(CM_BT_LC)
cmake-modular-server: cmake-modular-server-build
- @cd build/cmake/modular-server-$(CM_BT_LC)/staged && ./ballisticakit_server
+ cd build/cmake/modular-server-$(CM_BT_LC)/staged && ./ballisticakit_server
cmake-modular-server-build: assets-server meta cmake-modular-server-binary
@$(STAGE_BUILD) -cmakemodularserver -$(CM_BT_LC) \
@@ -1221,21 +1221,21 @@ ballisticakit-cmake/.clang-format: .clang-format
@mkdir -p ballisticakit-cmake
@cd ballisticakit-cmake && ln -sf ../.clang-format .
-# Irony in emacs requires us to use cmake to generate a full
-# list of compile commands for all files; lets try to keep it up to date
+# Various tools such as Irony for Emacs or clangd make use of a list of
+# compile commands for all files; lets try to keep it up to date
# whenever CMakeLists changes.
-.cache/irony/compile_commands.json: ballisticakit-cmake/CMakeLists.txt
- @tools/pcommand echo BLU Updating Irony build commands db...
- @echo Generating Irony compile-commands-list...
- @mkdir -p .cache/irony
- @cd .cache/irony \
+.cache/compile_commands_db/compile_commands.json: \
+ ballisticakit-cmake/CMakeLists.txt
+ @tools/pcommand echo BLU Updating compile commands db...
+ @mkdir -p .cache/compile_commands_db
+ @cd .cache/compile_commands_db \
&& cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Debug \
$(shell pwd)/ballisticakit-cmake
- @mv .cache/irony/compile_commands.json . \
- && rm -rf .cache/irony \
- && mkdir .cache/irony \
- && mv compile_commands.json .cache/irony
- @tools/pcommand echo BLU Created Irony build db at $@
+ @mv .cache/compile_commands_db/compile_commands.json . \
+ && rm -rf .cache/compile_commands_db \
+ && mkdir .cache/compile_commands_db \
+ && mv compile_commands.json .cache/compile_commands_db
+ @tools/pcommand echo BLU Created compile commands db at $@
_windows-wsl-build:
@tools/pcommand wsl_build_check_win_drive
diff --git a/ballisticakit-cmake/.idea/dictionaries/ericf.xml b/ballisticakit-cmake/.idea/dictionaries/ericf.xml
index ed693e85..965b454f 100644
--- a/ballisticakit-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticakit-cmake/.idea/dictionaries/ericf.xml
@@ -83,6 +83,7 @@
appletvsimulator
appmode
appmodeselector
+ appmodule
appname
appnameupper
appnow
@@ -549,6 +550,7 @@
enumvalue
enval
envcfg
+ envconfig
envglobals
envs
envval
@@ -1102,6 +1104,7 @@
namecap
namel
namepre
+ namepretty
nameu
nameval
nbuffer
@@ -1430,6 +1433,7 @@
recvfrom
redundants
reenabled
+ reexpose
refcounted
refl
regionid
diff --git a/config/toolconfigsrc/dir-locals.el b/config/toolconfigsrc/dir-locals.el
index 30c7cf5a..a1859328 100644
--- a/config/toolconfigsrc/dir-locals.el
+++ b/config/toolconfigsrc/dir-locals.el
@@ -2,7 +2,8 @@
;;; For more information see (info "(emacs) Directory Variables")
;;; Turn flycheck mode on for our c++ stuff and tell jedi where to look for our python stuff.
-((c++-mode (eval . (flycheck-mode)))
+(
+ ;; (c++-mode (eval . (flycheck-mode)))
(python-ts-mode (jedi:server-args . ("--sys-path" "__EFRO_PROJECT_ROOT__/tools"
@@ -22,8 +23,18 @@
(nil . ((projectile-globally-ignored-directories . ("docs"
"submodules"
"src/external"
+ "src/assets/ba_data/python-site-packages"
"src/assets/pylib-android"
"src/assets/pylib-apple"
"src/assets/windows"))))
+ ;; Trying to get project.el to work the same as projectile since its built in.
+ (nil . ((project-vc-ignores . ("docs"
+ "submodules"
+ "src/external"
+ "src/assets/ba_data/python-site-packages"
+ "src/assets/pylib-android"
+ "src/assets/pylib-apple"
+ "src/assets/windows"))))
+
)
diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py
index 52bf7cb5..7388a683 100644
--- a/src/assets/ba_data/python/babase/__init__.py
+++ b/src/assets/ba_data/python/babase/__init__.py
@@ -41,6 +41,7 @@ from _babase import (
fade_screen,
fatal_error,
get_display_resolution,
+ get_immediate_return_code,
get_low_level_config_value,
get_max_graphics_quality,
get_replays_dir,
@@ -204,6 +205,7 @@ __all__ = [
'fatal_error',
'garbage_collect',
'get_display_resolution',
+ 'get_immediate_return_code',
'get_ip_address_type',
'get_low_level_config_value',
'get_max_graphics_quality',
diff --git a/src/assets/ba_data/python/babase/_env.py b/src/assets/ba_data/python/babase/_env.py
index 08b13d3b..fd426fcd 100644
--- a/src/assets/ba_data/python/babase/_env.py
+++ b/src/assets/ba_data/python/babase/_env.py
@@ -156,11 +156,11 @@ def on_app_launching() -> None:
assert _babase.in_logic_thread()
- # Let the user know if the app Python dir is a custom one.
- user_sys_scripts_dir = baenv.get_user_system_scripts_dir()
- if user_sys_scripts_dir is not None:
+ # Let the user know if the app Python dir is a 'user' one.
+ envconfig = baenv.get_config()
+ if envconfig.is_user_app_python_dir:
_babase.screenmessage(
- f"Using user system scripts: '{user_sys_scripts_dir}'",
+ f"Using user system scripts: '{envconfig.app_python_dir}'",
color=(0.6, 0.6, 1.0),
)
diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py
index 9ae293a2..0e613c7a 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -24,20 +24,19 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING
import __main__
-from efro.log import setup_logging, LogLevel
-
if TYPE_CHECKING:
from efro.log import LogHandler
# IMPORTANT - It is likely (and in some cases expected) that this
# module's code will be exec'ed multiple times. This is because it is
-# the job of this module to set up paths for an engine run, and that may
-# involve modifying sys.path in such a way that this module resolves to
-# a different path afterwards (for example from
+# the job of this module to set up Python paths for an engine run, and
+# that may involve modifying sys.path in such a way that this module
+# resolves to a different path afterwards (for example from
# /abs/path/to/ba_data/scripts/babase.py to ba_data/scripts/babase.py).
# This can result in the next import of baenv loading us from our 'new'
# location, which may or may not actually be the same file on disk as
-# the old. Either way, however, multiple execs will happen in some form.
+# the last load. Either way, however, multiple execs will happen in some
+# form.
#
# So we need to do a few things to handle that situation gracefully.
#
@@ -51,13 +50,13 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 21196
+TARGET_BALLISTICA_BUILD = 21199
TARGET_BALLISTICA_VERSION = '1.7.24'
@dataclass
class EnvConfig:
- """Environment put together by the configure call."""
+ """Environment values put together by the configure call."""
config_dir: str
data_dir: str
@@ -66,49 +65,45 @@ class EnvConfig:
standard_app_python_dir: str
site_python_dir: str | None
log_handler: LogHandler | None
+ is_user_app_python_dir: bool
@dataclass
-class EnvGlobals:
+class _EnvGlobals:
"""Our globals we store in the main module."""
config: EnvConfig | None = None
- config_called: bool = False
+ called_configure: bool = False
paths_set_failed: bool = False
- user_system_scripts_dir: str | None = None
+ modular_main_called: bool = False
@classmethod
- def get(cls) -> EnvGlobals:
+ def get(cls) -> _EnvGlobals:
"""Create/return our singleton."""
name = '_baenv_globals'
- envglobals: EnvGlobals | None = getattr(__main__, name, None)
+ envglobals: _EnvGlobals | None = getattr(__main__, name, None)
if envglobals is None:
- envglobals = EnvGlobals()
+ envglobals = _EnvGlobals()
setattr(__main__, name, envglobals)
return envglobals
+def did_paths_set_fail() -> bool:
+ """Did we try to set paths and failed?"""
+ return _EnvGlobals.get().paths_set_failed
+
+
def config_exists() -> bool:
"""Has a config been created?"""
- return EnvGlobals.get().config is not None
-
-
-def did_paths_set_fail() -> bool:
- """Did we try to set paths and failed?"""
- return EnvGlobals.get().paths_set_failed
-
-
-def get_user_system_scripts_dir() -> str | None:
- """If there's a custom user system scripts dir in play, return it."""
- return EnvGlobals.get().user_system_scripts_dir
+ return _EnvGlobals.get().config is not None
def get_config() -> EnvConfig:
"""Return the active config, creating a default if none exists."""
- envglobals = EnvGlobals.get()
+ envglobals = _EnvGlobals.get()
- if not envglobals.config_called:
+ if not envglobals.called_configure:
configure()
config = envglobals.config
@@ -128,7 +123,7 @@ def configure(
site_python_dir: str | None = None,
contains_python_dist: bool = False,
) -> None:
- """Set up the Python environment for running a ballistica app.
+ """Set up the Python environment for running a Ballistica app.
This includes things such as Python path wrangling and app directory
creation. This should be called before any other ballistica modules
@@ -136,14 +131,14 @@ def configure(
where those modules get loaded from.
"""
- envglobals = EnvGlobals.get()
+ envglobals = _EnvGlobals.get()
- if envglobals.config_called:
+ if envglobals.called_configure:
raise RuntimeError(
'baenv.configure() has already been called;'
' it can only be called once.'
)
- envglobals.config_called = True
+ envglobals.called_configure = True
# The very first thing we do is set up our logging system and pipe
# Python's stdout/stderr into it. Then we can at least debug
@@ -167,6 +162,7 @@ def configure(
data_dir,
config_dir,
standard_app_python_dir,
+ is_user_app_python_dir,
) = _setup_paths(
user_python_dir,
app_python_dir,
@@ -190,6 +186,7 @@ def configure(
standard_app_python_dir=standard_app_python_dir,
site_python_dir=site_python_dir,
log_handler=log_handler,
+ is_user_app_python_dir=is_user_app_python_dir,
)
@@ -215,6 +212,8 @@ def _calc_data_dir(data_dir: str | None) -> str:
def _setup_logging() -> LogHandler:
+ from efro.log import setup_logging, LogLevel
+
log_handler = setup_logging(
log_path=None,
level=LogLevel.DEBUG,
@@ -249,11 +248,11 @@ def _setup_paths(
site_python_dir: str | None,
data_dir: str | None,
config_dir: str | None,
-) -> tuple[str | None, str | None, str | None, str, str, str]:
+) -> tuple[str | None, str | None, str | None, str, str, str, bool]:
# First a few paths we can ALWAYS calculate since they don't affect
# Python imports:
- envglobals = EnvGlobals.get()
+ envglobals = _EnvGlobals.get()
data_dir = _calc_data_dir(data_dir)
@@ -264,6 +263,9 @@ def _setup_paths(
# Standard app-python-dir is simply ba_data/python under data-dir.
standard_app_python_dir = str(Path(data_dir, 'ba_data', 'python'))
+ # Whether the final app-dir we're returning is a custom user-owned one.
+ is_user_app_python_dir = False
+
# If _babase has already been imported, there's not much we can do
# at this point aside from complain and inform for next time.
if '_babase' in sys.modules:
@@ -299,7 +301,8 @@ def _setup_paths(
# stuff.
check_dir = Path(user_python_dir, 'sys', TARGET_BALLISTICA_VERSION)
if check_dir.is_dir():
- envglobals.user_system_scripts_dir = app_python_dir = str(check_dir)
+ app_python_dir = str(check_dir)
+ is_user_app_python_dir = True
# Ok, now apply these to sys.path.
@@ -342,6 +345,7 @@ def _setup_paths(
data_dir,
config_dir,
standard_app_python_dir,
+ is_user_app_python_dir,
)
@@ -361,16 +365,96 @@ def _setup_dirs(config_dir: str | None, user_python_dir: str | None) -> None:
)
-def _main() -> None:
- # Run a default configure BEFORE importing babase.
- # (may affect where babase comes from).
- configure()
+def extract_arg(args: list[str], names: list[str], is_dir: bool) -> str | None:
+ """Given a list of args and an arg name, returns a value.
- import babase
+ The arg flag and value are removed from the arg list. We also check
+ to make sure the path exists.
- babase.app.run()
+ raises CleanErrors on any problems.
+ """
+ from efro.error import CleanError
+
+ count = sum(args.count(n) for n in names)
+ if not count:
+ return None
+
+ if count > 1:
+ raise CleanError(f'Arg {names} passed multiple times.')
+
+ for name in names:
+ if name not in args:
+ continue
+ argindex = args.index(name)
+ if argindex + 1 >= len(args):
+ raise CleanError(f'No value passed after {name} arg.')
+
+ val = args[argindex + 1]
+ del args[argindex : argindex + 2]
+
+ if is_dir and not os.path.isdir(val):
+ namepretty = names[0].removeprefix('--')
+ raise CleanError(
+ f"Provided {namepretty} path '{val}' is not a directory."
+ )
+ return val
+
+ raise RuntimeError(f'Expected arg name not found from {names}')
-# Allow exec'ing this module directly to do a standard app run.
+def _modular_main() -> None:
+ from efro.error import CleanError
+
+ try:
+ # Take note that we're running via modular-main.
+ # The native layer can key off this to know whether it
+ # should apply sys.argv or not.
+ _EnvGlobals.get().modular_main_called = True
+
+ # We run configure() BEFORE importing babase. (part of its job
+ # is to set up where babase and everything else gets loaded
+ # from).
+
+ # We deal with a few key ours here ourself before spinning up
+ # any engine stuff.
+
+ # NOTE: Need to keep these arg long/short versions synced to
+ # those in core_config.cc since they parse the same values (they
+ # just don't handle them).
+
+ args = sys.argv.copy()
+
+ # Our -c arg basically mirrors Python's -c arg. If we get that,
+ # simply exec it and return; no engine stuff.
+ command = extract_arg(args, ['--command', '-c'], is_dir=False)
+ if command is not None:
+ exec(command) # pylint: disable=exec-used
+ return
+
+ config_dir = extract_arg(args, ['--config-dir', '-C'], is_dir=True)
+ data_dir = extract_arg(args, ['--data-dir', '-d'], is_dir=True)
+ mods_dir = extract_arg(args, ['--mods-dir', '-m'], is_dir=True)
+ configure(
+ config_dir=config_dir,
+ data_dir=data_dir,
+ user_python_dir=mods_dir,
+ )
+
+ import babase
+
+ # The engine will have parsed and processed all other args as
+ # part of the above import. If there were errors or args such as
+ # --help which should lead to immediate returns, do so.
+ code = babase.get_immediate_return_code()
+ if code is not None:
+ sys.exit(code)
+
+ babase.app.run()
+ except CleanError as clean_exc:
+ clean_exc.pretty_print()
+ sys.exit(1)
+
+
+# Exec'ing this module directly will do a standard app run.
if __name__ == '__main__':
- _main()
+ _modular_main()
diff --git a/src/assets/ba_data/python/bascenev1/__init__.py b/src/assets/ba_data/python/bascenev1/__init__.py
index 7b44deeb..77cd93e6 100644
--- a/src/assets/ba_data/python/bascenev1/__init__.py
+++ b/src/assets/ba_data/python/bascenev1/__init__.py
@@ -45,6 +45,7 @@ from babase import (
NodeNotFoundError,
normalized_color,
NotFoundError,
+ PlayerNotFoundError,
Plugin,
pushcall,
safecolor,
@@ -374,6 +375,7 @@ __all__ = [
'PlayerDiedMessage',
'PlayerProfilesChangedMessage',
'PlayerInfo',
+ 'PlayerNotFoundError',
'PlayerRecord',
'PlayerScoredMessage',
'Plugin',
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py b/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py
index 7e301cc6..64675338 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py
@@ -4,14 +4,8 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-
-import babase
import bascenev1 as bs
-if TYPE_CHECKING:
- pass
-
class CoopJoinActivity(bs.JoinActivity):
"""Join-screen for co-op mode."""
@@ -46,7 +40,7 @@ class CoopJoinActivity(bs.JoinActivity):
).autoretain()
ControlsGuide(delay=1.0).autoretain()
- babase.pushcall(self._show_remaining_achievements)
+ bs.pushcall(self._show_remaining_achievements)
def _show_remaining_achievements(self) -> None:
from bascenev1lib.actor.text import Text
@@ -70,22 +64,22 @@ class CoopJoinActivity(bs.JoinActivity):
ts_h_offs = 60
# Show remaining achievements in some cases.
- if babase.app.classic is not None and not (
- babase.app.demo_mode or babase.app.arcade_mode
+ if bs.app.classic is not None and not (
+ bs.app.demo_mode or bs.app.arcade_mode
):
achievements = [
a
- for a in babase.app.classic.ach.achievements_for_coop_level(
+ for a in bs.app.classic.ach.achievements_for_coop_level(
levelname
)
if not a.complete
]
have_achievements = bool(achievements)
achievements = [a for a in achievements if not a.complete]
- vrmode = babase.app.vr_mode
+ vrmode = bs.app.vr_mode
if have_achievements:
Text(
- babase.Lstr(resource='achievementsRemainingText'),
+ bs.Lstr(resource='achievementsRemainingText'),
host_only=True,
position=(ts_h_offs - 10, vpos),
transition=Text.Transition.FADE_IN,
@@ -105,7 +99,7 @@ class CoopJoinActivity(bs.JoinActivity):
vpos -= 55
if not achievements:
Text(
- babase.Lstr(resource='noAchievementsRemainingText'),
+ bs.Lstr(resource='noAchievementsRemainingText'),
host_only=True,
position=(ts_h_offs + 15, vpos + 10),
transition=Text.Transition.FADE_IN,
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/drawscore.py b/src/assets/ba_data/python/bascenev1lib/activity/drawscore.py
index 991e5289..2d260dfd 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/drawscore.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/drawscore.py
@@ -4,15 +4,9 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-
-import babase
+import bascenev1 as bs
from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity
from bascenev1lib.actor.zoomtext import ZoomText
-import bascenev1 as bs
-
-if TYPE_CHECKING:
- pass
class DrawScoreScreenActivity(MultiTeamScoreScreenActivity):
@@ -21,10 +15,10 @@ class DrawScoreScreenActivity(MultiTeamScoreScreenActivity):
default_music = None # Awkward silence...
def on_begin(self) -> None:
- babase.set_analytics_screen('Draw Score Screen')
+ bs.set_analytics_screen('Draw Score Screen')
super().on_begin()
ZoomText(
- babase.Lstr(resource='drawText'),
+ bs.Lstr(resource='drawText'),
position=(0, 0),
maxwidth=400,
shiftposition=(-220, 0),
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/dualteamscore.py b/src/assets/ba_data/python/bascenev1lib/activity/dualteamscore.py
index 8d665031..c369e17b 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/dualteamscore.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/dualteamscore.py
@@ -4,16 +4,10 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-
-import babase
import bascenev1 as bs
from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity
from bascenev1lib.actor.zoomtext import ZoomText
-if TYPE_CHECKING:
- pass
-
class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
"""Scorescreen between rounds of a dual-team session."""
@@ -24,7 +18,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
assert isinstance(self._winner, bs.SessionTeam)
def on_begin(self) -> None:
- babase.set_analytics_screen('Teams Score Screen')
+ bs.set_analytics_screen('Teams Score Screen')
super().on_begin()
height = 130
@@ -37,13 +31,13 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
# 'First to 4'.
session = self.session
assert isinstance(session, bs.MultiTeamSession)
- if babase.app.lang.get_resource('bestOfUseFirstToInstead'):
- best_txt = babase.Lstr(
+ if bs.app.lang.get_resource('bestOfUseFirstToInstead'):
+ best_txt = bs.Lstr(
resource='firstToSeriesText',
subs=[('${COUNT}', str(session.get_series_length() / 2 + 1))],
)
else:
- best_txt = babase.Lstr(
+ best_txt = bs.Lstr(
resource='bestOfSeriesText',
subs=[('${COUNT}', str(session.get_series_length()))],
)
@@ -63,7 +57,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
for team in self.session.sessionteams:
bs.timer(
i * 0.15 + 0.15,
- babase.WeakCall(
+ bs.WeakCall(
self._show_team_name,
vval - i * height,
team,
@@ -78,7 +72,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
delay = 1.2
bs.timer(
i * 0.150 + 0.2,
- babase.WeakCall(
+ bs.WeakCall(
self._show_team_old_score,
vval - i * height,
team,
@@ -89,7 +83,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
bs.timer(
i * 0.150 + delay,
- babase.WeakCall(
+ bs.WeakCall(
self._show_team_score,
vval - i * height,
team,
@@ -110,7 +104,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
) -> None:
del kill_delay # Unused arg.
ZoomText(
- babase.Lstr(value='${A}:', subs=[('${A}', team.name)]),
+ bs.Lstr(value='${A}:', subs=[('${A}', team.name)]),
position=(100, pos_v),
shiftposition=(-150, pos_v),
shiftdelay=shiftdelay,
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/multiteamjoin.py b/src/assets/ba_data/python/bascenev1lib/activity/multiteamjoin.py
index 28f4f978..2870c1fd 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/multiteamjoin.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/multiteamjoin.py
@@ -4,15 +4,9 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-
-import babase
import bascenev1 as bs
from bascenev1lib.actor.text import Text
-if TYPE_CHECKING:
- pass
-
class MultiTeamJoinActivity(bs.JoinActivity):
"""Join screen for teams sessions."""
@@ -32,10 +26,10 @@ class MultiTeamJoinActivity(bs.JoinActivity):
# Show info about the next up game.
self._next_up_text = Text(
- babase.Lstr(
+ bs.Lstr(
value='${1} ${2}',
subs=[
- ('${1}', babase.Lstr(resource='upFirstText')),
+ ('${1}', bs.Lstr(resource='upFirstText')),
('${2}', session.get_next_game_description()),
],
),
@@ -72,12 +66,12 @@ class MultiTeamJoinActivity(bs.JoinActivity):
).autoretain()
Text(
- babase.Lstr(
+ bs.Lstr(
resource='mustInviteFriendsText',
subs=[
(
'${GATHER}',
- babase.Lstr(resource='gatherWindow.titleText'),
+ bs.Lstr(resource='gatherWindow.titleText'),
)
],
),
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/multiteamscore.py b/src/assets/ba_data/python/bascenev1lib/activity/multiteamscore.py
index dfd498a1..ef2ba012 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/multiteamscore.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/multiteamscore.py
@@ -3,16 +3,10 @@
"""Functionality related to teams mode score screen."""
from __future__ import annotations
-from typing import TYPE_CHECKING
-
-import babase
import bascenev1 as bs
from bascenev1lib.actor.text import Text
from bascenev1lib.actor.image import Image
-if TYPE_CHECKING:
- pass
-
class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
"""Base class for score screens."""
@@ -28,12 +22,12 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
super().on_begin()
session = self.session
if self._show_up_next and isinstance(session, bs.MultiTeamSession):
- txt = babase.Lstr(
+ txt = bs.Lstr(
value='${A} ${B}',
subs=[
(
'${A}',
- babase.Lstr(
+ bs.Lstr(
resource='upNextText',
subs=[
('${COUNT}', str(session.get_game_number() + 1))
@@ -84,7 +78,7 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
return val
return p_rec.accumscore
- def _get_prec_score_str(p_rec: bs.PlayerRecord) -> str | babase.Lstr:
+ def _get_prec_score_str(p_rec: bs.PlayerRecord) -> str | bs.Lstr:
if is_free_for_all and results is not None:
assert isinstance(results, bs.GameResults)
assert p_rec.team.activityteam is not None
@@ -135,7 +129,7 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
def _txt(
xoffs: float,
yoffs: float,
- text: babase.Lstr,
+ text: bs.Lstr,
h_align: Text.HAlign = Text.HAlign.RIGHT,
extrascale: float = 1.0,
maxwidth: float | None = 120.0,
@@ -157,7 +151,7 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
session = self.session
assert isinstance(session, bs.MultiTeamSession)
- tval = babase.Lstr(
+ tval = bs.Lstr(
resource='gameLeadersText',
subs=[('${COUNT}', str(session.get_game_number()))],
)
@@ -169,14 +163,12 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
extrascale=1.4,
maxwidth=None,
)
- _txt(
- -15, 4, babase.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT
- )
- _txt(180, 4, babase.Lstr(resource='killsText'))
- _txt(280, 4, babase.Lstr(resource='deathsText'), maxwidth=100)
+ _txt(-15, 4, bs.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT)
+ _txt(180, 4, bs.Lstr(resource='killsText'))
+ _txt(280, 4, bs.Lstr(resource='deathsText'), maxwidth=100)
score_label = 'Score' if results is None else results.score_label
- translated = babase.Lstr(translate=('scoreNames', score_label))
+ translated = bs.Lstr(translate=('scoreNames', score_label))
_txt(390, 0, translated)
@@ -191,7 +183,7 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
topkilledcount = min(topkilledcount, prec.accum_killed_count)
def _scoretxt(
- text: str | babase.Lstr,
+ text: str | bs.Lstr,
x_offs: float,
highlight: bool,
delay2: float,
@@ -228,7 +220,7 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
transition_delay=tdelay,
).autoretain()
Text(
- babase.Lstr(value=playerrec.getname(full=True)),
+ bs.Lstr(value=playerrec.getname(full=True)),
maxwidth=160,
scale=0.75 * scale,
position=(
@@ -237,7 +229,7 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
- color=babase.safecolor(playerrec.team.color + (1,)),
+ color=bs.safecolor(playerrec.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/multiteamvictory.py b/src/assets/ba_data/python/bascenev1lib/activity/multiteamvictory.py
index 64493e77..203f0516 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/multiteamvictory.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/multiteamvictory.py
@@ -4,15 +4,9 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-
-import babase
import bascenev1 as bs
from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity
-if TYPE_CHECKING:
- pass
-
class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
"""Final score screen for a team series."""
@@ -35,25 +29,25 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
from bascenev1lib.actor.text import Text
from bascenev1lib.actor.image import Image
- babase.set_analytics_screen(
+ bs.set_analytics_screen(
'FreeForAll Series Victory Screen'
if self._is_ffa
else 'Teams Series Victory Screen'
)
- assert babase.app.classic is not None
- if babase.app.ui_v1.uiscale is babase.UIScale.LARGE:
- sval = babase.Lstr(resource='pressAnyKeyButtonPlayAgainText')
+ assert bs.app.classic is not None
+ if bs.app.ui_v1.uiscale is bs.UIScale.LARGE:
+ sval = bs.Lstr(resource='pressAnyKeyButtonPlayAgainText')
else:
- sval = babase.Lstr(resource='pressAnyButtonPlayAgainText')
+ sval = bs.Lstr(resource='pressAnyButtonPlayAgainText')
self._show_up_next = False
self._custom_continue_message = sval
super().on_begin()
winning_sessionteam = self.settings_raw['winner']
# Pause a moment before playing victory music.
- bs.timer(0.6, babase.WeakCall(self._play_victory_music))
+ bs.timer(0.6, bs.WeakCall(self._play_victory_music))
bs.timer(
- 4.4, babase.WeakCall(self._show_winner, self.settings_raw['winner'])
+ 4.4, bs.WeakCall(self._show_winner, self.settings_raw['winner'])
)
bs.timer(4.6, self._score_display_sound.play)
@@ -82,19 +76,19 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
tval = 6.4
t_incr = 0.12
- always_use_first_to = babase.app.lang.get_resource(
+ always_use_first_to = bs.app.lang.get_resource(
'bestOfUseFirstToInstead'
)
session = self.session
if self._is_ffa:
assert isinstance(session, bs.FreeForAllSession)
- txt = babase.Lstr(
+ txt = bs.Lstr(
value='${A}:',
subs=[
(
'${A}',
- babase.Lstr(
+ bs.Lstr(
resource='firstToFinalText',
subs=[
(
@@ -115,12 +109,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
# they're not using this language. Should try to come up
# with a wording that works everywhere.
if always_use_first_to:
- txt = babase.Lstr(
+ txt = bs.Lstr(
value='${A}:',
subs=[
(
'${A}',
- babase.Lstr(
+ bs.Lstr(
resource='firstToFinalText',
subs=[
(
@@ -135,12 +129,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
],
)
else:
- txt = babase.Lstr(
+ txt = bs.Lstr(
value='${A}:',
subs=[
(
'${A}',
- babase.Lstr(
+ bs.Lstr(
resource='bestOfFinalText',
subs=[
(
@@ -173,7 +167,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
if not self._is_ffa:
Text(
- babase.Lstr(
+ bs.Lstr(
resource='gamesToText',
subs=[
('${WINCOUNT}', str(win_score)),
@@ -208,7 +202,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
break
if mvp is not None:
Text(
- babase.Lstr(resource='mostValuablePlayerText'),
+ bs.Lstr(resource='mostValuablePlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
@@ -228,13 +222,13 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
).autoretain()
assert mvp_name is not None
Text(
- babase.Lstr(value=mvp_name),
+ bs.Lstr(value=mvp_name),
position=(280, ts_height / 2 - 55 + 15 - 5),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=170,
scale=1.3,
- color=babase.safecolor(mvp.team.color + (1,)),
+ color=bs.safecolor(mvp.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
@@ -249,7 +243,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
most_kills = entry[2].kill_count
if mvp is not None:
Text(
- babase.Lstr(resource='mostViolentPlayerText'),
+ bs.Lstr(resource='mostViolentPlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
@@ -259,12 +253,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
transition_delay=tval,
).autoretain()
Text(
- babase.Lstr(
+ bs.Lstr(
value='(${A})',
subs=[
(
'${A}',
- babase.Lstr(
+ bs.Lstr(
resource='killsTallyText',
subs=[('${COUNT}', str(most_kills))],
),
@@ -289,12 +283,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
).autoretain()
assert mvp_name is not None
Text(
- babase.Lstr(value=mvp_name),
+ bs.Lstr(value=mvp_name),
position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=180,
- color=babase.safecolor(mvp.team.color + (1,)),
+ color=bs.safecolor(mvp.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
@@ -310,7 +304,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
most_killed = entry[2].killed_count
if mkp is not None:
Text(
- babase.Lstr(resource='mostViolatedPlayerText'),
+ bs.Lstr(resource='mostViolatedPlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
@@ -320,12 +314,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
transition_delay=tval,
).autoretain()
Text(
- babase.Lstr(
+ bs.Lstr(
value='(${A})',
subs=[
(
'${A}',
- babase.Lstr(
+ bs.Lstr(
resource='deathsTallyText',
subs=[('${COUNT}', str(most_killed))],
),
@@ -349,11 +343,11 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
).autoretain()
assert mkp_name is not None
Text(
- babase.Lstr(value=mkp_name),
+ bs.Lstr(value=mkp_name),
position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
- color=babase.safecolor(mkp.team.color + (1,)),
+ color=bs.safecolor(mkp.team.color + (1,)),
maxwidth=180,
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
@@ -363,7 +357,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
# Now show individual scores.
tdelay = tval
Text(
- babase.Lstr(resource='finalScoresText'),
+ bs.Lstr(resource='finalScoresText'),
color=(0.5, 0.5, 0.5, 1.0),
position=(ts_h_offs, ts_height / 2),
transition=Text.Transition.IN_RIGHT,
@@ -396,17 +390,17 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
transition_delay=tdelay,
).autoretain()
Text(
- babase.Lstr(value=name),
+ bs.Lstr(value=name),
position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=180,
- color=babase.safecolor(prec.team.color + (1,)),
+ color=bs.safecolor(prec.team.color + (1,)),
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay,
).autoretain()
- bs.timer(15.0, babase.WeakCall(self._show_tips))
+ bs.timer(15.0, bs.WeakCall(self._show_tips))
def _show_tips(self) -> None:
from bascenev1lib.actor.tipstext import TipsText
@@ -443,7 +437,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
assert i.node
bs.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0})
ZoomText(
- babase.Lstr(
+ bs.Lstr(
value=team.players[0].getname(full=True, icon=False)
),
position=(0, 97 + offs_v),
@@ -460,7 +454,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
wins_resource = 'seriesWinLine1PlayerText'
else:
wins_resource = 'seriesWinLine1TeamText'
- wins_text = babase.Lstr(resource=wins_resource)
+ wins_text = bs.Lstr(resource=wins_resource)
# Temp - if these come up as the english default, fall-back to the
# unified old form which is more likely to be translated.
@@ -473,7 +467,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
maxwidth=250,
).autoretain()
ZoomText(
- babase.Lstr(resource='seriesWinLine2Text'),
+ bs.Lstr(resource='seriesWinLine2Text'),
position=(0, -110 + offs_v),
scale=1.0 * s_extra,
color=team.color,
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/playerspaz.py b/src/assets/ba_data/python/bascenev1lib/actor/playerspaz.py
index 928d5ecb..57c1db0f 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/playerspaz.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/playerspaz.py
@@ -6,7 +6,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING, TypeVar, overload
-import babase
import bascenev1 as bs
from bascenev1lib.actor.spaz import Spaz
@@ -99,7 +98,7 @@ class PlayerSpaz(Spaz):
player: Any = self._player
assert isinstance(player, playertype)
if not player.exists() and doraise:
- raise babase.PlayerNotFoundError()
+ raise bs.PlayerNotFoundError()
return player if player.exists() else None
def connect_controls_to_player(
@@ -129,16 +128,16 @@ class PlayerSpaz(Spaz):
else:
player.resetinput()
- player.assigninput(babase.InputType.UP_DOWN, self.on_move_up_down)
- player.assigninput(babase.InputType.LEFT_RIGHT, self.on_move_left_right)
+ player.assigninput(bs.InputType.UP_DOWN, self.on_move_up_down)
+ player.assigninput(bs.InputType.LEFT_RIGHT, self.on_move_left_right)
player.assigninput(
- babase.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press
+ bs.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press
)
player.assigninput(
- babase.InputType.HOLD_POSITION_RELEASE,
+ bs.InputType.HOLD_POSITION_RELEASE,
self.on_hold_position_release,
)
- intp = babase.InputType
+ intp = bs.InputType
if enable_jump:
player.assigninput(intp.JUMP_PRESS, self.on_jump_press)
player.assigninput(intp.JUMP_RELEASE, self.on_jump_release)
@@ -210,7 +209,7 @@ class PlayerSpaz(Spaz):
picked_up_by = msg.node.source_player
if picked_up_by:
self.last_player_attacked_by = picked_up_by
- self.last_attacked_time = babase.apptime()
+ self.last_attacked_time = bs.apptime()
self.last_attacked_type = ('picked_up', 'default')
elif isinstance(msg, bs.StandMessage):
super().handlemessage(msg) # Augment standard behavior.
@@ -248,7 +247,7 @@ class PlayerSpaz(Spaz):
# something like last_actor_attacked_by to fix that.
if (
self.last_player_attacked_by
- and babase.apptime() - self.last_attacked_time < 4.0
+ and bs.apptime() - self.last_attacked_time < 4.0
):
killerplayer = self.last_player_attacked_by
else:
@@ -279,7 +278,7 @@ class PlayerSpaz(Spaz):
source_player = msg.get_source_player(type(self._player))
if source_player:
self.last_player_attacked_by = source_player
- self.last_attacked_time = babase.apptime()
+ self.last_attacked_time = bs.apptime()
self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
super().handlemessage(msg) # Augment standard behavior.
activity = self._activity()
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/popuptext.py b/src/assets/ba_data/python/bascenev1lib/actor/popuptext.py
index e08de510..c5c1dccd 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/popuptext.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/popuptext.py
@@ -7,7 +7,6 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
-import babase
import bascenev1 as bs
if TYPE_CHECKING:
@@ -22,7 +21,7 @@ class PopupText(bs.Actor):
def __init__(
self,
- text: str | babase.Lstr,
+ text: str | bs.Lstr,
position: Sequence[float] = (0.0, 0.0, 0.0),
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
random_offset: float = 0.5,
@@ -116,7 +115,7 @@ class PopupText(bs.Actor):
# kill ourself
self._die_timer = bs.Timer(
- lifespan, babase.WeakCall(self.handlemessage, bs.DieMessage())
+ lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage())
)
def handlemessage(self, msg: Any) -> Any:
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/respawnicon.py b/src/assets/ba_data/python/bascenev1lib/actor/respawnicon.py
index 1b5da40c..72a14c93 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/respawnicon.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/respawnicon.py
@@ -5,14 +5,9 @@
from __future__ import annotations
import weakref
-from typing import TYPE_CHECKING
-import babase
import bascenev1 as bs
-if TYPE_CHECKING:
- pass
-
class RespawnIcon:
"""An icon with a countdown that appears alongside the screen.
@@ -22,11 +17,11 @@ class RespawnIcon:
This is used to indicate that a bascenev1.Player is waiting to respawn.
"""
- _MASKTEXSTORENAME = babase.storagename('masktex')
- _ICONSSTORENAME = babase.storagename('icons')
+ _MASKTEXSTORENAME = bs.storagename('masktex')
+ _ICONSSTORENAME = bs.storagename('icons')
def __init__(self, player: bs.Player, respawn_time: float):
- """Instantiate with a bascenev1.Player and respawn_time (in seconds)."""
+ """Instantiate with a Player and respawn_time (in seconds)."""
self._visible = True
on_right, offs_extra, respawn_icons = self._get_context(player)
@@ -81,13 +76,13 @@ class RespawnIcon:
attrs={
'v_attach': 'top',
'h_attach': 'right' if on_right else 'left',
- 'text': babase.Lstr(value=player.getname()),
+ 'text': bs.Lstr(value=player.getname()),
'maxwidth': 100,
'h_align': 'center',
'v_align': 'center',
'shadow': 1.0,
'flatness': 1.0,
- 'color': babase.safecolor(icon['tint_color']),
+ 'color': bs.safecolor(icon['tint_color']),
'scale': 0.5,
'position': npos,
},
@@ -109,7 +104,7 @@ class RespawnIcon:
'shadow': 0.5,
'flatness': 0.5,
'v_attach': 'top',
- 'color': babase.safecolor(icon['tint_color']),
+ 'color': bs.safecolor(icon['tint_color']),
'text': '',
},
)
@@ -118,10 +113,10 @@ class RespawnIcon:
assert self._text.node
bs.animate(self._text.node, 'scale', {0: 0, 0.1: 0.9})
- self._respawn_time = babase.apptime() + respawn_time
+ self._respawn_time = bs.time() + respawn_time
self._update()
self._timer: bs.Timer | None = bs.Timer(
- 1.0, babase.WeakCall(self._update), repeat=True
+ 1.0, bs.WeakCall(self._update), repeat=True
)
@property
@@ -159,7 +154,7 @@ class RespawnIcon:
return on_right, offs_extra, icons
def _update(self) -> None:
- remaining = int(round(self._respawn_time - babase.apptime()))
+ remaining = int(round(self._respawn_time - bs.time()))
if remaining > 0:
assert self._text is not None
if self._text.node:
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/text.py b/src/assets/ba_data/python/bascenev1lib/actor/text.py
index b69edd5f..1a274d64 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/text.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/text.py
@@ -7,7 +7,6 @@ from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
-import babase
import bascenev1 as bs
if TYPE_CHECKING:
@@ -56,7 +55,7 @@ class Text(bs.Actor):
def __init__(
self,
- text: str | babase.Lstr,
+ text: str | bs.Lstr,
position: tuple[float, float] = (0.0, 0.0),
h_align: HAlign = HAlign.LEFT,
v_align: VAlign = VAlign.NONE,
@@ -219,7 +218,7 @@ class Text(bs.Actor):
if transition_out_delay is not None:
bs.timer(
transition_delay + transition_out_delay + 1.0,
- babase.WeakCall(self.handlemessage, bs.DieMessage()),
+ bs.WeakCall(self.handlemessage, bs.DieMessage()),
)
def handlemessage(self, msg: Any) -> Any:
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/tipstext.py b/src/assets/ba_data/python/bascenev1lib/actor/tipstext.py
index 83c4f2d7..ede00802 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/tipstext.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/tipstext.py
@@ -6,7 +6,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING
-import babase
import bascenev1 as bs
if TYPE_CHECKING:
@@ -34,8 +33,8 @@ class TipsText(bs.Actor):
'v_attach': 'bottom',
},
)
- tval = babase.Lstr(
- value='${A}:', subs=[('${A}', babase.Lstr(resource='tipText'))]
+ tval = bs.Lstr(
+ value='${A}:', subs=[('${A}', bs.Lstr(resource='tipText'))]
)
self.title_node = bs.newnode(
'text',
@@ -54,7 +53,7 @@ class TipsText(bs.Actor):
self._message_spacing = 3000
self._change_timer = bs.Timer(
0.001 * (self._message_duration + self._message_spacing),
- babase.WeakCall(self.change_phrase),
+ bs.WeakCall(self.change_phrase),
repeat=True,
)
self._combine = bs.newnode(
@@ -70,11 +69,11 @@ class TipsText(bs.Actor):
"""Switch the visible tip phrase."""
from babase import get_remote_app_name
- next_tip = babase.Lstr(
+ next_tip = bs.Lstr(
translate=(
'tips',
- babase.app.classic.get_next_tip()
- if babase.app.classic is not None
+ bs.app.classic.get_next_tip()
+ if bs.app.classic is not None
else '',
),
subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py b/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py
index 0a63dae2..ad6276cd 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py
@@ -5,9 +5,9 @@
from __future__ import annotations
import random
+import logging
from typing import TYPE_CHECKING
-import babase
import bascenev1 as bs
if TYPE_CHECKING:
@@ -24,7 +24,7 @@ class ZoomText(bs.Actor):
def __init__(
self,
- text: str | babase.Lstr,
+ text: str | bs.Lstr,
position: tuple[float, float] = (0.0, 0.0),
shiftposition: tuple[float, float] | None = None,
shiftdelay: float | None = None,
@@ -47,7 +47,7 @@ class ZoomText(bs.Actor):
if shiftdelay is None:
shiftdelay = 2.500
if shiftdelay < 0.0:
- babase.print_error('got shiftdelay < 0')
+ logging.error('got shiftdelay < 0')
shiftdelay = 0.0
self._project_scale = project_scale
self.node = bs.newnode(
@@ -69,7 +69,7 @@ class ZoomText(bs.Actor):
)
# we never jitter in vr mode..
- if babase.app.vr_mode:
+ if bs.app.vr_mode:
jitter = 0.0
# if they want jitter, animate its position slightly...
@@ -82,14 +82,12 @@ class ZoomText(bs.Actor):
positionadjusted2 = (shiftposition[0], shiftposition[1] - 100)
bs.timer(
shiftdelay,
- babase.WeakCall(
- self._shift, positionadjusted, positionadjusted2
- ),
+ bs.WeakCall(self._shift, positionadjusted, positionadjusted2),
)
if jitter > 0.0:
bs.timer(
shiftdelay + 0.25,
- babase.WeakCall(
+ bs.WeakCall(
self._jitter, positionadjusted2, jitter * scale
),
)
@@ -158,9 +156,7 @@ class ZoomText(bs.Actor):
# if they give us a lifespan, kill ourself down the line
if lifespan is not None:
- bs.timer(
- lifespan, babase.WeakCall(self.handlemessage, bs.DieMessage())
- )
+ bs.timer(lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage()))
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
diff --git a/src/assets/ba_data/python/bascenev1lib/gameutils.py b/src/assets/ba_data/python/bascenev1lib/gameutils.py
index ca8d7ab1..35810d06 100644
--- a/src/assets/ba_data/python/bascenev1lib/gameutils.py
+++ b/src/assets/ba_data/python/bascenev1lib/gameutils.py
@@ -6,7 +6,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING
-import babase
import bascenev1 as bs
if TYPE_CHECKING:
@@ -23,7 +22,7 @@ class SharedObjects:
standard materials.
"""
- _STORENAME = babase.storagename()
+ _STORENAME = bs.storagename()
def __init__(self) -> None:
activity = bs.getactivity()
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/__init__.py b/src/assets/ba_data/python/bauiv1lib/playlist/__init__.py
index 3aa24bc6..dbe143f7 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/__init__.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/__init__.py
@@ -4,14 +4,8 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-
-import babase
import bascenev1 as bs
-if TYPE_CHECKING:
- pass
-
# FIXME: Could change this to be a classmethod of session types?
class PlaylistTypeVars:
@@ -26,26 +20,26 @@ class PlaylistTypeVars:
self.sessiontype: type[bs.Session]
if issubclass(sessiontype, bs.DualTeamSession):
- play_mode_name = babase.Lstr(
+ play_mode_name = bs.Lstr(
resource='playModes.teamsText', fallback_resource='teamsText'
)
self.get_default_list_call = get_default_teams_playlist
self.session_type_name = 'bascenev1.DualTeamSession'
self.config_name = 'Team Tournament'
- self.window_title_name = babase.Lstr(
+ self.window_title_name = bs.Lstr(
resource='playModes.teamsText', fallback_resource='teamsText'
)
self.sessiontype = bs.DualTeamSession
elif issubclass(sessiontype, bs.FreeForAllSession):
- play_mode_name = babase.Lstr(
+ play_mode_name = bs.Lstr(
resource='playModes.freeForAllText',
fallback_resource='freeForAllText',
)
self.get_default_list_call = get_default_free_for_all_playlist
self.session_type_name = 'bascenev1.FreeForAllSession'
self.config_name = 'Free-for-All'
- self.window_title_name = babase.Lstr(
+ self.window_title_name = bs.Lstr(
resource='playModes.freeForAllText',
fallback_resource='freeForAllText',
)
@@ -55,11 +49,11 @@ class PlaylistTypeVars:
raise RuntimeError(
f'Playlist type vars undefined for sessiontype: {sessiontype}'
)
- self.default_list_name = babase.Lstr(
+ self.default_list_name = bs.Lstr(
resource='defaultGameListNameText',
subs=[('${PLAYMODE}', play_mode_name)],
)
- self.default_new_list_name = babase.Lstr(
+ self.default_new_list_name = bs.Lstr(
resource='defaultNewGameListNameText',
subs=[('${PLAYMODE}', play_mode_name)],
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/vrtesting.py b/src/assets/ba_data/python/bauiv1lib/settings/vrtesting.py
index d4916759..19432834 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/vrtesting.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/vrtesting.py
@@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
-import babase
+import bauiv1 as bui
from bauiv1lib.settings.testing import TestingWindow
if TYPE_CHECKING:
@@ -18,10 +18,10 @@ class VRTestingWindow(TestingWindow):
def __init__(self, transition: str = 'in_right'):
entries: list[dict[str, Any]] = []
- app = babase.app
+ app = bui.app
assert app.classic is not None
- # these are gear-vr only
+ # These are gear-vr only.
if (
app.classic.platform == 'android'
and app.classic.subplatform == 'oculus'
@@ -44,16 +44,19 @@ class VRTestingWindow(TestingWindow):
},
# {'name':'eyeOffsX','label':'Eye IPD','increment':0.001}
]
- # cardboard/gearvr get eye offset controls..
+
+ # Cardboard/gearvr get eye offset controls.
# if app.platform == 'android':
# entries += [
# {'name':'eyeOffsY','label':'Eye Offset Y','increment':0.01},
# {'name':'eyeOffsZ','label':'Eye Offset Z','increment':0.005}]
- # everyone gets head-scale
+
+ # Everyone gets head-scale.
entries += [
{'name': 'headScale', 'label': 'Head Scale', 'increment': 1.0}
]
- # and everyone gets all these..
+
+ # And everyone gets all these.
entries += [
{
'name': 'vrCamOffsetY',
@@ -88,7 +91,7 @@ class VRTestingWindow(TestingWindow):
]
super().__init__(
- babase.Lstr(resource='settingsWindowAdvanced.vrTestingText'),
+ bui.Lstr(resource='settingsWindowAdvanced.vrTestingText'),
entries,
transition,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/tabs.py b/src/assets/ba_data/python/bauiv1lib/tabs.py
index 2803e634..ffb0cff4 100644
--- a/src/assets/ba_data/python/bauiv1lib/tabs.py
+++ b/src/assets/ba_data/python/bauiv1lib/tabs.py
@@ -7,7 +7,6 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, TypeVar, Generic
-import babase
import bauiv1 as bui
if TYPE_CHECKING:
@@ -35,7 +34,7 @@ class TabRow(Generic[T]):
def __init__(
self,
parent: bui.Widget,
- tabdefs: list[tuple[T, babase.Lstr]],
+ tabdefs: list[tuple[T, bui.Lstr]],
pos: tuple[float, float],
size: tuple[float, float],
on_select_call: Callable[[T], None] | None = None,
@@ -58,7 +57,7 @@ class TabRow(Generic[T]):
size=size,
label=tab_label,
enable_sound=False,
- on_activate_call=babase.Call(
+ on_activate_call=bui.Call(
self._tick_and_call, on_select_call, tab_id
),
)
diff --git a/src/ballistica/base/app/stress_test.cc b/src/ballistica/base/app/stress_test.cc
index 88a21428..e35da034 100644
--- a/src/ballistica/base/app/stress_test.cc
+++ b/src/ballistica/base/app/stress_test.cc
@@ -45,7 +45,7 @@ void StressTest::Update() {
if (t - last_stress_test_update_time_ >= 10000) {
if (stress_test_stats_file_ == nullptr) {
assert(g_core);
- auto user_python_dir = g_core->platform->GetUserPythonDirectory();
+ auto user_python_dir = g_core->GetUserPythonDirectory();
if (user_python_dir) {
std::string f_name = *user_python_dir + "/stress_test_stats.csv";
stress_test_stats_file_ =
diff --git a/src/ballistica/base/assets/assets.cc b/src/ballistica/base/assets/assets.cc
index 1b57aedf..2f603a7b 100644
--- a/src/ballistica/base/assets/assets.cc
+++ b/src/ballistica/base/assets/assets.cc
@@ -39,7 +39,7 @@ static const bool kShowPruningInfo = false;
#define PENDING_LOAD_PROCESS_TIME 5
Assets::Assets() {
- asset_paths_.emplace_back(g_core->platform->GetDataDirectory() + BA_DIRSLASH
+ asset_paths_.emplace_back(g_core->GetDataDirectory() + BA_DIRSLASH
+ "ba_data");
for (bool& have_pending_load : have_pending_loads_) {
have_pending_load = false;
diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc
index fbb6773e..3192c6a1 100644
--- a/src/ballistica/base/base.cc
+++ b/src/ballistica/base/base.cc
@@ -86,7 +86,7 @@ void BaseFeatureSet::OnModuleExec(PyObject* module) {
// Want to run this at the last possible moment before spinning up our
// BaseFeatureSet. This locks in baenv customizations.
- g_core->python->ApplyBaEnvConfig();
+ g_core->ApplyBaEnvConfig();
// Create our feature-set's C++ front-end.
assert(g_base == nullptr);
diff --git a/src/ballistica/base/graphics/text/text_graphics.cc b/src/ballistica/base/graphics/text/text_graphics.cc
index 09aaba4f..f9e5a8d5 100644
--- a/src/ballistica/base/graphics/text/text_graphics.cc
+++ b/src/ballistica/base/graphics/text/text_graphics.cc
@@ -826,8 +826,8 @@ void TextGraphics::LoadGlyphPage(uint32_t index) {
if (g_glyph_pages[index] == nullptr) {
char buffer[256];
snprintf(buffer, sizeof(buffer), "%s%sba_data%sfonts%sfontSmall%d.fdata",
- g_core->platform->GetDataDirectory().c_str(), BA_DIRSLASH,
- BA_DIRSLASH, BA_DIRSLASH, index);
+ g_core->GetDataDirectory().c_str(), BA_DIRSLASH, BA_DIRSLASH,
+ BA_DIRSLASH, index);
FILE* f = g_core->platform->FOpen(buffer, "rb");
BA_PRECONDITION(f);
BA_PRECONDITION(sizeof(TextGraphics::Glyph[2]) == sizeof(float[18]));
diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc
index 62c9a69e..6e8be733 100644
--- a/src/ballistica/base/python/methods/python_methods_app.cc
+++ b/src/ballistica/base/python/methods/python_methods_app.cc
@@ -711,12 +711,9 @@ static auto PyEnv(PyObject* self) -> PyObject* {
default:
throw Exception();
}
- std::optional user_py_dir =
- g_core->platform->GetUserPythonDirectory();
- std::optional app_py_dir =
- g_core->platform->GetAppPythonDirectory();
- std::optional site_py_dir =
- g_core->platform->GetSitePythonDirectory();
+ std::optional user_py_dir = g_core->GetUserPythonDirectory();
+ std::optional app_py_dir = g_core->GetAppPythonDirectory();
+ std::optional site_py_dir = g_core->GetSitePythonDirectory();
// clang-format off
PyObject* env = Py_BuildValue(
@@ -772,7 +769,7 @@ static auto PyEnv(PyObject* self) -> PyObject* {
"device_name",
g_core->platform->GetDeviceName().c_str(),
"data_directory",
- g_core->platform->GetDataDirectory().c_str());
+ g_core->GetDataDirectory().c_str());
// clang-format on
g_base->python->StoreEnv(env);
}
@@ -1449,6 +1446,29 @@ static PyMethodDef PyEmptyAppModeHandleIntentExecDef = {
"(internal)",
};
+// ---------------------- get_immediate_return_code ----------------------------
+
+static auto PyGetImmediateReturnCode(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+ BA_PRECONDITION(g_core);
+ auto val = g_core->core_config().immediate_return_code;
+ if (!val.has_value()) {
+ Py_RETURN_NONE;
+ }
+ return PyLong_FromLong(*val);
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyGetImmediateReturnCodeDef = {
+ "get_immediate_return_code", // name
+ (PyCFunction)PyGetImmediateReturnCode, // method
+ METH_NOARGS, // flags
+
+ "get_immediate_return_code() -> int | None\n"
+ "\n"
+ "(internal)\n",
+};
+
// -----------------------------------------------------------------------------
auto PythonMethodsApp::GetMethods() -> std::vector {
@@ -1496,6 +1516,7 @@ auto PythonMethodsApp::GetMethods() -> std::vector {
PyEmptyAppModeDeactivateDef,
PyEmptyAppModeHandleIntentDefaultDef,
PyEmptyAppModeHandleIntentExecDef,
+ PyGetImmediateReturnCodeDef,
};
}
diff --git a/src/ballistica/base/python/methods/python_methods_misc.cc b/src/ballistica/base/python/methods/python_methods_misc.cc
index 8dbc6b6f..07a6a0f8 100644
--- a/src/ballistica/base/python/methods/python_methods_misc.cc
+++ b/src/ballistica/base/python/methods/python_methods_misc.cc
@@ -854,7 +854,7 @@ static PyMethodDef PySetPlatformMiscReadValsDef = {
static auto PyGetLogFilePath(PyObject* self, PyObject* args) -> PyObject* {
BA_PYTHON_TRY;
- std::string config_dir = g_core->platform->GetConfigDirectory();
+ std::string config_dir = g_core->GetConfigDirectory();
std::string logpath = config_dir + BA_DIRSLASH + "log.json";
return PyUnicode_FromString(logpath.c_str());
BA_PYTHON_CATCH;
diff --git a/src/ballistica/core/core.cc b/src/ballistica/core/core.cc
index a515a8c2..c2d1461b 100644
--- a/src/ballistica/core/core.cc
+++ b/src/ballistica/core/core.cc
@@ -15,21 +15,46 @@ CoreFeatureSet* g_core{};
BaseSoftInterface* g_base_soft{};
auto CoreFeatureSet::Import(const CoreConfig* config) -> CoreFeatureSet* {
- // Only accept a config in monolithic builds if this is the first import.
- if (config != nullptr) {
- if (!g_buildconfig.monolithic_build()) {
- FatalError("CoreConfig can only be passed in monolithic builds.");
- }
- if (g_core != nullptr) {
- FatalError("CoreConfig can only be passed on the first import call.");
- }
-
- if (g_core == nullptr) {
- DoImport(*config);
+ // In monolithic builds we can accept an explicit core-config the first
+ // time we're imported.
+ if (g_buildconfig.monolithic_build()) {
+ if (config != nullptr) {
+ if (g_core != nullptr) {
+ FatalError(
+ "CoreConfig can only be passed on the first CoreFeatureSet::Import "
+ "call.");
+ }
+ if (g_core == nullptr) {
+ DoImport(*config);
+ }
+ } else {
+ // No config passed; use a default.
+ if (g_core == nullptr) {
+ DoImport({});
+ }
}
} else {
+ // In modular builds we autogenerate a CoreConfig that takes into
+ // account only env-vars (or env-vars plus Python args if we're being
+ // run via the baenv script).
+ if (config != nullptr) {
+ FatalError("CoreConfig can't be explicitly passed in modular builds.");
+ }
if (g_core == nullptr) {
- DoImport({});
+ bool can_do_args = CorePython::WasModularMainCalled();
+ if (can_do_args) {
+ // Wrangle Python's sys.argv into a standard C-style argc/argv so we
+ // can pass to the same handler as the monolithic C route. Note that
+ // a few of the values we parse here (--command, etc) have already
+ // been handled at the Python layer, but we parse them here just the
+ // same so that we have uniform records and invalid-value handling
+ // between monolithic and modular.
+ std::vector argbuffer;
+ std::vector argv = CorePython::FetchPythonArgs(&argbuffer);
+ DoImport(CoreConfig::ForArgsAndEnvVars(argv.size(), argv.data()));
+ } else {
+ DoImport(CoreConfig::ForEnvVars());
+ }
}
}
return g_core;
@@ -90,14 +115,88 @@ void CoreFeatureSet::PostInit() {
// (so that our log handling system is fully bootstrapped), but
// technically we can push our log calls out to Python any time now since
// we grabbed the logging calls above. Do so immediately here if asked.
- if (!g_core->core_config().hold_early_logs) {
+ if (!core_config_.hold_early_logs) {
python->EnablePythonLoggingCalls();
}
+}
- // FIXME: MOVE THIS TO A RUN_APP_TO_COMPLETION() SORT OF PLACE.
- // For now it does the right thing here since all we have is monolithic
- // builds but this will need to account for more situations later.
- // python->ReleaseMainThreadGIL();
+auto CoreFeatureSet::core_config() const -> const CoreConfig& {
+ // Try to make a bit of noise if we're accessed in modular builds before
+ // baenv values are set, since in that case we won't yet have our final
+ // core-config values. Though we want to keep this to a minimal printf so
+ // we don't interfere with low-level stuff like FatalError handling that
+ // might need core_config access at any time.
+ if (!g_buildconfig.monolithic_build()) {
+ if (!HaveBaEnvVals()) {
+ static bool did_warn = false;
+ if (!did_warn) {
+ did_warn = true;
+ printf(
+ "WARNING: accessing core_config() before baenv values have been "
+ "applied to it.\n");
+ }
+ }
+ }
+ return core_config_;
+}
+
+void CoreFeatureSet::ApplyBaEnvConfig() {
+ auto envcfg =
+ python->objs().Get(core::CorePython::ObjID::kBaEnvGetConfigCall).Call();
+ BA_PRECONDITION_FATAL(envcfg.Exists());
+
+ assert(!have_ba_env_vals_);
+ have_ba_env_vals_ = true;
+
+ // Grab everything baenv shipped us.
+ ba_env_config_dir_ = envcfg.GetAttr("config_dir").ValueAsString();
+ ba_env_data_dir_ = envcfg.GetAttr("data_dir").ValueAsString();
+ ba_env_app_python_dir_ =
+ envcfg.GetAttr("app_python_dir").ValueAsOptionalString();
+ ba_env_user_python_dir_ =
+ envcfg.GetAttr("user_python_dir").ValueAsOptionalString();
+ ba_env_site_python_dir_ =
+ envcfg.GetAttr("site_python_dir").ValueAsOptionalString();
+
+ // Consider app-python-dir to be 'custom' if baenv provided a value for it
+ // AND that value differs from baenv's default.
+ auto standard_app_python_dir =
+ envcfg.GetAttr("standard_app_python_dir").ValueAsString();
+ using_custom_app_python_dir_ =
+ ba_env_app_python_dir_.has_value()
+ && *ba_env_app_python_dir_ != standard_app_python_dir;
+
+ // Ok, now look for the existence of ba_data in the dir we've got.
+ auto fullpath = ba_env_data_dir_ + BA_DIRSLASH + "ba_data";
+ if (!platform->FilePathExists(fullpath)) {
+ FatalError("ba_data directory not found at '" + fullpath + "'.");
+ }
+}
+
+auto CoreFeatureSet::GetAppPythonDirectory() -> std::optional {
+ BA_PRECONDITION(have_ba_env_vals_);
+ return ba_env_app_python_dir_;
+}
+
+auto CoreFeatureSet::GetUserPythonDirectory() -> std::optional {
+ BA_PRECONDITION(have_ba_env_vals_);
+ return ba_env_user_python_dir_;
+}
+
+// Return the ballisticakit config dir. This does not vary across versions.
+auto CoreFeatureSet::GetConfigDirectory() -> std::string {
+ BA_PRECONDITION(have_ba_env_vals_);
+ return ba_env_config_dir_;
+}
+
+auto CoreFeatureSet::GetDataDirectory() -> std::string {
+ BA_PRECONDITION(have_ba_env_vals_);
+ return ba_env_data_dir_;
+}
+
+auto CoreFeatureSet::GetSitePythonDirectory() -> std::optional {
+ BA_PRECONDITION(have_ba_env_vals_);
+ return ba_env_site_python_dir_;
}
auto CoreFeatureSet::CalcBuildSrcDir() -> std::string {
diff --git a/src/ballistica/core/core.h b/src/ballistica/core/core.h
index bedfdb6d..cc9e90a0 100644
--- a/src/ballistica/core/core.h
+++ b/src/ballistica/core/core.h
@@ -3,6 +3,7 @@
#ifndef BALLISTICA_CORE_CORE_H_
#define BALLISTICA_CORE_CORE_H_
+#include
#include
#include
#include
@@ -54,13 +55,15 @@ class CoreFeatureSet {
auto SoftImportBase() -> BaseSoftInterface*;
/// The core-config we were inited with.
- const auto& core_config() const { return core_config_; }
+ auto core_config() const -> const CoreConfig&;
/// Start a timer to force-kill our process after the set length of time.
/// Can be used during shutdown or when trying to send a crash-report to
/// ensure we don't hang indefinitely.
void StartSuicideTimer(const std::string& action, millisecs_t delay);
+ void ApplyBaEnvConfig();
+
// Call this if the main thread changes.
// Fixme: Should come up with something less hacky feeling.
void UpdateMainThreadID();
@@ -108,6 +111,38 @@ class CoreFeatureSet {
legacy_user_agent_string_ = val;
}
+ /// Return true if baenv values have been locked in: python paths, log
+ /// handling, etc. Early-running code may wish to explicitly avoid making log
+ /// calls until this condition is met to ensure predictable behavior.
+ auto HaveBaEnvVals() const { return have_ba_env_vals_; }
+
+ /// Return the directory where the app expects to find its bundled Python
+ /// files.
+ auto GetAppPythonDirectory() -> std::optional;
+
+ /// Return a directory where the local user can manually place Python
+ /// files where they will be accessible by the app. When possible, this
+ /// directory should be in a place easily accessible to the user.
+ auto GetUserPythonDirectory() -> std::optional;
+
+ /// Get the root config directory. This dir contains the app config file
+ /// and other data considered essential to the app install. This directory
+ /// should be included in OS backups.
+ auto GetConfigDirectory() -> std::string;
+
+ /// Get the data directory. This dir contains ba_data and possibly other
+ /// platform-specific bits needed for the app to function.
+ auto GetDataDirectory() -> std::string;
+
+ /// Return the directory where bundled 3rd party Python files live.
+ auto GetSitePythonDirectory() -> std::optional;
+
+ // Are we using a non-standard app python dir (such as a 'sys' dir within a
+ // user-python-dir).
+ auto using_custom_app_python_dir() const {
+ return using_custom_app_python_dir_;
+ }
+
// Subsystems.
CorePython* const python;
CorePlatform* const platform;
@@ -159,6 +194,13 @@ class CoreFeatureSet {
std::mutex app_time_mutex_;
std::string legacy_user_agent_string_{
"BA_USER_AGENT_UNSET (" BA_PLATFORM_STRING ")"};
+ bool have_ba_env_vals_{};
+ std::optional ba_env_app_python_dir_;
+ std::string ba_env_config_dir_;
+ std::optional ba_env_user_python_dir_;
+ std::optional ba_env_site_python_dir_;
+ std::string ba_env_data_dir_;
+ bool using_custom_app_python_dir_{};
};
} // namespace ballistica::core
diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc
index 7244b2f1..daa92f0d 100644
--- a/src/ballistica/core/platform/core_platform.cc
+++ b/src/ballistica/core/platform/core_platform.cc
@@ -144,7 +144,7 @@ auto CorePlatform::GetLegacyDeviceUUID() -> const std::string& {
// in our config dir. This should be globally-unique, but the downside is
// the user can tamper with it.
if (!have_real_unique_uuid) {
- std::string path = GetConfigDirectory() + BA_DIRSLASH + ".bsuuid";
+ std::string path = g_core->GetConfigDirectory() + BA_DIRSLASH + ".bsuuid";
if (FILE* f = FOpen(path.c_str(), "rb")) {
// There's an existing one; read it.
@@ -201,13 +201,14 @@ auto CorePlatform::DoGetConfigDirectoryMonolithicDefault()
}
auto CorePlatform::GetConfigFilePath() -> std::string {
- return GetConfigDirectory() + BA_DIRSLASH + "config.json";
+ return g_core->GetConfigDirectory() + BA_DIRSLASH + "config.json";
}
// FIXME: should make this unnecessary.
auto CorePlatform::GetLowLevelConfigValue(const char* key, int default_value)
-> int {
- std::string path = GetConfigDirectory() + BA_DIRSLASH + ".cvar_" + key;
+ std::string path =
+ g_core->GetConfigDirectory() + BA_DIRSLASH + ".cvar_" + key;
int val = default_value;
FILE* f = FOpen(path.c_str(), "r");
if (f) {
@@ -225,7 +226,8 @@ auto CorePlatform::GetLowLevelConfigValue(const char* key, int default_value)
// FIXME: should make this unnecessary.
void CorePlatform::SetLowLevelConfigValue(const char* key, int value) {
- std::string path = GetConfigDirectory() + BA_DIRSLASH + ".cvar_" + key;
+ std::string path =
+ g_core->GetConfigDirectory() + BA_DIRSLASH + ".cvar_" + key;
std::string out = std::to_string(value);
FILE* f = FOpen(path.c_str(), "w");
if (f) {
@@ -238,11 +240,6 @@ void CorePlatform::SetLowLevelConfigValue(const char* key, int value) {
}
}
-auto CorePlatform::GetUserPythonDirectory() -> std::optional {
- BA_PRECONDITION(have_ba_env_vals_);
- return ba_env_user_python_dir_;
-}
-
auto CorePlatform::GetVolatileDataDirectory() -> std::string {
if (!made_volatile_data_dir_) {
volatile_data_dir_ = GetDefaultVolatileDataDirectory();
@@ -254,23 +251,13 @@ auto CorePlatform::GetVolatileDataDirectory() -> std::string {
auto CorePlatform::GetDefaultVolatileDataDirectory() -> std::string {
// By default, stuff this in a subdir under our config dir.
- return GetConfigDirectory() + BA_DIRSLASH + "vdata";
-}
-
-auto CorePlatform::GetAppPythonDirectory() -> std::optional {
- BA_PRECONDITION(have_ba_env_vals_);
- return ba_env_app_python_dir_;
-}
-
-auto CorePlatform::GetSitePythonDirectory() -> std::optional {
- BA_PRECONDITION(have_ba_env_vals_);
- return ba_env_site_python_dir_;
+ return g_core->GetConfigDirectory() + BA_DIRSLASH + "vdata";
}
auto CorePlatform::GetReplaysDir() -> std::string {
static bool made_dir = false;
if (!made_dir) {
- replays_dir_ = GetConfigDirectory() + BA_DIRSLASH + "replays";
+ replays_dir_ = g_core->GetConfigDirectory() + BA_DIRSLASH + "replays";
MakeDir(replays_dir_);
made_dir = true;
}
@@ -355,13 +342,6 @@ auto CorePlatform::GetErrnoString() -> std::string {
#endif
}
-// Return the ballisticakit config dir
-// This does not vary across versions.
-auto CorePlatform::GetConfigDirectory() -> std::string {
- BA_PRECONDITION(have_ba_env_vals_);
- return ba_env_config_dir_;
-}
-
auto CorePlatform::GetConfigDirectoryMonolithicDefault()
-> std::optional {
// CoreConfig value trumps all. Otherwise go with platform-specific default.
@@ -371,11 +351,6 @@ auto CorePlatform::GetConfigDirectoryMonolithicDefault()
return DoGetConfigDirectoryMonolithicDefault();
}
-auto CorePlatform::GetDataDirectory() -> std::string {
- BA_PRECONDITION(have_ba_env_vals_);
- return ba_env_data_dir_;
-}
-
auto CorePlatform::GetDataDirectoryMonolithicDefault() -> std::string {
// CoreConfig arg trumps all. Otherwise ask for platform-specific value.
if (g_core->core_config().data_dir.has_value()) {
@@ -1216,32 +1191,4 @@ auto CorePlatform::System(const char* cmd) -> int {
#endif
}
-void CorePlatform::SetBaEnvVals(const PythonRef& ref) {
- assert(!have_ba_env_vals_);
- have_ba_env_vals_ = true;
-
- ba_env_config_dir_ = ref.GetAttr("config_dir").ValueAsString();
- ba_env_data_dir_ = ref.GetAttr("data_dir").ValueAsString();
- ba_env_app_python_dir_ =
- ref.GetAttr("app_python_dir").ValueAsOptionalString();
- ba_env_user_python_dir_ =
- ref.GetAttr("user_python_dir").ValueAsOptionalString();
- ba_env_site_python_dir_ =
- ref.GetAttr("site_python_dir").ValueAsOptionalString();
-
- // Consider app-python-dir 'custom' if baenv provided a value
- // for it AND that value differs from baenv's default.
- auto standard_app_python_dir =
- ref.GetAttr("standard_app_python_dir").ValueAsString();
- using_custom_app_python_dir_ =
- ba_env_app_python_dir_.has_value()
- && *ba_env_app_python_dir_ != standard_app_python_dir;
-
- // Ok, now look for the existence of ba_data in the dir we've got.
- auto fullpath = ba_env_data_dir_ + BA_DIRSLASH + "ba_data";
- if (!FilePathExists(fullpath)) {
- FatalError("ba_data directory not found at '" + fullpath + "'.");
- }
-}
-
} // namespace ballistica::core
diff --git a/src/ballistica/core/platform/core_platform.h b/src/ballistica/core/platform/core_platform.h
index 16b08f18..b7efa227 100644
--- a/src/ballistica/core/platform/core_platform.h
+++ b/src/ballistica/core/platform/core_platform.h
@@ -141,35 +141,16 @@ class CorePlatform {
/// etc).
virtual auto GetUIScale() -> UIScale;
- /// Get the data directory. This dir contains ba_data and possibly other
- /// platform-specific bits needed for the app to function.
- auto GetDataDirectory() -> std::string;
-
/// Return default DataDirectory value for monolithic builds.
auto GetDataDirectoryMonolithicDefault() -> std::string;
- /// Get the root config directory. This dir contains the app config file
- /// and other data considered essential to the app install. This directory
- /// should be included in OS backups.
- auto GetConfigDirectory() -> std::string;
auto GetConfigDirectoryMonolithicDefault() -> std::optional;
/// Get the path of the app config file.
auto GetConfigFilePath() -> std::string;
- /// Return a directory where the local user can manually place Python
- /// files where they will be accessible by the app. When possible, this
- /// directory should be in a place easily accessible to the user.
- auto GetUserPythonDirectory() -> std::optional;
auto GetUserPythonDirectoryMonolithicDefault() -> std::optional;
- /// Return the directory where the app expects to find its bundled Python
- /// files.
- auto GetAppPythonDirectory() -> std::optional;
-
- /// Return the directory where bundled 3rd party Python files live.
- auto GetSitePythonDirectory() -> std::optional;
-
/// Get a directory where the app can store internal generated data. This
/// directory should not be included in backups and the app should remain
/// functional if this directory is completely cleared between runs
@@ -470,20 +451,9 @@ class CorePlatform {
// return true and set the native full res here. Otherwise return false;
virtual auto GetDisplayResolution(int* x, int* y) -> bool;
- auto using_custom_app_python_dir() const {
- return using_custom_app_python_dir_;
- }
-
/// Are we being run from a terminal? (should we show prompts, etc?).
auto is_stdin_a_terminal() const { return is_stdin_a_terminal_; }
- void SetBaEnvVals(const PythonRef& ref);
-
- /// Return true if baenv values have been locked in: python paths, log
- /// handling, etc. Early-running code may wish to explicitly avoid making log
- /// calls until this condition is met to ensure predictable behavior.
- auto HaveBaEnvVals() const { return have_ba_env_vals_; }
-
protected:
/// Are we being run from a terminal? (should we show prompts, etc?).
virtual auto GetIsStdinATerminal() -> bool;
@@ -544,7 +514,6 @@ class CorePlatform {
private:
bool is_stdin_a_terminal_{};
- bool using_custom_app_python_dir_{};
bool have_has_touchscreen_value_{};
bool have_touchscreen_{};
bool is_tegra_k1_{};
@@ -553,17 +522,11 @@ class CorePlatform {
bool made_volatile_data_dir_{};
bool have_device_uuid_{};
bool ran_base_post_init_{};
- bool have_ba_env_vals_{};
millisecs_t start_time_millisecs_{};
std::string device_name_;
std::string legacy_device_uuid_;
std::string volatile_data_dir_;
std::string replays_dir_;
- std::string ba_env_config_dir_;
- std::string ba_env_data_dir_;
- std::optional ba_env_app_python_dir_;
- std::optional ba_env_user_python_dir_;
- std::optional ba_env_site_python_dir_;
};
} // namespace ballistica::core
diff --git a/src/ballistica/core/python/core_python.cc b/src/ballistica/core/python/core_python.cc
index 9ff02388..36f27548 100644
--- a/src/ballistica/core/python/core_python.cc
+++ b/src/ballistica/core/python/core_python.cc
@@ -15,13 +15,6 @@ void LowLevelPythonDebugLog(const char* msg) {
g_core->platform->DebugLog(msg);
}
-void CorePython::ApplyBaEnvConfig() {
- // Fetch the env-config (creates it if need be).
- auto envcfg = objs().Get(core::CorePython::ObjID::kBaEnvGetConfigCall).Call();
- BA_PRECONDITION(envcfg.Exists());
- g_core->platform->SetBaEnvVals(envcfg);
-}
-
static void CheckPyInitStatus(const char* where, const PyStatus& status) {
if (PyStatus_Exception(status)) {
FatalError(std::string("Error in ") + where + ": "
@@ -337,4 +330,76 @@ void CorePython::LoggingCall(LogLevel loglevel, const std::string& msg) {
objs().Get(logcallobj).Call(args);
}
+auto CorePython::WasModularMainCalled() -> bool {
+ assert(!g_buildconfig.monolithic_build());
+
+ // This gets called in modular builds before anything is inited, so we need to
+ // avoid using anything from g_core or whatnot here; only raw Python stuff.
+
+ PyObject* baenv = PyImport_ImportModule("baenv");
+ if (!baenv) {
+ FatalError("Unable to import baenv module.");
+ }
+ PyObject* env_globals_class = PyObject_GetAttrString(baenv, "_EnvGlobals");
+ if (!env_globals_class) {
+ FatalError("_EnvGlobals class not found in baenv.");
+ }
+ PyObject* get_call = PyObject_GetAttrString(env_globals_class, "get");
+ if (!get_call) {
+ FatalError("get() call not found on baenv._EnvGlobals.");
+ }
+ PyObject* env_globals_instance = PyObject_CallNoArgs(get_call);
+ if (!get_call) {
+ FatalError("baenv._EnvGlobals.get() call failed.");
+ }
+ PyObject* modular_main_called =
+ PyObject_GetAttrString(env_globals_instance, "modular_main_called");
+ if (!modular_main_called || !PyBool_Check(modular_main_called)) {
+ FatalError("modular_main_called bool not found on baenv _EnvGlobals.");
+ }
+ assert(modular_main_called == Py_True || modular_main_called == Py_False);
+ bool val = modular_main_called == Py_True;
+
+ Py_DECREF(baenv);
+ Py_DECREF(env_globals_class);
+ Py_DECREF(get_call);
+ Py_DECREF(env_globals_instance);
+ Py_DECREF(modular_main_called);
+
+ return val;
+}
+
+auto CorePython::FetchPythonArgs(std::vector* buffer)
+ -> std::vector {
+ // This gets called in modular builds before anything is inited, so we need to
+ // avoid using anything from g_core or whatnot here; only raw Python stuff.
+
+ assert(buffer && buffer->empty());
+ PyObject* sys = PyImport_ImportModule("sys");
+ if (!sys) {
+ FatalError("Unable to import sys module.");
+ }
+ PyObject* argv = PyObject_GetAttrString(sys, "argv");
+ if (!argv || !PyList_Check(argv)) {
+ FatalError("Unable to fetch sys.argv list.");
+ }
+ Py_ssize_t listlen = PyList_GET_SIZE(argv);
+ for (Py_ssize_t i = 0; i < listlen; ++i) {
+ PyObject* arg = PyList_GET_ITEM(argv, i);
+ BA_PRECONDITION_FATAL(PyUnicode_Check(arg));
+ buffer->push_back(PyUnicode_AsUTF8(arg));
+ }
+ Py_DECREF(sys);
+ Py_DECREF(argv);
+
+ // Ok, we've filled the buffer so it won't be resizing anymore. Now set up
+ // argv pointers to it.
+ std::vector out;
+ out.reserve(buffer->size());
+ for (int i = 0; i < buffer->size(); ++i) {
+ out.push_back(const_cast((*buffer)[i].c_str()));
+ }
+ return out;
+}
+
} // namespace ballistica::core
diff --git a/src/ballistica/core/python/core_python.h b/src/ballistica/core/python/core_python.h
index 9b9ab74e..2aa49e42 100644
--- a/src/ballistica/core/python/core_python.h
+++ b/src/ballistica/core/python/core_python.h
@@ -43,10 +43,6 @@ class CorePython {
/// pent up ones) to Python.
void EnablePythonLoggingCalls();
- /// Should be called just before base feature set import; locks in the
- /// baenv environment and runs some checks.
- void ApplyBaEnvConfig();
-
/// Calls Python logging function (logging.error, logging.warning, etc.)
/// Can be called from any thread at any time. If called before Python
/// logging is available, logs locally using Logging::DisplayLog()
@@ -56,6 +52,13 @@ class CorePython {
void VerifyPythonEnvironment();
void SoftImportBase();
+ static auto WasModularMainCalled() -> bool;
+
+ /// Builds a vector of strings out of Python's sys.argv. Returns an argv
+ /// array pointing to them.
+ static auto FetchPythonArgs(std::vector* buffer)
+ -> std::vector;
+
const auto& objs() { return objs_; }
private:
diff --git a/src/ballistica/core/support/core_config.cc b/src/ballistica/core/support/core_config.cc
index 62373cb9..f18b08e4 100644
--- a/src/ballistica/core/support/core_config.cc
+++ b/src/ballistica/core/support/core_config.cc
@@ -75,45 +75,36 @@ static auto ParseArgValue(int argc, char** argv, int* i, const char* arg_long,
return {};
}
-auto CoreConfig::FromCommandLineAndEnv(int argc, char** argv) -> CoreConfig {
- auto cfg = CoreConfig();
-
- // First set any values we allow env-vars for.
- // We want explicitly passed values to override these in any cases where both
- // forms are accepted.
+void CoreConfig::ApplyEnvVars() {
if (auto* envval = getenv("BA_LIFECYCLE_LOG")) {
if (!strcmp(envval, "1")) {
- cfg.lifecycle_log = true;
+ lifecycle_log = true;
}
}
if (auto* envval = getenv("BA_DEBUGGER_ATTACHED")) {
if (!strcmp(envval, "1")) {
- cfg.debugger_attached = true;
+ debugger_attached = true;
}
}
if (auto* envval = getenv("BA_DEBUG_TIMING")) {
if (!strcmp(envval, "1")) {
- cfg.debug_timing = true;
+ debug_timing = true;
}
}
+}
- // REMOVE ME FOR 1.7.20 FINAL.
- if (explicit_bool(false)) {
- printf("TEMP: forcing BA_LIFECYCLE_LOG=1 during 1.7.20 development.\n");
- cfg.lifecycle_log = true;
- }
-
+void CoreConfig::ApplyArgs(int argc, char** argv) {
try {
// First handle single-arg special cases like --help or --version.
if (IsSingleArgSpecialCase(argc, argv, "--help", "-h")) {
PrintHelp();
- cfg.immediate_return_code = 0;
- return cfg;
+ immediate_return_code = 0;
+ return;
}
if (IsSingleArgSpecialCase(argc, argv, "--version", "-v")) {
printf("BallisticaKit %s build %d\n", kEngineVersion, kEngineBuildNumber);
- cfg.immediate_return_code = 0;
- return cfg;
+ immediate_return_code = 0;
+ return;
}
if (IsSingleArgSpecialCase(argc, argv, "--crash")) {
int dummyval{};
@@ -126,7 +117,7 @@ auto CoreConfig::FromCommandLineAndEnv(int argc, char** argv) -> CoreConfig {
if (explicit_bool(true)) {
*invalid_ptr = 1;
}
- return cfg;
+ return;
}
// Ok, all single-arg cases handled; now go through everything else
@@ -135,35 +126,35 @@ auto CoreConfig::FromCommandLineAndEnv(int argc, char** argv) -> CoreConfig {
std::optional value;
while (i < argc) {
if ((value = ParseArgValue(argc, argv, &i, "--command", "-c"))) {
- cfg.call_command = *value;
+ call_command = *value;
} else if ((value = ParseArgValue(argc, argv, &i, "--exec", "-e"))) {
- cfg.exec_command = *value;
+ exec_command = *value;
} else if ((value =
ParseArgValue(argc, argv, &i, "--config-dir", "-C"))) {
- cfg.config_dir = *value;
+ config_dir = *value;
// Make sure what they passed exists.
// Note: Normally baenv will try to create whatever the config dir is;
// do we just want to allow that to happen in this case? But perhaps
// being more strict is ok when accepting user input.
- if (!std::filesystem::exists(*cfg.config_dir)) {
- printf("Error: Provided config dir does not exist: '%s'.",
- cfg.config_dir->c_str());
+ if (!std::filesystem::is_directory(*config_dir)) {
+ printf("Error: Provided config-dir path '%s' is not a directory.",
+ config_dir->c_str());
throw BadArgsException();
}
} else if ((value = ParseArgValue(argc, argv, &i, "--data-dir", "-d"))) {
- cfg.data_dir = *value;
+ data_dir = *value;
// Make sure what they passed exists.
- if (!std::filesystem::exists(*cfg.data_dir)) {
- printf("Error: Provided data dir does not exist: '%s'.",
- cfg.data_dir->c_str());
+ if (!std::filesystem::is_directory(*data_dir)) {
+ printf("Error: Provided data-dir path '%s' is not a directory.",
+ data_dir->c_str());
throw BadArgsException();
}
} else if ((value = ParseArgValue(argc, argv, &i, "--mods-dir", "-m"))) {
- cfg.user_python_dir = *value;
+ user_python_dir = *value;
// Make sure what they passed exists.
- if (!std::filesystem::exists(*cfg.user_python_dir)) {
- printf("Error: Provided mods dir does not exist: '%s'.",
- cfg.user_python_dir->c_str());
+ if (!std::filesystem::is_directory(*user_python_dir)) {
+ printf("Error: Provided mods-dir path '%s' is not a directory.",
+ user_python_dir->c_str());
throw BadArgsException();
}
} else {
@@ -175,8 +166,25 @@ auto CoreConfig::FromCommandLineAndEnv(int argc, char** argv) -> CoreConfig {
}
}
} catch (const BadArgsException&) {
- cfg.immediate_return_code = 1;
+ immediate_return_code = 1;
}
+}
+
+auto CoreConfig::ForEnvVars() -> CoreConfig {
+ CoreConfig cfg{};
+
+ cfg.ApplyEnvVars();
+
+ return cfg;
+}
+
+auto CoreConfig::ForArgsAndEnvVars(int argc, char** argv) -> CoreConfig {
+ CoreConfig cfg{};
+
+ // Apply env-vars first. We want explicit args to override these.
+ cfg.ApplyEnvVars();
+ cfg.ApplyArgs(argc, argv);
+
return cfg;
}
diff --git a/src/ballistica/core/support/core_config.h b/src/ballistica/core/support/core_config.h
index 27e73e65..ddcd5450 100644
--- a/src/ballistica/core/support/core_config.h
+++ b/src/ballistica/core/support/core_config.h
@@ -14,7 +14,17 @@ namespace ballistica::core {
/// when initing the core feature-set.
class CoreConfig {
public:
- static auto FromCommandLineAndEnv(int argc, char** argv) -> CoreConfig;
+ static auto ForArgsAndEnvVars(int argc, char** argv) -> CoreConfig;
+
+ static auto ForEnvVars() -> CoreConfig;
+
+ /// Build a core-config for a modular app being run from the command-line.
+ /// In this case, Python has already been inited and Ballistica has
+ /// already been imported (since that's where this code lives) so there is
+ /// less that can be affected by a core-config.
+
+ void ApplyEnvVars();
+ void ApplyArgs(int argc, char** argv);
/// Enable vr mode on supported platforms.
bool vr_mode{};
@@ -26,19 +36,19 @@ class CoreConfig {
std::optional immediate_return_code{};
/// If set, this single Python command will be run instead of the
- /// normal app loop.
+ /// normal app loop (monolithic builds only).
std::optional call_command{};
/// Python command to be run within the normal app loop.
std::optional exec_command{};
- /// Explicitly set config dir.
+ /// Explicitly passed config dir.
std::optional config_dir{};
- /// Explicitly set data dir.
+ /// Explicitly passed data dir.
std::optional data_dir{};
- /// Explicitly set user-python (mods) dir.
+ /// Explicitly passed user-python (mods) dir.
std::optional user_python_dir{};
/// Log various stages/times in the bootstrapping process.
diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc
index e58c14e7..f085a14b 100644
--- a/src/ballistica/shared/ballistica.cc
+++ b/src/ballistica/shared/ballistica.cc
@@ -25,7 +25,7 @@
#if BA_MONOLITHIC_BUILD && BA_DEFINE_MAIN
auto main(int argc, char** argv) -> int {
auto core_config =
- ballistica::core::CoreConfig::FromCommandLineAndEnv(argc, argv);
+ ballistica::core::CoreConfig::ForArgsAndEnvVars(argc, argv);
// Arg-parsing may have yielded an error or printed simple output for
// things such as '--help', in which case we're done.
@@ -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 = 21196;
+const int kEngineBuildNumber = 21199;
const char* kEngineVersion = "1.7.24";
#if BA_MONOLITHIC_BUILD
@@ -65,9 +65,6 @@ 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);
- // TEMP - bug hunting.
- l_core->platform->DebugLog("mm1");
-
// 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
// environment setup (aside from the bits core does automatically such
@@ -80,9 +77,6 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
exit(success ? 0 : 1);
}
- // TEMP - bug hunting.
- l_core->platform->DebugLog("mm2");
-
// Ok, looks like we're doing a standard monolithic-mode app run.
// -------------------------------------------------------------------------
@@ -95,9 +89,6 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// those modules get loaded from in the first place.
l_core->python->MonolithicModeBaEnvConfigure();
- // TEMP - bug hunting.
- l_core->platform->DebugLog("mm3");
-
// 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.
l_base = l_core->SoftImportBase();
@@ -109,9 +100,6 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// Phase 2: "The pieces are moving."
// -------------------------------------------------------------------------
- // TEMP - bug hunting.
- l_core->platform->DebugLog("mm4");
-
// Spin up all app machinery such as threads and subsystems. This gets
// things ready to rock, but there's no actual rocking quite yet.
l_base->StartApp();
@@ -120,9 +108,6 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// Phase 3: "We come to it at last; the great battle of our time."
// -------------------------------------------------------------------------
- // TEMP - bug hunting.
- l_core->platform->DebugLog("mm5");
-
// At this point we unleash the beast and then simply process events
// until the app exits (or we return from this function and let the
// environment do that part).
@@ -165,7 +150,6 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
}
}
}
-
if (l_core) {
l_core->platform->WillExitMain(false);
return l_core->return_value;
diff --git a/src/ballistica/shared/python/python.cc b/src/ballistica/shared/python/python.cc
index 139cccb6..c1f30067 100644
--- a/src/ballistica/shared/python/python.cc
+++ b/src/ballistica/shared/python/python.cc
@@ -105,7 +105,8 @@ auto Python::GetPyString(PyObject* o) -> std::string {
return PyUnicode_AsUTF8(o);
}
throw Exception(
- "Can't get string from value: " + Python::ObjToString(o) + ".", exctype);
+ "Expected a string object; got type " + Python::ObjTypeToString(o) + ".",
+ exctype);
}
template
@@ -220,6 +221,24 @@ auto Python::GetPyFloats(PyObject* o) -> std::vector {
return vals;
}
+auto Python::GetPyStrings(PyObject* o) -> std::list {
+ assert(HaveGIL());
+ BA_PRECONDITION_FATAL(o != nullptr);
+
+ if (!PySequence_Check(o)) {
+ throw Exception("Object is not a sequence.", PyExcType::kType);
+ }
+ PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal);
+ assert(sequence.Exists());
+ Py_ssize_t size = PySequence_Fast_GET_SIZE(sequence.Get());
+ PyObject** py_objects = PySequence_Fast_ITEMS(sequence.Get());
+ std::list vals;
+ for (Py_ssize_t i = 0; i < size; i++) {
+ vals.emplace_back(Python::GetPyString(py_objects[i]));
+ }
+ return vals;
+}
+
template
auto GetPyIntsT(PyObject* o) -> std::vector {
assert(Python::HaveGIL());
diff --git a/src/ballistica/shared/python/python.h b/src/ballistica/shared/python/python.h
index a4245004..de3c04a9 100644
--- a/src/ballistica/shared/python/python.h
+++ b/src/ballistica/shared/python/python.h
@@ -121,6 +121,7 @@ class Python {
static auto GetPyInts(PyObject* o) -> std::vector;
static auto GetPyUInts64(PyObject* o) -> std::vector;
static auto GetPyPoint2D(PyObject* o) -> Point2D;
+ static auto GetPyStrings(PyObject* o) -> std::list;
/// Set Python exception from C++ Exception.
static void SetPythonException(const Exception& exc);
diff --git a/src/ballistica/shared/python/python_command.h b/src/ballistica/shared/python/python_command.h
index 71e200da..f3a84095 100644
--- a/src/ballistica/shared/python/python_command.h
+++ b/src/ballistica/shared/python/python_command.h
@@ -12,14 +12,14 @@ namespace ballistica {
// String based Python commands.
-// Note to self: originally I though I'd be using this in a lot of places,
-// so I added the ability to compile once and run repeatedly, quietly capture
-// output instead of printing it, etc. Now, however, its usage is pretty
-// much limited to a few places such as handling stdin and the in-game console.
-// (Most places it is much cleaner to work with proper python modules and just
-// interact with PyObject* refs to them)
-// I should look and see if python's default high level calls would suffice
-// for these purposes and potentially kill this off.
+// Note to self: Originally I though I'd be using this in a lot of places,
+// so I added the ability to compile once and run repeatedly, quietly
+// capture output instead of printing it, etc. Now, however, its usage is
+// pretty much limited to a few places such as handling stdin and the
+// in-app console. (Most places it is much cleaner to work with proper
+// python modules and just interact with PyObject* refs to them) I should
+// look and see if python's default high level calls would suffice for these
+// purposes and potentially kill this off.
class PythonCommand {
public:
PythonCommand();
diff --git a/src/ballistica/shared/python/python_ref.cc b/src/ballistica/shared/python/python_ref.cc
index c05db174..660a3ea4 100644
--- a/src/ballistica/shared/python/python_ref.cc
+++ b/src/ballistica/shared/python/python_ref.cc
@@ -4,6 +4,7 @@
#include "ballistica/core/python/core_python.h"
#include "ballistica/core/support/base_soft.h"
+#include "ballistica/shared/foundation/types.h"
#include "ballistica/shared/math/vector2f.h"
#include "ballistica/shared/python/python.h"
#include "ballistica/shared/python/python_sys.h"
@@ -13,7 +14,6 @@ namespace ballistica {
// Note: implicitly using core globals here; our behavior is undefined
// if core has not been imported by anyone yet.
using core::g_base_soft;
-using core::g_core;
// Ignore a few things that python macros do.
#pragma clang diagnostic push
@@ -179,6 +179,16 @@ auto PythonRef::ValueAsOptionalString() const -> std::optional {
return Python::GetPyString(obj_);
}
+auto PythonRef::ValueAsOptionalStringSequence() const
+ -> std::optional> {
+ assert(Python::HaveGIL());
+ ThrowIfUnset();
+ if (obj_ == Py_None) {
+ return {};
+ }
+ return Python::GetPyStrings(obj_);
+}
+
auto PythonRef::ValueAsInt() const -> int64_t {
assert(Python::HaveGIL());
ThrowIfUnset();
@@ -238,15 +248,8 @@ auto PythonRef::UnicodeCheck() const -> bool {
return static_cast(PyUnicode_Check(obj_));
}
-auto PythonRef::Call(PyObject* args, PyObject* keywds, bool print_errors) const
- -> PythonRef {
- assert(obj_);
- assert(Python::HaveGIL());
- assert(CallableCheck());
- assert(args);
- assert(PyTuple_Check(args)); // NOLINT (signed bitwise stuff)
- assert(!keywds || PyDict_Check(keywds)); // NOLINT (signed bitwise)
- PyObject* out = PyObject_Call(obj_, args, keywds);
+static inline auto _HandleCallResults(PyObject* out, bool print_errors)
+ -> PyObject* {
if (!out) {
if (print_errors) {
// Save/restore error or it can mess with context print calls.
@@ -262,20 +265,36 @@ auto PythonRef::Call(PyObject* args, PyObject* keywds, bool print_errors) const
}
PyErr_Clear();
}
+ return out;
+}
+
+auto PythonRef::Call(PyObject* args, PyObject* keywds, bool print_errors) const
+ -> PythonRef {
+ assert(obj_);
+ assert(Python::HaveGIL());
+ assert(CallableCheck());
+ assert(args);
+ assert(PyTuple_Check(args)); // NOLINT (signed bitwise stuff)
+ assert(!keywds || PyDict_Check(keywds)); // NOLINT (signed bitwise)
+ PyObject* out = PyObject_Call(obj_, args, keywds);
+ out = _HandleCallResults(out, print_errors);
return out ? PythonRef(out, PythonRef::kSteal) : PythonRef();
}
-auto PythonRef::Call() const -> PythonRef {
- // NOTE: Using core globals directly here; normally don't do this.
- assert(g_core);
- return Call(
- g_core->python->objs().Get(core::CorePython::ObjID::kEmptyTuple).Get());
+auto PythonRef::Call(bool print_errors) const -> PythonRef {
+ assert(obj_);
+ assert(Python::HaveGIL());
+ assert(CallableCheck());
+ PyObject* out = PyObject_CallNoArgs(obj_);
+ out = _HandleCallResults(out, print_errors);
+ return out ? PythonRef(out, PythonRef::kSteal) : PythonRef();
}
-auto PythonRef::Call(const Vector2f& val) const -> PythonRef {
+auto PythonRef::Call(const Vector2f& val, bool print_errors) const
+ -> PythonRef {
assert(Python::HaveGIL());
PythonRef args(Py_BuildValue("((ff))", val.x, val.y), PythonRef::kSteal);
- return Call(args);
+ return Call(args.Get(), nullptr, print_errors);
}
PythonRef::~PythonRef() { Release(); }
diff --git a/src/ballistica/shared/python/python_ref.h b/src/ballistica/shared/python/python_ref.h
index 3e16f242..9c5cc0e0 100644
--- a/src/ballistica/shared/python/python_ref.h
+++ b/src/ballistica/shared/python/python_ref.h
@@ -3,6 +3,7 @@
#ifndef BALLISTICA_SHARED_PYTHON_PYTHON_REF_H_
#define BALLISTICA_SHARED_PYTHON_PYTHON_REF_H_
+#include
#include
#include
@@ -166,6 +167,8 @@ class PythonRef {
auto ValueAsString() const -> std::string;
auto ValueAsOptionalString() const -> std::optional;
+ auto ValueAsOptionalStringSequence() const
+ -> std::optional>;
auto ValueAsInt() const -> int64_t;
@@ -185,10 +188,10 @@ class PythonRef {
bool print_errors = true) const -> PythonRef {
return Call(args.Get(), keywds.Get(), print_errors);
}
- auto Call() const -> PythonRef;
+ auto Call(bool print_errors = true) const -> PythonRef;
/// Call with Vector2f passed as a tuple.
- auto Call(const Vector2f& val) const -> PythonRef;
+ auto Call(const Vector2f& val, bool print_errors = true) const -> PythonRef;
private:
void ThrowIfUnset() const;
diff --git a/tools/batools/dummymodule.py b/tools/batools/dummymodule.py
index 0c18cd71..c2ae4d54 100755
--- a/tools/batools/dummymodule.py
+++ b/tools/batools/dummymodule.py
@@ -271,6 +271,8 @@ def _writefuncs(
returnstr = 'return (0.0, 0.0)'
elif returns == 'str | None':
returnstr = "return ''"
+ elif returns == 'int | None':
+ returnstr = 'return 0'
elif returns == 'tuple[float, float, float, float]':
returnstr = 'return (0.0, 0.0, 0.0, 0.0)'
elif returns == 'bauiv1.Widget | None':
diff --git a/tools/batools/pruneincludes.py b/tools/batools/pruneincludes.py
index 9b24405a..3a282fa0 100755
--- a/tools/batools/pruneincludes.py
+++ b/tools/batools/pruneincludes.py
@@ -105,7 +105,7 @@ class Pruner:
self.paths = [os.path.abspath(p) for p in self.paths]
def _get_entries(self) -> list[_CompileCommandsEntry]:
- cmdspath = '.cache/irony/compile_commands.json'
+ cmdspath = '.cache/compile_commands_db/compile_commands.json'
if not os.path.isfile(cmdspath):
raise CleanError(
f'Compile-commands not found at "{cmdspath}".'
diff --git a/tools/batools/spinoff/_context.py b/tools/batools/spinoff/_context.py
index 807751f5..c7e1306b 100644
--- a/tools/batools/spinoff/_context.py
+++ b/tools/batools/spinoff/_context.py
@@ -786,34 +786,16 @@ class SpinoffContext:
if 'base' in self._src_omit_feature_sets:
text = replace_exact(
text,
- (
- 'def _main() -> None:\n'
- ' # Run a default configure BEFORE importing'
- ' babase.\n'
- ' # (may affect where babase comes from).\n'
- ' configure()\n'
- '\n'
- ' import babase\n'
- '\n'
- ' babase.app.run()\n'
- '\n'
- ),
- (
- 'def _main() -> None:\n'
- ' # DISABLED; REQUIRES BASE FEATURE SET.\n'
- ' # Run a default configure BEFORE importing'
- ' babase.\n'
- ' # (may affect where babase comes from).\n'
- ' # configure()\n'
- '\n'
- ' # import babase\n'
- '\n'
- ' # babase.app.run()\n'
- '\n'
- " raise RuntimeError('App-exec requires"
- " base feature set.')\n"
- '\n'
- ),
+ ' import babase\n',
+ ' # (Hack; spinoff disabled babase).\n'
+ ' if TYPE_CHECKING:\n'
+ ' from typing import Any\n'
+ '\n'
+ ' # import babase\n'
+ '\n'
+ ' babase: Any = None\n'
+ ' if bool(True):\n'
+ " raise CleanError('babase not present')\n",
label=src_path,
)
@@ -1587,7 +1569,7 @@ class SpinoffContext:
os.chmod(dst_path_full, mode)
else:
raise RuntimeError(
- f"Invalid entity type: '{src_entity['type']}'."
+ f"Invalid entity type: '{src_entity.entity_type}'."
)
# NOTE TO SELF - was using lchmod here but it doesn't exist
diff --git a/tools/batools/staging.py b/tools/batools/staging.py
index 95effb0d..322293da 100755
--- a/tools/batools/staging.py
+++ b/tools/batools/staging.py
@@ -526,7 +526,7 @@ class AssetStager:
'# Basically this will do:\n'
'# import baenv; baenv.configure();'
' import babase; babase.app.run().\n'
- 'exec python3.11 ba_data/python/baenv.py $@\n'
+ 'exec python3.11 ba_data/python/baenv.py "$@"\n'
)
subprocess.run(['chmod', '+x', path], check=True)
diff --git a/tools/efro/error.py b/tools/efro/error.py
index ed731ae3..8bd4c8c7 100644
--- a/tools/efro/error.py
+++ b/tools/efro/error.py
@@ -25,16 +25,18 @@ class CleanError(Exception):
more descriptive exception types.
"""
- def pretty_print(self, flush: bool = False) -> None:
+ def pretty_print(self, flush: bool = True, prefix: str = 'Error') -> None:
"""Print the error to stdout, using red colored output if available.
If the error has an empty message, prints nothing (not even a newline).
"""
from efro.terminal import Clr
+ if prefix:
+ prefix = f'{prefix}: '
errstr = str(self)
if errstr:
- print(f'{Clr.SRED}{errstr}{Clr.RST}', flush=flush)
+ print(f'{Clr.SRED}{prefix}{errstr}{Clr.RST}', flush=flush)
class CommunicationError(Exception):