diff --git a/.efrocachemap b/.efrocachemap
index 8c71bc14..474ca50f 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -421,11 +421,11 @@
"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/a1/f9/645b8c7e1e99dd11446bc77005da",
- "build/assets/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/83/87/06fc7255ebf8a895ad37d2dfa13d",
+ "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/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/50/4c/4fb39f065b1a2f0320026a2e1b92",
+ "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",
"build/assets/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/f3/ce/219840946cb8f9aa6d3e25927ab3",
"build/assets/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/3f/d6/9080783d5c9dcc0af737f02b6f1e",
@@ -438,7 +438,7 @@
"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/00/ba/cf1b8bb9f7914f64647d4665b0a8",
+ "build/assets/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/a9/b5/10de2f3928d8c1f4887e0975743f",
"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/korean.json": "https://files.ballistica.net/cache/ba1/bd/c1/3f8632adda5517059323d928f192",
@@ -450,13 +450,13 @@
"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/bd/5e/80c74f96bb50d270396d437d6750",
+ "build/assets/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/45/dd/ce6d9dd446293f5e0ae541f36943",
"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/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/venetian.json": "https://files.ballistica.net/cache/ba1/e4/74/5c85bc56487bb715712c8b90a1eb",
+ "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",
"build/assets/ba_data/data/maps/bridgit.json": "https://files.ballistica.net/cache/ba1/6a/ea/74805f4880cc11237c5734a24422",
@@ -951,7 +951,7 @@
"build/assets/ba_data/python-site-packages/certifi/cacert.pem": "https://files.ballistica.net/cache/ba1/6a/c2/9a6bccca11cd2ed7e16e27dfccec",
"build/assets/ba_data/python-site-packages/certifi/core.py": "https://files.ballistica.net/cache/ba1/1b/50/5388f1475fabd1b60031f985271c",
"build/assets/ba_data/python-site-packages/typing_extensions.py": "https://files.ballistica.net/cache/ba1/08/4d/93bb609d798a3930dfb5e25eba59",
- "build/assets/ba_data/python-site-packages/yaml/__init__.py": "https://files.ballistica.net/cache/ba1/55/7c/37ea8dbd4fa4d6dac97f399b6fdd",
+ "build/assets/ba_data/python-site-packages/yaml/__init__.py": "https://files.ballistica.net/cache/ba1/2b/74/7e5772c203377222afc888ac6b71",
"build/assets/ba_data/python-site-packages/yaml/composer.py": "https://files.ballistica.net/cache/ba1/ce/f8/71e1f5f99ba2a7c44941b70afb06",
"build/assets/ba_data/python-site-packages/yaml/constructor.py": "https://files.ballistica.net/cache/ba1/8a/15/e361e34b79491c81553bb3534062",
"build/assets/ba_data/python-site-packages/yaml/cyaml.py": "https://files.ballistica.net/cache/ba1/9b/11/cba12e6f1cf2efe1725a20d7e1e5",
@@ -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/a8/5e/e644cd4120304fba4d4dbd9762e2",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/92/73/0a1325df721b51d0c9f2e0f6f075",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/24/f8/b372c8e02345a5540bd976069de8",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/81/5d/8368c53554a9b997f0a1d38c0ced",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/cb/90/6f74ab7d83e5ff9db887f391664a",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/cd/51/6639bbf611e918b21c1676b25764",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/d1/4b/5f2cb963955fd973bd7498c57519",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/9b/9f/1e1d79b604749c2033e7fad06394",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/97/f6/2f0e7dba9be2437f43dc4af5e449",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/2e/22/297bedb0cbfb169fb9ba607bff1b",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/c6/3e/6b09e07290a01483186b2ca44f8b",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/9e/c9/89f8f396ba8597453d81542f6ff6",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/0b/d8/4b6df9ec36d59a5560615c763bbf",
- "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/2d/5e/f6e1619a73c21c886425d878b96a",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/b6/4b/c7e9a14a16d69b20313f7007662c",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/2c/00/fe4d13eb53c0d825da2750b30cec",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/69/19/13f59849fe2a31eac8a350db2e12",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/01/ab/f243ffaa211fd9463cc7ede10f29",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/87/d3/88a28894dc15e511f3ba56f44267",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/b5/1e/bb7075104c420ca1c0663d24768c",
- "build/prefab/lib/linux_arm64_gui/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/84/aa/534f35b6499762739646ea173382",
- "build/prefab/lib/linux_arm64_gui/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/25/2f/3bd787d6debb2c4073fd6c2e8098",
- "build/prefab/lib/linux_arm64_server/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/84/aa/534f35b6499762739646ea173382",
- "build/prefab/lib/linux_arm64_server/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/25/2f/3bd787d6debb2c4073fd6c2e8098",
- "build/prefab/lib/linux_x86_64_gui/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/b8/01/153180116f1ab302aa8e6fd9ca9c",
- "build/prefab/lib/linux_x86_64_gui/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/93/1c/1aba110dcf69d8651b428f2927ed",
- "build/prefab/lib/linux_x86_64_server/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/b8/01/153180116f1ab302aa8e6fd9ca9c",
- "build/prefab/lib/linux_x86_64_server/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/93/1c/1aba110dcf69d8651b428f2927ed",
- "build/prefab/lib/mac_arm64_gui/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/4b/1a/21983185f7bdd78842b572535dad",
- "build/prefab/lib/mac_arm64_gui/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/3c/1d/d7864d7822c64ee06cee0dde659e",
- "build/prefab/lib/mac_arm64_server/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/4b/1a/21983185f7bdd78842b572535dad",
- "build/prefab/lib/mac_arm64_server/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/3c/1d/d7864d7822c64ee06cee0dde659e",
- "build/prefab/lib/mac_x86_64_gui/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/65/ab/524fe2f6a339b6480173c2c1624a",
- "build/prefab/lib/mac_x86_64_gui/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/47/61/eca0961c54b2eae2cf65fac7848d",
- "build/prefab/lib/mac_x86_64_server/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/06/5c/90c3a49e16a004e2db71909af919",
- "build/prefab/lib/mac_x86_64_server/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/47/61/eca0961c54b2eae2cf65fac7848d",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/d3/34/d2fa72d15a085424bad4157a6f2e",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/56/5f/6cde7712eebd76bcd9081b1d063a",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/8e/bb/cde5d48031a147358f49372348fc",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/82/e5/7d8d72481b84b81a3ec2b85cddf1",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/74/11/5059d262beb03fda192c967760ea",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/bf/57/94af76a5f7f51c10e9725730469e",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/fc/56/19374bffec117190ae9c132cff68",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/c6/c9/b6828fe5295e6d5df08fad9ebf3f",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/31/48/05c88daed3cf3e90f5a051ec6a0c",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/72/f9/d85a330987d81ffc895fc8ec922c",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/1a/92/6d465e12a3460498d978f31abf15",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/0c/0e/1b219bc8d0b6d7ccf57bdd721860",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/4a/7f/1068c0311d88aa8145e487f0e30b",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/1d/0d/fd0d55be1b0e69de4c4667d34644",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/3f/d0/cec6952032a5ddc8c1a63b1e1ba3",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/a7/6e/b40ca2ec7dde8c0e9edb0db376c1",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/fe/88/123692ae9ffbc97aaddbd51c0f30",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/16/31/b64154143f4e903388b9013b41c7",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/c0/f9/aea06ce01cffb7931aca46285316",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/d0/a0/a9a8c9fe3addcc0a9f7a014acef1",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/66/e3/d827e9edb1363d55a85636fc5f58",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/b4/05/e5a12646d900aeeaa7e974dabda2",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/94/16/24b8715c0f5c2eb6699ff20e594d",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/83/6d/d914432abe2d071890a6b3e2d6f1",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/10/b7/04b0835c36649b8bcd70abf2f3d7",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/0a/13/807d5e92e146b5e9bfc679dbd44b",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/3f/1e/2970e115041bc1828b3b2b8ef4db",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/9f/28/bf466437892861a399375c864a0d",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/4e/69/3d3715ffb88e61962dff80e52fa0",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/df/78/f138dbf92a93dcd647831fb8fde4",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/4e/69/3d3715ffb88e61962dff80e52fa0",
+ "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/df/78/f138dbf92a93dcd647831fb8fde4",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/4d/45/84cd8d36933f680c4c5ea6ed56e3",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/31/e8/ebc78517b4f6c3dba799d96b6770",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/4d/45/84cd8d36933f680c4c5ea6ed56e3",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/31/e8/ebc78517b4f6c3dba799d96b6770",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/16/31/fa50eca4cccba5819aba7598cdd2",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/80/f5/1e75ca051bcc9cf5622443368820",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/16/31/fa50eca4cccba5819aba7598cdd2",
+ "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/80/f5/1e75ca051bcc9cf5622443368820",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/34/26/fe4dacd23b76a39c024e220a6851",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/84/7d/952ba7e47c98635853b6b3e046fa",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/ad/de/141e3f5ea646f9d359a7edc40524",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "https://files.ballistica.net/cache/ba1/84/7d/952ba7e47c98635853b6b3e046fa",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/0d/e3/9a30e693bc57d27a093019988e2d",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/b3/df/933a84818c7a7f7059c5d2aef159",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/76/46/7e4792528fdc5ee7b432b8239567",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/03/54/742e4cae6ce81180e6977ccf3cd1",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/35/1b/cd6f88e5a7dda82848362d3ee41e",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/8f/b4/32805c7732aa03bc4417aed0f61b",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/7a/10/3c2abaa7fa0280fe5111209a9fd5",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/60/88/4d71bc815388e9d867d8ffa95c61",
"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/3e/7a/203e2a5d2b5bb42cfe3fd2fe16c2",
@@ -4119,7 +4119,7 @@
"src/ballistica/classic/mgen/pyembed/binding_classic.inc": "https://files.ballistica.net/cache/ba1/3c/eb/412513963f0818ab39c58bf292e3",
"src/ballistica/core/mgen/pyembed/binding_core.inc": "https://files.ballistica.net/cache/ba1/9d/0a/3c9636138e35284923e0c8311c69",
"src/ballistica/core/mgen/pyembed/env.inc": "https://files.ballistica.net/cache/ba1/8b/e4/6e5818f360d10b7b0224a9e91d07",
- "src/ballistica/core/mgen/python_modules_monolithic.h": "https://files.ballistica.net/cache/ba1/c4/8c/4f5294e83eb1ff22edffebf2bf8b",
+ "src/ballistica/core/mgen/python_modules_monolithic.h": "https://files.ballistica.net/cache/ba1/fb/96/7ed1c7db0c77d8deb4f00a7103c5",
"src/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc": "https://files.ballistica.net/cache/ba1/d8/0f/970053099b3044204bfe29ddefce",
"src/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc": "https://files.ballistica.net/cache/ba1/44/a4/5492db057bf7f7158c3b0fa11f0f",
"src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "https://files.ballistica.net/cache/ba1/6e/98/2bd0dda68e8b821f5b5cc18ce1d5"
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index ddecd372..9c646237 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -233,6 +233,8 @@
ballistica's
ballisticakit
ballisticakitcb
+ ballisticakitso
+ ballisticaplus
bamaster
bamasteraddr
bamasterlegacy
@@ -318,6 +320,7 @@
bmas
bmasl
bmcjphh
+ bmodfeaturesets
bname
bndl
boffs
@@ -522,6 +525,7 @@
clrred
cmakelist
cmakelists
+ cmakemodular
cmakeserver
cmath
cmathmodule
@@ -729,6 +733,7 @@
dingsound
dingsoundhigh
dinl
+ dir's
dirfilter
dirmanifest
dirname
@@ -832,6 +837,7 @@
dusing
dval
dxml
+ dylibdir
dynload
eachother
eaddrnotavail
@@ -894,6 +900,7 @@
enumvalue
enval
envcfg
+ envglobals
envhash
envname
envs
@@ -930,6 +937,7 @@
excludetypes
excstr
exec'ed
+ exec'ing
execcode
execed
execing
@@ -982,6 +990,7 @@
fdata
fdataraw
fdcount
+ fdcwd
fdesc
fdict
fdirs
@@ -1103,6 +1112,8 @@
fsconfigpath
fsdf
fset
+ fsetmfilenames
+ fsetmfilenamevals
fsetname
fsets
fsettings
@@ -1130,6 +1141,7 @@
fullpath
fullprice
fullscreen
+ fullstr
fulltest
funcname
funcnames
@@ -1403,6 +1415,7 @@
initializers
initialplayerinfos
initing
+ initname
inits
inittab
inmobi
@@ -1419,6 +1432,7 @@
insta
installdir
instancer
+ instpath
interfacetype
internalmodule
internalsrc
@@ -1551,6 +1565,7 @@
lfval
libballistica
libballisticakit
+ libballisticaplus
libbz
libbzip
libcrypto
@@ -1721,6 +1736,7 @@
maxw
maxwait
maxwidth
+ mbstowcs
mbytecount
mdiv
mdocs
@@ -1741,6 +1757,7 @@
metaprogramming
metascan
meteorshower
+ mfilename
mfpath
mhash
mhbegin
@@ -1984,6 +2001,7 @@
olde
oldlady
oldpath
+ oldpaths
oldtoken
olduuid
olduuids
@@ -2003,7 +2021,9 @@
opposingbody
opposingnode
opstr
+ optnm
optparse
+ optstuff
orchestrahitsound
origwrapper
ortho
@@ -2019,6 +2039,7 @@
ourhash
ourname
ourpackage
+ ourpaths
ourself
outdata
outdelay
@@ -2065,6 +2086,7 @@
pathbar
pathcapture
pathdst
+ pathlen
pathlib
pathlist
pathnames
@@ -2357,8 +2379,10 @@
pythondontwritebytecode
pythonenumsmodule
pythonhashseed
+ pythonoptimize
pythonpath
pythonpaths
+ pythonutf
pythonw
pytree
pytz
@@ -2390,6 +2414,8 @@
readexactly
readline
readlines
+ readlink
+ readlinkat
realpath
realsies
recache
@@ -2708,10 +2734,12 @@
sred
sriyakaal
sshd
+ ssize
sslcontext
sslproto
ssval
stackstr
+ stager
standin
starscale
startercache
@@ -2820,6 +2848,7 @@
syncitem
syncitems
synclist
+ syscall
sysconfigdata
sysctl
syslogmodule
@@ -3186,6 +3215,7 @@
winapi
winbeast
wincfg
+ wincfglc
wincount
winempty
winnergroup
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea9f29b6..28726bbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,38 @@
-### 1.7.23 (build 21171, api 8, 2023-07-16)
+### 1.7.24 (build 21183, api 8, 2023-07-21)
+
+- 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
+ 'modular'. The traditional form of the app, where we bootstrap Python
+ ourselves inside a standalone binary, is called 'monolithic'. To build and run
+ Ballistica in modular form, you can do `make cmake-modular`. This should make
+ it easier to use certain things like Python debuggers with Ballistica. While I
+ expect most builds of the game to remain monolithic, this may become the
+ default for certain situations such as server builds or possibly Linux builds
+ if it seems beneficial. We'll see. Modular mode should work on Linux and Mac
+ currently; other platforms remain monolithic-only for now.
+- Changed path wrangling a bit in baenv.py. All ballistica Python paths
+ (including python-site-packages) are now placed before any other existing
+ Python paths. This should provide a more consistent environment and means
+ Ballistica will always use its own version of things like yaml or certifi or
+ typing_extensions instead of one the user has installed via pip. Holler if you
+ run into any problems because of this and we can make an option to use the old
+ behavior where Ballistica's app and site paths get placed at the end.
+- It is now possible to manually run the app loop even on monolithic builds;
+ just do `PYTHONPATH=ba_data/python ./ballisticacore -c "import baenv;
+ baenv.configure(); import babase; babase.app.run()"`. This is basically the
+ same thing modular builds are doing except that they use a regular Python
+ interpreter instead of the ballisticakit binary.
+- Cleaned up the `tools/pcommand stage_assets` command. It now always expects a
+ separate `-debug` or `-release` arg. So old commands such as `tools/pcommand
+ stage_assets -win-Win32-Debug .` now look like `tools/pcommand stage_assets
+ -win-Win32 -debug .`. Please holler if you run into any broken asset-staging
+ calls in the Makefile/etc.
+- `FeatureSet.has_native_python_module` has been renamed to
+ `FeatureSet.has_python_binary_module` for better consistency with related
+ functionality.
+
+### 1.7.23 (build 21178, api 8, 2023-07-19)
- Network security improvements. (Thanks Dliwk!)
- You can now double click a chat message to copy it. (Thanks Vishal332008!)
diff --git a/Makefile b/Makefile
index 5427675b..4b78c784 100644
--- a/Makefile
+++ b/Makefile
@@ -243,16 +243,16 @@ prefab-mac-arm64-gui-debug: prefab-mac-arm64-gui-debug-build
prefab-mac-x86-64-gui-debug-build: prereqs assets-cmake \
build/prefab/full/mac_x86_64_gui/debug/ballisticakit
- @$(STAGE_ASSETS) -cmake build/prefab/full/mac_x86_64_gui/debug
+ @$(STAGE_ASSETS) -cmake -debug build/prefab/full/mac_x86_64_gui/debug
prefab-mac-arm64-gui-debug-build: prereqs assets-cmake \
build/prefab/full/mac_arm64_gui/debug/ballisticakit
- @$(STAGE_ASSETS) -cmake build/prefab/full/mac_arm64_gui/debug
+ @$(STAGE_ASSETS) -cmake -debug build/prefab/full/mac_arm64_gui/debug
build/prefab/full/mac_%_gui/debug/ballisticakit: .efrocachemap
@tools/pcommand efrocache_get $@
-build/prefab/lib/mac_%_gui/debug/libballistica_plus.a: .efrocachemap
+build/prefab/lib/mac_%_gui/debug/libballisticaplus.a: .efrocachemap
@tools/pcommand efrocache_get $@
# Mac gui release:
@@ -273,16 +273,16 @@ prefab-mac-arm64-gui-release: prefab-mac-arm64-gui_release-build
prefab-mac-x86-64-gui-release-build: prereqs assets-cmake \
build/prefab/full/mac_x86_64_gui/release/ballisticakit
- @$(STAGE_ASSETS) -cmake build/prefab/full/mac_x86_64_gui/release
+ @$(STAGE_ASSETS) -cmake -release build/prefab/full/mac_x86_64_gui/release
prefab-mac-arm64-gui-release-build: prereqs assets-cmake \
build/prefab/full/mac_arm64_gui/release/ballisticakit
- @$(STAGE_ASSETS) -cmake build/prefab/full/mac_arm64_gui/release
+ @$(STAGE_ASSETS) -cmake -release build/prefab/full/mac_arm64_gui/release
build/prefab/full/mac_%_gui/release/ballisticakit: .efrocachemap
@tools/pcommand efrocache_get $@
-build/prefab/lib/mac_%_gui/release/libballistica_plus.a: .efrocachemap
+build/prefab/lib/mac_%_gui/release/libballisticaplus.a: .efrocachemap
@tools/pcommand efrocache_get $@
# Mac server debug:
@@ -312,7 +312,7 @@ prefab-mac-arm64-server-debug-build: prereqs assets-server \
build/prefab/full/mac_%_server/debug/dist/ballisticakit_headless: .efrocachemap
@tools/pcommand efrocache_get $@
-build/prefab/lib/mac_%_server/debug/libballistica_plus.a: .efrocachemap
+build/prefab/lib/mac_%_server/debug/libballisticaplus.a: .efrocachemap
@tools/pcommand efrocache_get $@
# Mac server release:
@@ -344,7 +344,7 @@ prefab-mac-arm64-server-release-build: prereqs assets-server \
build/prefab/full/mac_%_server/release/dist/ballisticakit_headless: .efrocachemap
@tools/pcommand efrocache_get $@
-build/prefab/lib/mac_%_server/release/libballistica_plus.a: .efrocachemap
+build/prefab/lib/mac_%_server/release/libballisticaplus.a: .efrocachemap
@tools/pcommand efrocache_get $@
# Linux gui debug:
@@ -365,16 +365,16 @@ prefab-linux-arm64-gui-debug: prefab-linux-arm64-gui-debug-build
prefab-linux-x86-64-gui-debug-build: prereqs assets-cmake \
build/prefab/full/linux_x86_64_gui/debug/ballisticakit
- @$(STAGE_ASSETS) -cmake build/prefab/full/linux_x86_64_gui/debug
+ @$(STAGE_ASSETS) -cmake -debug build/prefab/full/linux_x86_64_gui/debug
prefab-linux-arm64-gui-debug-build: prereqs assets-cmake \
build/prefab/full/linux_arm64_gui/debug/ballisticakit
- @$(STAGE_ASSETS) -cmake build/prefab/full/linux_arm64_gui/debug
+ @$(STAGE_ASSETS) -cmake -debug build/prefab/full/linux_arm64_gui/debug
build/prefab/full/linux_%_gui/debug/ballisticakit: .efrocachemap
@tools/pcommand efrocache_get $@
-build/prefab/lib/linux_%_gui/debug/libballistica_plus.a: .efrocachemap
+build/prefab/lib/linux_%_gui/debug/libballisticaplus.a: .efrocachemap
@tools/pcommand efrocache_get $@
# Linux gui release:
@@ -395,16 +395,16 @@ prefab-linux-arm64-gui-release: prefab-linux-arm64-gui-release-build
prefab-linux-x86-64-gui-release-build: prereqs assets-cmake \
build/prefab/full/linux_x86_64_gui/release/ballisticakit
- @$(STAGE_ASSETS) -cmake build/prefab/full/linux_x86_64_gui/release
+ @$(STAGE_ASSETS) -cmake -release build/prefab/full/linux_x86_64_gui/release
prefab-linux-arm64-gui-release-build: prereqs assets-cmake \
build/prefab/full/linux_arm64_gui/release/ballisticakit
- @$(STAGE_ASSETS) -cmake build/prefab/full/linux_arm64_gui/release
+ @$(STAGE_ASSETS) -cmake -release build/prefab/full/linux_arm64_gui/release
build/prefab/full/linux_%_gui/release/ballisticakit: .efrocachemap
@tools/pcommand efrocache_get $@
-build/prefab/lib/linux_%_gui/release/libballistica_plus.a: .efrocachemap
+build/prefab/lib/linux_%_gui/release/libballisticaplus.a: .efrocachemap
@tools/pcommand efrocache_get $@
# Linux server debug:
@@ -436,7 +436,7 @@ prefab-linux-arm64-server-debug-build: prereqs assets-server \
build/prefab/full/linux_%_server/debug/dist/ballisticakit_headless: .efrocachemap
@tools/pcommand efrocache_get $@
-build/prefab/lib/linux_%_server/debug/libballistica_plus.a: .efrocachemap
+build/prefab/lib/linux_%_server/debug/libballisticaplus.a: .efrocachemap
@tools/pcommand efrocache_get $@
# Linux server release:
@@ -468,7 +468,7 @@ prefab-linux-arm64-server-release-build: prereqs assets-server \
build/prefab/full/linux_%_server/release/dist/ballisticakit_headless: .efrocachemap
@tools/pcommand efrocache_get $@
-build/prefab/lib/linux_%_server/release/libballistica_plus.a: .efrocachemap
+build/prefab/lib/linux_%_server/release/libballisticaplus.a: .efrocachemap
@tools/pcommand efrocache_get $@
# Windows gui debug:
@@ -482,7 +482,7 @@ prefab-windows-x86-gui-debug: prefab-windows-x86-gui-debug-build
prefab-windows-x86-gui-debug-build: prereqs assets-windows-$(WINPLAT_X86) \
build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe
- @$(STAGE_ASSETS) -win-$(WINPLAT_X86)-Debug \
+ @$(STAGE_ASSETS) -win-$(WINPLAT_X86) -debug \
build/prefab/full/windows_x86_gui/debug
build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe: .efrocachemap
@@ -506,7 +506,7 @@ prefab-windows-x86-gui-release: prefab-windows-x86-gui-release-build
prefab-windows-x86-gui-release-build: prereqs \
assets-windows-$(WINPLAT_X86) \
build/prefab/full/windows_x86_gui/release/BallisticaKit.exe
- @$(STAGE_ASSETS) -win-$(WINPLAT_X86)-Release \
+ @$(STAGE_ASSETS) -win-$(WINPLAT_X86) -release \
build/prefab/full/windows_x86_gui/release
build/prefab/full/windows_x86_gui/release/BallisticaKit.exe: .efrocachemap
@@ -531,7 +531,7 @@ prefab-windows-x86-server-debug: prefab-windows-x86-server-debug-build
prefab-windows-x86-server-debug-build: prereqs \
assets-windows-$(WINPLAT_X86) \
build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe
- @$(STAGE_ASSETS) -winserver-$(WINPLAT_X86)-Debug \
+ @$(STAGE_ASSETS) -winserver-$(WINPLAT_X86) -debug \
build/prefab/full/windows_x86_server/debug
build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe: .efrocachemap
@@ -556,7 +556,7 @@ prefab-windows-x86-server-release: prefab-windows-x86-server-release-build
prefab-windows-x86-server-release-build: prereqs \
assets-windows-$(WINPLAT_X86) \
build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe
- @$(STAGE_ASSETS) -winserver-$(WINPLAT_X86)-Release \
+ @$(STAGE_ASSETS) -winserver-$(WINPLAT_X86) -release \
build/prefab/full/windows_x86_server/release
build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe: .efrocachemap
@@ -931,7 +931,7 @@ WINDOWS_CONFIGURATION ?= Debug
# Stage assets and other files so a built binary will run.
windows-staging: assets-windows resources meta
- $(STAGE_ASSETS) -win-$(WINPLT)-$(WINCFG) \
+ $(STAGE_ASSETS) -win-$(WINPLT) -$(WINCFGLC) \
build/windows/$(WINCFG)_$(WINPLT)
# Build and run a debug windows build (from WSL).
@@ -1026,7 +1026,7 @@ cmake-lldb: cmake-build
# Build but don't run it.
cmake-build: assets-cmake resources cmake-binary
- @$(STAGE_ASSETS) -cmake build/cmake/$(CM_BT_LC)
+ @$(STAGE_ASSETS) -cmake -$(CM_BT_LC) build/cmake/$(CM_BT_LC)
@tools/pcommand echo BLD Build complete: BLU build/cmake/$(CM_BT_LC)
cmake-binary: meta
@@ -1046,25 +1046,43 @@ cmake-server: cmake-server-build
cmake-server-build: assets-server meta cmake-server-binary
@$(STAGE_ASSETS) -cmakeserver -$(CM_BT_LC) build/cmake/server-$(CM_BT_LC)
@tools/pcommand echo BLD \
- Server build complete: BLU build/cmake/server-$(CM_BT_LC)
+ Server build complete: BLU build/cmake/server-$(CM_BT_LC)
-# Build just the headless binary.
-# Note: We currently symlink FOO_headless. In packaged builds we rename it.
cmake-server-binary: meta
@tools/pcommand cmake_prep_dir build/cmake/server-$(CM_BT_LC)/dist
@cd build/cmake/server-$(CM_BT_LC)/dist && test -f Makefile \
|| cmake -DCMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) -DHEADLESS=true \
$(shell pwd)/ballisticakit-cmake
@tools/pcommand update_cmake_prefab_lib server $(CM_BT_LC) build/cmake/server-$(CM_BT_LC)/dist
- @cd build/cmake/server-$(CM_BT_LC)/dist && $(MAKE) -j$(CPUS)
+ @cd build/cmake/server-$(CM_BT_LC)/dist && $(MAKE) -j$(CPUS) \
+ ballisticakit
cmake-server-clean:
rm -rf build/cmake/server-$(CM_BT_LC)
+cmake-modular-build: assets-cmake meta cmake-modular-binary
+ @$(STAGE_ASSETS) -cmakemodular -$(CM_BT_LC) \
+ build/cmake/modular-$(CM_BT_LC)/staged
+ @tools/pcommand echo BLD \
+ 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
+
+cmake-modular-binary: meta
+ @tools/pcommand cmake_prep_dir build/cmake/modular-$(CM_BT_LC)
+ @cd build/cmake/modular-$(CM_BT_LC) && test -f Makefile \
+ || cmake -DCMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) \
+ $(shell pwd)/ballisticakit-cmake
+ @cd build/cmake/modular-$(CM_BT_LC) && $(MAKE) -j$(CPUS) ballisticakitso
+
+cmake-modular-clean:
+ rm -rf build/cmake/modular-$(CM_BT_LC)
+
# Stage assets for building/running within CLion.
clion-staging: assets-cmake resources meta
- $(STAGE_ASSETS) -cmake build/clion_debug
- $(STAGE_ASSETS) -cmake build/clion_release
+ $(STAGE_ASSETS) -cmake -debug build/clion_debug
+ $(STAGE_ASSETS) -cmake -release build/clion_release
# Tell make which of these targets don't represent files.
.PHONY: cmake cmake-build cmake-clean cmake-server cmake-server-build \
@@ -1162,6 +1180,7 @@ WIN_MSBUILD_EXE_B = "$(_WMSBE_1B)$(_WMSBE_2B)"
WINPRJ = $(WINDOWS_PROJECT)
WINPLT = $(WINDOWS_PLATFORM)
WINCFG = $(WINDOWS_CONFIGURATION)
+WINCFGLC = $(shell echo $(WINDOWS_CONFIGURATION) | tr A-Z a-z)
# When using CLion, our cmake dir is root. Expose .clang-format there too.
ballisticakit-cmake/.clang-format: .clang-format
diff --git a/ballisticakit-cmake/.idea/dictionaries/ericf.xml b/ballisticakit-cmake/.idea/dictionaries/ericf.xml
index 2ff36119..1d8e0e06 100644
--- a/ballisticakit-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticakit-cmake/.idea/dictionaries/ericf.xml
@@ -137,6 +137,8 @@
ballistica
ballistica's
ballisticakit
+ ballisticakitso
+ ballisticaplus
bamasteraddr
bamasterlegacy
bameta
@@ -206,6 +208,7 @@
bmas
bmasl
bmcjphh
+ bmodfeaturesets
bname
bodyid
bodypart
@@ -324,6 +327,7 @@
clipcount
cloudtoba
cmakelist
+ cmakemodular
cmath
cmds
cmdspath
@@ -454,6 +458,7 @@
diesound
diffbit
dinl
+ dir's
dirfilter
dirslash
displaytime
@@ -507,6 +512,7 @@
dusing
dval
dxgi
+ dylibdir
dynamicdata
echidna
echofile
@@ -539,6 +545,7 @@
enumvalue
enval
envcfg
+ envglobals
envs
envval
ericf
@@ -558,6 +565,7 @@
exargs
exctype
exec'ed
+ exec'ing
execed
execinfo
execing
@@ -592,6 +600,7 @@
fdata
fdataraw
fdcount
+ fdcwd
fdirs
fdirx
fdiry
@@ -660,6 +669,8 @@
fsarg
fsconfigpath
fset
+ fsetmfilenames
+ fsetmfilenamevals
fsetname
fsets
fsmetapackagename
@@ -673,6 +684,7 @@
ftos
ftou
fullpath
+ fullstr
funcname
funcp
fval
@@ -826,10 +838,12 @@
inides
initguid
initing
+ initname
inittab
inputdevice
inputter
insta
+ instpath
intercollide
internalformat
internalmodule
@@ -914,6 +928,7 @@
lgui
lhalf
libballistica
+ libballisticaplus
libbz
libbzip
libfile
@@ -998,6 +1013,7 @@
maxtries
maxwait
maxwidth
+ mbstowcs
mdpath
mediump
memalign
@@ -1014,6 +1030,7 @@
metallink
metamakefile
meth
+ mfilename
mhbegin
mhend
microsecs
@@ -1164,6 +1181,7 @@
olde
oldname
oldpath
+ oldpaths
oldtoken
olduuid
olduuids
@@ -1192,6 +1210,8 @@
opposingbody
opposingnode
optin
+ optnm
+ optstuff
ortho
osis
ossaudiodev
@@ -1201,6 +1221,7 @@
ourcode
ourname
ourpackage
+ ourpaths
ourself
ourstanding
outdict
@@ -1223,6 +1244,7 @@
pathbar
pathcapture
pathdst
+ pathlen
pathlist
pathparts
pathsegs
@@ -1362,6 +1384,8 @@
pysitedir
pythondevmode
pythonenumsmodule
+ pythonoptimize
+ pythonutf
pytype
qerr
qrcode
@@ -1386,6 +1410,8 @@
rdynamic
reaaaly
readexactly
+ readlink
+ readlinkat
readset
realloc
reallocations
@@ -1579,6 +1605,7 @@
sssssssi
ssssssssssss
ssval
+ stager
standin
startedptr
startms
@@ -1635,6 +1662,7 @@
swiftgeneratepch
swiftmergegeneratedheaders
symbolification
+ syscall
syscalls
sysresponse
tabdefs
@@ -1839,6 +1867,7 @@
wiimote
wiimotes
winapi
+ wincfglc
windowshade
winmm
winsock
diff --git a/ballisticakit-cmake/CMakeLists.txt b/ballisticakit-cmake/CMakeLists.txt
index f5763dfe..8d79abde 100644
--- a/ballisticakit-cmake/CMakeLists.txt
+++ b/ballisticakit-cmake/CMakeLists.txt
@@ -177,6 +177,12 @@ add_library(ode
)
target_include_directories(ode PRIVATE ${ODE_SRC_ROOT})
+# Necessary on GCC to allow linking in to our .so version.
+# (but disabling semantic-interposition should lessen the associated speed hit).
+if (CMAKE_CXX_COMPILER_ID MATCHES GNU)
+ target_compile_options(ode PRIVATE -fPIC -fno-semantic-interposition)
+endif ()
+
# NOTE: There used to be an issue with optimized GCC builds where mesh
# collisions would fail randomly, leading to characters falling through
# floors somewhat regularly. For this reason I was limiting optimization to
@@ -194,8 +200,7 @@ target_include_directories(ode PRIVATE ${ODE_SRC_ROOT})
# endif()
# endif ()
-# BallisticaKit binary.
-add_executable(ballisticakit
+set(BALLISTICA_SOURCES
${BA_SRC_ROOT}/external/qr_code_generator/QrCode.cpp
# AUTOGENERATED_PUBLIC_BEGIN (this section is managed by the "update_project" tool)
${BA_SRC_ROOT}/ballistica/base/app/app.cc
@@ -742,28 +747,65 @@ add_executable(ballisticakit
${BA_SRC_ROOT}/ballistica/ui_v1/widget/widget.cc
${BA_SRC_ROOT}/ballistica/ui_v1/widget/widget.h
# AUTOGENERATED_PUBLIC_END
- )
+)
+
+# BallisticaKit monolithic binary.
+add_executable(ballisticakit ${BALLISTICA_SOURCES})
# Gets -rdynamic added when linking gcc builds which exports all symbols
# which gives us more meaningful stack traces using backtrace_symbols().
set_target_properties(ballisticakit PROPERTIES ENABLE_EXPORTS 1)
if (HEADLESS)
- set_target_properties(ballisticakit PROPERTIES OUTPUT_NAME "ballisticakit_headless")
+ set_target_properties(ballisticakit
+ PROPERTIES OUTPUT_NAME "ballisticakit_headless")
endif ()
target_include_directories(ballisticakit PRIVATE
${Python_INCLUDE_DIRS}
${BA_SRC_ROOT}/external/open_dynamics_engine-ef
- ${EXTRA_INCLUDE_DIRS}
- )
+ ${EXTRA_INCLUDE_DIRS})
target_link_libraries(ballisticakit PRIVATE
- ${CMAKE_CURRENT_BINARY_DIR}/prefablib/libballistica_plus.a ode pthread ${Python_LIBRARIES}
+ ${CMAKE_CURRENT_BINARY_DIR}/prefablib/libballisticaplus.a ode pthread ${Python_LIBRARIES}
${SDL2_LIBRARIES} ${EXTRA_LIBRARIES} dl)
-# Hack for building on rpi (might be due to my manually built Python 3.8)
-# Hopefully can remove later...
+# Hack for building on rpi; need to update my pi so I can remove this.
if(EXISTS "/home/pi")
target_link_libraries(ballisticakit PRIVATE dl util stdc++fs)
endif()
+
+
+# BallisticaKit modular shared library
+# (for use with vanilla Python interpreters).
+add_library(ballisticakitso SHARED ${BALLISTICA_SOURCES})
+
+# This is a 'modular' build.
+target_compile_definitions(ballisticakitso PRIVATE BA_MONOLITHIC_BUILD=0)
+
+# Gets -rdynamic added when linking gcc builds which exports all symbols
+# which gives us more meaningful stack traces using backtrace_symbols().
+set_target_properties(ballisticakitso PROPERTIES ENABLE_EXPORTS 1)
+
+set_target_properties(ballisticakitso
+ PROPERTIES PREFIX "")
+set_target_properties(ballisticakitso
+ PROPERTIES SUFFIX ".so")
+
+if (HEADLESS)
+ set_target_properties(ballisticakitso
+ PROPERTIES OUTPUT_NAME "ballisticakit_headless")
+else ()
+ set_target_properties(ballisticakitso
+ PROPERTIES OUTPUT_NAME "ballisticakit")
+endif ()
+
+target_include_directories(ballisticakitso PRIVATE
+ ${Python_INCLUDE_DIRS}
+ ${BA_SRC_ROOT}/external/open_dynamics_engine-ef
+ ${EXTRA_INCLUDE_DIRS})
+
+target_link_libraries(ballisticakitso PRIVATE
+ ${CMAKE_CURRENT_BINARY_DIR}/prefablib/libballisticaplus.a ode pthread ${Python_LIBRARIES}
+ ${SDL2_LIBRARIES} ${EXTRA_LIBRARIES} dl)
+
diff --git a/config/featuresets/featureset_core.py b/config/featuresets/featureset_core.py
index 79658f97..f83ad5c7 100644
--- a/config/featuresets/featureset_core.py
+++ b/config/featuresets/featureset_core.py
@@ -14,7 +14,7 @@ fset = FeatureSet.get_active()
fset.requirements = set()
-fset.has_native_python_module = False
+fset.has_python_binary_module = False
# Bits of code we're using that don't conform to our feature-set based
# namespace scheme.
diff --git a/config/featuresets/featureset_scene_v1_lib.py b/config/featuresets/featureset_scene_v1_lib.py
index f74d682c..9d79d77b 100644
--- a/config/featuresets/featureset_scene_v1_lib.py
+++ b/config/featuresets/featureset_scene_v1_lib.py
@@ -12,6 +12,6 @@ from batools.featureset import FeatureSet
# Grab the FeatureSet we should apply to.
fset = FeatureSet.get_active()
-fset.has_native_python_module = False
+fset.has_python_binary_module = False
fset.requirements = {'core', 'base', 'scene_v1'}
diff --git a/config/featuresets/featureset_ui_v1_lib.py b/config/featuresets/featureset_ui_v1_lib.py
index 3f4f4eb9..16112371 100644
--- a/config/featuresets/featureset_ui_v1_lib.py
+++ b/config/featuresets/featureset_ui_v1_lib.py
@@ -12,6 +12,6 @@ from batools.featureset import FeatureSet
# Grab the FeatureSet we should apply to.
fset = FeatureSet.get_active()
-fset.has_native_python_module = False
+fset.has_python_binary_module = False
fset.requirements = {'core', 'base', 'ui_v1', 'classic'}
diff --git a/src/assets/ba_data/python/babase/_env.py b/src/assets/ba_data/python/babase/_env.py
index a651dc88..9fabca2c 100644
--- a/src/assets/ba_data/python/babase/_env.py
+++ b/src/assets/ba_data/python/babase/_env.py
@@ -54,8 +54,10 @@ def on_native_module_import() -> None:
# make noise if that's not the case.
if debug_build != sys.flags.dev_mode:
logging.warning(
- 'Mismatch in ballistica debug_build %s'
- ' and sys.flags.dev_mode %s; this may cause problems.',
+ 'Ballistica was built with debug-mode %s'
+ ' but Python is running with dev-mode %s;'
+ ' this mismatch may cause problems.'
+ ' See https://docs.python.org/3/library/devmode.html',
debug_build,
sys.flags.dev_mode,
)
@@ -75,11 +77,11 @@ def setup_env_for_app_run() -> None:
assert baenv.config_exists()
# If we were unable to set paths earlier, complain now.
- if baenv.g_paths_set_failed:
+ if baenv.did_paths_set_fail():
logging.warning(
'Ballistica Python paths have not been set. This may cause'
' problems. To ensure paths are set, run baenv.configure()'
- ' before importing any ballistica modules.'
+ ' BEFORE importing any Ballistica modules.'
)
# Set up interrupt-signal handling.
@@ -144,9 +146,10 @@ def on_app_launching() -> None:
assert _babase.in_logic_thread()
# Let the user know if the app python dir is a custom one.
- if baenv.g_user_system_scripts_dir is not None:
+ user_sys_scripts_dir = baenv.get_user_system_scripts_dir()
+ if user_sys_scripts_dir is not None:
_babase.screenmessage(
- f"Using user system scripts: '{baenv.g_user_system_scripts_dir}'",
+ f"Using user system scripts: '{user_sys_scripts_dir}'",
color=(0.6, 0.6, 1.0),
)
diff --git a/src/assets/ba_data/python/baclassic/_store.py b/src/assets/ba_data/python/baclassic/_store.py
index 8caf4950..b82f73dc 100644
--- a/src/assets/ba_data/python/baclassic/_store.py
+++ b/src/assets/ba_data/python/baclassic/_store.py
@@ -267,6 +267,9 @@ class StoreSubsystem:
'icons.mikirog': {
'icon': babase.charstr(babase.SpecialChar.MIKIROG)
},
+ 'icons.explodinary': {
+ 'icon': babase.charstr(babase.SpecialChar.EXPLODINARY_LOGO)
+ },
}
return babase.app.classic.store_items
diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py
index 8cd7fc29..6f755b66 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -4,9 +4,11 @@
This module is used to set up and/or check the global Python environment
before running a ballistica app. This includes things such as paths,
-logging, debug-modes, garbage-collection settings, and signal handling.
-Because these things are global in nature, this should be done before
-any ballistica modules are imported.
+logging, and app-dirs. Because these things are global in nature, this
+should be done before any ballistica modules are imported.
+
+This module can also be exec'ed directly to set up a default environment
+and then run the app.
Ballistica can be used without explicitly configuring the environment in
order to integrate it in arbitrary Python environments, but this may
@@ -20,25 +22,42 @@ import logging
from pathlib import Path
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
+# /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.
+#
+# So we need to do a few things to handle that situation gracefully.
+#
+# - First, we need to store any mutable global state in the __main__
+# module; not in ourself. This way, alternate versions of ourself will
+# still know if we already ran configure/etc.
+#
+# - Second, we should avoid the use of isinstance and similar calls for
+# our types. An EnvConfig we create would technically be a different
+# type than that created by an alternate baenv.
+
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 21171
-TARGET_BALLISTICA_VERSION = '1.7.23'
-
-_g_env_config: EnvConfig | None = None
-g_paths_set_failed = False # pylint: disable=invalid-name
-g_user_system_scripts_dir: str | None = None
+TARGET_BALLISTICA_BUILD = 21183
+TARGET_BALLISTICA_VERSION = '1.7.24'
@dataclass
class EnvConfig:
- """Final settings put together by the configure call."""
+ """Environment put together by the configure call."""
config_dir: str
data_dir: str
@@ -49,17 +68,56 @@ class EnvConfig:
log_handler: LogHandler | None
+@dataclass
+class EnvGlobals:
+ """Our globals we store in the main module."""
+
+ config: EnvConfig | None = None
+ config_called: bool = False
+ paths_set_failed: bool = False
+ user_system_scripts_dir: str | None = None
+
+ @classmethod
+ def get(cls) -> EnvGlobals:
+ """Create/return our singleton."""
+ name = '_baenv_globals'
+ envglobals: EnvGlobals | None = getattr(__main__, name, None)
+ if envglobals is None:
+ envglobals = EnvGlobals()
+ setattr(__main__, name, envglobals)
+ return envglobals
+
+
def config_exists() -> bool:
"""Has a config been created?"""
- return _g_env_config is not None
+
+ 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
def get_config() -> EnvConfig:
- """Return the active env-config. Creates default if none exists."""
- if _g_env_config is None:
+ """Return the active config, creating a default if none exists."""
+ envglobals = EnvGlobals.get()
+
+ if not envglobals.config_called:
configure()
- assert _g_env_config is not None
- return _g_env_config
+
+ config = envglobals.config
+ if config is None:
+ raise RuntimeError(
+ 'baenv.configure() has been called but no config exists;'
+ ' perhaps it errored?'
+ )
+ return config
def configure(
@@ -72,22 +130,91 @@ def configure(
) -> None:
"""Set up the Python environment for running a ballistica app.
- This includes things such as Python paths and log redirection. For
- that reason, this should be called before any other ballistica
- modules are imported, since it may make changes to sys.path,
- affecting where those modules get loaded from.
+ This includes things such as Python path wrangling and app directory
+ creation. This should be called before any other ballistica modules
+ are imported since it may make changes to sys.path which can affect
+ where those modules get loaded from.
"""
- # pylint: disable=too-many-branches
- # pylint: disable=too-many-locals
- global _g_env_config # pylint: disable=global-statement
- if _g_env_config is not None:
- raise RuntimeError('An EnvConfig has already been created.')
+ envglobals = EnvGlobals.get()
- # The very first thing we do is set up our logging system and feed
- # Python's stdout/stderr into it. Then we can at least debug problems
- # on systems where native stdout/stderr is not easily accessible
- # such as Android.
+ if envglobals.config_called:
+ raise RuntimeError(
+ 'baenv.configure() has already been called;'
+ ' it can only be called once.'
+ )
+ envglobals.config_called = 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
+ # problems on systems where native stdout/stderr is not easily
+ # accessible such as Android.
+ log_handler = _setup_logging()
+
+ # We want to always be run in UTF-8 mode; complain if we're not.
+ if sys.flags.utf8_mode != 1:
+ logging.warning(
+ "Python's UTF-8 mode is not set. Running ballistica without"
+ ' it may lead to errors.'
+ )
+
+ # Attempt to set up Python paths and our own data paths so that
+ # engine modules, mods, etc. are pulled from predictable places.
+ (
+ user_python_dir,
+ app_python_dir,
+ site_python_dir,
+ data_dir,
+ config_dir,
+ standard_app_python_dir,
+ ) = _setup_paths(
+ user_python_dir,
+ app_python_dir,
+ site_python_dir,
+ data_dir,
+ config_dir,
+ )
+
+ # Attempt to create dirs that we'll write stuff to.
+ _setup_dirs(config_dir, user_python_dir)
+
+ # Get ssl working if needed so we can use https and all that.
+ _setup_certs(contains_python_dist)
+
+ # This is now the active config.
+ envglobals.config = EnvConfig(
+ config_dir=config_dir,
+ data_dir=data_dir,
+ user_python_dir=user_python_dir,
+ app_python_dir=app_python_dir,
+ standard_app_python_dir=standard_app_python_dir,
+ site_python_dir=site_python_dir,
+ log_handler=log_handler,
+ )
+
+
+def _calc_data_dir(data_dir: str | None) -> str:
+ if data_dir is None:
+ # To calc default data_dir, we assume this module was imported
+ # from that dir's ba_data/python subdir.
+ assert Path(__file__).parts[-3:-1] == ('ba_data', 'python')
+ data_dir_path = Path(__file__).parents[2]
+
+ # Prefer tidy relative paths like '.' if possible so that things
+ # like stack traces are easier to read.
+
+ # NOTE: Perhaps we should have an option to disable this
+ # behavior for cases where the user might be doing chdir stuff.
+ cwd_path = Path.cwd()
+ data_dir = str(
+ data_dir_path.relative_to(cwd_path)
+ if data_dir_path.is_relative_to(cwd_path)
+ else data_dir_path
+ )
+ return data_dir
+
+
+def _setup_logging() -> LogHandler:
log_handler = setup_logging(
log_path=None,
level=LogLevel.DEBUG,
@@ -95,41 +222,46 @@ def configure(
log_stdout_stderr=True,
cache_size_limit=1024 * 1024,
)
+ return log_handler
- # Sanity check: we should always be run in UTF-8 mode.
- if sys.flags.utf8_mode != 1:
- logging.warning(
- "Python's UTF-8 mode is not set. Running ballistica without"
- ' it may lead to errors.'
- )
- # Now do paths. We want to set stuff up so that engine modules,
- # mods, etc. are pulled from predictable places.
- cwd_path = Path.cwd()
+def _setup_certs(contains_python_dist: bool) -> None:
+ # In situations where we're bringing our own Python let's also
+ # provide our own root certs so ssl works. We can consider
+ # overriding this in particular embedded cases if we can verify that
+ # system certs are working. We also allow forcing this via an env
+ # var if the user desires.
+ if (
+ contains_python_dist
+ or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'
+ ):
+ import certifi
- # A few paths we can ALWAYS calculate since they don't affect Python
- # imports:
+ # Let both OpenSSL and requests (if present) know to use this.
+ os.environ['SSL_CERT_FILE'] = os.environ[
+ 'REQUESTS_CA_BUNDLE'
+ ] = certifi.where()
- # Default data_dir assumes this module was imported from its
- # ba_data/python subdir.
- if data_dir is None:
- assert Path(__file__).parts[-3:-1] == ('ba_data', 'python')
- data_dir_path = Path(__file__).parents[2]
- # Prefer tidy relative paths like '.' if possible.
- data_dir = str(
- data_dir_path.relative_to(cwd_path)
- if data_dir_path.is_relative_to(cwd_path)
- else data_dir_path
- )
+
+def _setup_paths(
+ user_python_dir: str | None,
+ app_python_dir: str | None,
+ site_python_dir: str | None,
+ data_dir: str | None,
+ config_dir: str | None,
+) -> tuple[str | None, str | None, str | None, str, str, str]:
+ # First a few paths we can ALWAYS calculate since they don't affect
+ # Python imports:
+
+ envglobals = EnvGlobals.get()
+
+ data_dir = _calc_data_dir(data_dir)
# Default config-dir is simply ~/.ballisticakit
if config_dir is None:
config_dir = str(Path(Path.home(), '.ballisticakit'))
- # Ok now Python paths.
-
- # By default, app-python-dir is simply ba_data/python under
- # data-dir.
+ # Standard app-python-dir is simply ba_data/python under data-dir.
standard_app_python_dir = str(Path(data_dir, 'ba_data', 'python'))
# If _babase has already been imported, there's not much we can do
@@ -137,12 +269,11 @@ def configure(
if '_babase' in sys.modules:
app_python_dir = user_python_dir = site_python_dir = None
- # We don't actually complain yet here; we simply take note
- # that we weren't able to set paths. Then we complain if/when
- # the app is started. This way, non-app uses of babase won't be
- # filled with unnecessary warnings.
- global g_paths_set_failed # pylint: disable=global-statement
- g_paths_set_failed = True
+ # We don't actually complain yet here; we simply take note that
+ # we weren't able to set paths. Then we complain if/when the app
+ # is started. This way, non-app uses of babase won't be filled
+ # with unnecessary warnings.
+ envglobals.paths_set_failed = True
else:
# Ok; _babase hasn't been imported yet so we can muck with
@@ -163,35 +294,58 @@ def configure(
# Wherever our user_python_dir is, if we find a sys/FOO dir
# under it where FOO matches our version, use that as our
- # app_python_dir.
- check_dir = os.path.join(
- user_python_dir, 'sys', TARGET_BALLISTICA_VERSION
- )
- if os.path.isdir(check_dir):
- global g_user_system_scripts_dir # pylint: disable=global-statement
- g_user_system_scripts_dir = check_dir
- app_python_dir = check_dir
+ # app_python_dir. This allows modding built-in stuff on
+ # platforms where there is no write access to said built-in
+ # 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)
# Ok, now apply these to sys.path.
# First off, strip out any instances of the path containing this
- # module. We will probably be re-adding the same path in a
- # moment but its technically possible that we won't be (if
- # app_python_dir is overridden to somewhere else, etc.)
+ # module. We will *probably* be re-adding the same path in a
+ # moment so this keeps things cleaner. Though hmm should we
+ # leave it in there in cases where we *don't* re-add the same
+ # path?...
our_parent_path = Path(__file__).parent.resolve()
- paths: list[str] = [
+ oldpaths: list[str] = [
p for p in sys.path if Path(p).resolve() != our_parent_path
]
- # Let's lookup mods first (so users can do whatever they want).
- # and then our bundled scripts last (don't want bundled
- # site-package stuff overwriting system versions)
- paths.insert(0, user_python_dir)
- paths.append(app_python_dir)
- paths.append(site_python_dir)
- sys.path = paths
- # Attempt to create the dirs that we'll write stuff to. Not the end
- # of the world if we fail though.
+ # Let's place mods first (so users can override whatever they
+ # want) followed by our app scripts and lastly our bundled site
+ # stuff.
+
+ # One could make the argument that at least our bundled app &
+ # site stuff should be placed at the end so actual local site
+ # stuff could override it. That could be a good thing or a bad
+ # thing. Maybe we could add an option for that, but for now I'm
+ # prioritizing our stuff to give as consistent an environment as
+ # possible.
+ ourpaths = [user_python_dir, app_python_dir, site_python_dir]
+
+ # Special case: our modular builds will have a 'python-dylib'
+ # dir alongside the 'python' scripts dir which contains our
+ # binary Python modules. If we see that, add it to the path also.
+ # Not sure if we'd ever have a need to customize this path.
+ dylibdir = f'{app_python_dir}-dylib'
+ if os.path.exists(dylibdir):
+ ourpaths.append(dylibdir)
+
+ sys.path = ourpaths + oldpaths
+
+ return (
+ user_python_dir,
+ app_python_dir,
+ site_python_dir,
+ data_dir,
+ config_dir,
+ standard_app_python_dir,
+ )
+
+
+def _setup_dirs(config_dir: str | None, user_python_dir: str | None) -> None:
create_dirs: list[tuple[str, str | None]] = [
('config', config_dir),
('user_python', user_python_dir),
@@ -201,32 +355,22 @@ def configure(
try:
os.makedirs(cdir, exist_ok=True)
except Exception:
+ # Not the end of the world if we can't make these dirs.
logging.warning(
"Unable to create %s dir at '%s'.", cdirname, cdir
)
- _g_env_config = EnvConfig(
- config_dir=config_dir,
- data_dir=data_dir,
- user_python_dir=user_python_dir,
- app_python_dir=app_python_dir,
- standard_app_python_dir=standard_app_python_dir,
- site_python_dir=site_python_dir,
- log_handler=log_handler,
- )
- # In embedded situations (when we're providing our own Python) let's
- # also provide our own root certs so ssl works. We can consider
- # overriding this in particular embedded cases if we can verify that
- # system certs are working. (We also allow forcing this via an env
- # var if the user desires)
- if (
- contains_python_dist
- or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'
- ):
- import certifi
+def _main() -> None:
+ # Run a default configure BEFORE importing babase.
+ # (may affect where babase comes from).
+ configure()
- # Let both OpenSSL and requests (if present) know to use this.
- os.environ['SSL_CERT_FILE'] = os.environ[
- 'REQUESTS_CA_BUNDLE'
- ] = certifi.where()
+ import babase
+
+ babase.app.run()
+
+
+# Allow exec'ing this module directly to do a standard app run.
+if __name__ == '__main__':
+ _main()
diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc
index a1f6ed03..fbb6773e 100644
--- a/src/ballistica/base/base.cc
+++ b/src/ballistica/base/base.cc
@@ -177,6 +177,11 @@ void BaseFeatureSet::StartApp() {
LogVersionInfo();
+ // The logic thread (or maybe other things) need to run Python as
+ // we're bringing them up, so let it go for the duration of this call.
+ // We'll explicitly grab it if/when we need it.
+ Python::ScopedInterpreterLockRelease gil_release;
+
// Read in ba.app.config for anyone who wants to start looking at it
// (though we don't explicitly ask anyone to apply it until later).
python->ReadConfig();
@@ -280,6 +285,10 @@ void BaseFeatureSet::RunAppToCompletion() {
StartApp();
}
+ // Let go of the GIL while we're running. The logic thread or other things
+ // will grab it when needed.
+ Python::ScopedInterpreterLockRelease gil_release;
+
// On our event-loop-managing platforms we now simply sit in our event
// loop until the app is quit.
g_core->main_event_loop()->RunEventLoop(false);
@@ -494,8 +503,13 @@ void BaseFeatureSet::DoV1CloudLog(const std::string& msg) {
// We may attempt to import stuff and that should *never* happen before
// base is fully imported.
if (!IsBaseCompletelyImported()) {
- printf(
- "WARNING: V1CloudLog called before babase fully imported; ignoring.\n");
+ static bool warned = false;
+ if (!warned) {
+ warned = true;
+ printf(
+ "WARNING: V1CloudLog called before babase fully imported; "
+ "ignoring.\n");
+ }
return;
}
@@ -523,7 +537,11 @@ void BaseFeatureSet::DoV1CloudLog(const std::string& msg) {
// Need plus for direct sends.
if (!HavePlus()) {
- printf("WARNING: V1CloudLog direct-sends not available; ignoring.\n");
+ static bool did_warn = false;
+ if (!did_warn) {
+ did_warn = true;
+ printf("WARNING: V1CloudLog direct-sends not available; ignoring.\n");
+ }
return;
}
diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc
index d4c121b7..62c9a69e 100644
--- a/src/ballistica/base/python/methods/python_methods_app.cc
+++ b/src/ballistica/base/python/methods/python_methods_app.cc
@@ -49,8 +49,6 @@ static PyMethodDef PyAppNameDef = {
static auto PyRunApp(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
- FatalError("NOT WORKING YET; COME BACK SOON.");
-
assert(g_base);
g_base->RunAppToCompletion();
diff --git a/src/ballistica/core/core.cc b/src/ballistica/core/core.cc
index effbeb24..a515a8c2 100644
--- a/src/ballistica/core/core.cc
+++ b/src/ballistica/core/core.cc
@@ -97,7 +97,7 @@ void CoreFeatureSet::PostInit() {
// 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();
+ // python->ReleaseMainThreadGIL();
}
auto CoreFeatureSet::CalcBuildSrcDir() -> std::string {
diff --git a/src/ballistica/core/python/core_python.cc b/src/ballistica/core/python/core_python.cc
index 40a76b88..9ff02388 100644
--- a/src/ballistica/core/python/core_python.cc
+++ b/src/ballistica/core/python/core_python.cc
@@ -202,14 +202,6 @@ void CorePython::ImportPythonObjs() {
}
}
-void CorePython::ReleaseMainThreadGIL() {
- assert(g_core->InMainThread());
- // After we bootstrap Python here in the main thread we release the GIL.
- // We'll explicitly reacquire it anytime we need it (mainly in the logic
- // thread once that comes up later).
- PyEval_SaveThread();
-}
-
void CorePython::SoftImportBase() {
auto gil{Python::ScopedInterpreterLock()};
auto result = PythonRef::StolenSoft(PyImport_ImportModule("_babase"));
@@ -345,29 +337,4 @@ void CorePython::LoggingCall(LogLevel loglevel, const std::string& msg) {
objs().Get(logcallobj).Call(args);
}
-void CorePython::AcquireGIL() {
- assert(g_base_soft && g_base_soft->InLogicThread());
- auto debug_timing{g_core->core_config().debug_timing};
- millisecs_t startms{debug_timing ? CorePlatform::GetCurrentMillisecs() : 0};
-
- if (logic_thread_state_) {
- PyEval_RestoreThread(logic_thread_state_);
- logic_thread_state_ = nullptr;
- }
-
- if (debug_timing) {
- auto duration{CorePlatform::GetCurrentMillisecs() - startms};
- if (duration > (1000 / 120)) {
- Log(LogLevel::kInfo,
- "GIL acquire took too long (" + std::to_string(duration) + " ms).");
- }
- }
-}
-
-void CorePython::ReleaseGIL() {
- assert(g_base_soft && g_base_soft->InLogicThread());
- assert(logic_thread_state_ == nullptr);
- logic_thread_state_ = PyEval_SaveThread();
-}
-
} // namespace ballistica::core
diff --git a/src/ballistica/core/python/core_python.h b/src/ballistica/core/python/core_python.h
index 619f6eca..9b9ab74e 100644
--- a/src/ballistica/core/python/core_python.h
+++ b/src/ballistica/core/python/core_python.h
@@ -52,11 +52,8 @@ class CorePython {
/// logging is available, logs locally using Logging::DisplayLog()
/// (with an added warning).
void LoggingCall(LogLevel loglevel, const std::string& msg);
- void AcquireGIL();
- void ReleaseGIL();
void ImportPythonObjs();
void VerifyPythonEnvironment();
- void ReleaseMainThreadGIL();
void SoftImportBase();
const auto& objs() { return objs_; }
@@ -64,8 +61,6 @@ class CorePython {
private:
PythonObjectSet objs_;
- PyThreadState* logic_thread_state_{};
-
// Log calls we make before we're set up to ship logs through Python
// go here. They all get shipped at once as soon as it is possible.
bool python_logging_calls_enabled_{};
diff --git a/src/ballistica/plus/README.md b/src/ballistica/plus/README.md
index 0451f7e4..fcb16a64 100644
--- a/src/ballistica/plus/README.md
+++ b/src/ballistica/plus/README.md
@@ -2,5 +2,5 @@
Bits of the engine related to accounts and cloud functionality. In prefab builds
the compiled code for this feature set is contained in the pre-compiled static
-ballistica_plus library. The plus feature set can also be removed from
+ballisticaplus library. The plus feature set can also be removed from
spinoff projects if desired to remove the need for that library.
diff --git a/src/ballistica/scene_v1/connection/connection_to_client.cc b/src/ballistica/scene_v1/connection/connection_to_client.cc
index 2fcda718..8e3cdc9c 100644
--- a/src/ballistica/scene_v1/connection/connection_to_client.cc
+++ b/src/ballistica/scene_v1/connection/connection_to_client.cc
@@ -385,16 +385,18 @@ void ConnectionToClient::HandleMessagePacket(
case BA_MESSAGE_CLIENT_INFO: {
if (buffer.size() > 1) {
+ // Make a null-terminated copy of the string data.
std::vector str_buffer(buffer.size());
- memcpy(&(str_buffer[0]), &(buffer[1]), buffer.size() - 1);
+ memcpy(str_buffer.data(), buffer.data() + 1, buffer.size() - 1);
str_buffer[str_buffer.size() - 1] = 0;
- cJSON* info = cJSON_Parse(reinterpret_cast(&(buffer[1])));
+
+ cJSON* info = cJSON_Parse(str_buffer.data());
if (info) {
cJSON* b = cJSON_GetObjectItem(info, "b");
if (b) {
build_number_ = b->valueint;
} else {
- Log(LogLevel::kError, "no buildnumber in clientinfo msg");
+ Log(LogLevel::kError, "No buildnumber in clientinfo msg.");
}
// Grab their token (we use this to ask the
@@ -403,7 +405,7 @@ void ConnectionToClient::HandleMessagePacket(
if (t) {
token_ = t->valuestring;
} else {
- Log(LogLevel::kError, "no token in clientinfo msg");
+ Log(LogLevel::kError, "No token in clientinfo msg.");
}
// Newer clients also pass a peer-hash, which
@@ -423,9 +425,9 @@ void ConnectionToClient::HandleMessagePacket(
cJSON_Delete(info);
} else {
Log(LogLevel::kError,
- "got invalid json in clientinfo message: '"
+ "Got invalid json in clientinfo message: '"
+ std::string(reinterpret_cast(&(buffer[1])))
- + "'");
+ + "'.");
}
}
got_client_info_ = true;
diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc
index 1ddfbe7d..88f590e9 100644
--- a/src/ballistica/shared/ballistica.cc
+++ b/src/ballistica/shared/ballistica.cc
@@ -22,7 +22,7 @@
#endif
// If desired, define main() in the global namespace.
-#if BA_DEFINE_MAIN
+#if BA_MONOLITHIC_BUILD && BA_DEFINE_MAIN
auto main(int argc, char** argv) -> int {
auto core_config =
ballistica::core::CoreConfig::FromCommandLineAndEnv(argc, argv);
@@ -39,8 +39,10 @@ auto main(int argc, char** argv) -> int {
namespace ballistica {
// These are set automatically via script; don't modify them here.
-const int kEngineBuildNumber = 21171;
-const char* kEngineVersion = "1.7.23";
+const int kEngineBuildNumber = 21183;
+const char* kEngineVersion = "1.7.24";
+
+#if BA_MONOLITHIC_BUILD
auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// This code is meant to be run standalone so won't inherit any
@@ -171,6 +173,8 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
return -1; // Didn't even get core; something clearly wrong.
}
+#endif // BA_MONOLITHIC_BUILD
+
void FatalError(const std::string& message) {
// Let the user and/or master-server know we're dying.
FatalError::ReportFatalError(message, false);
diff --git a/src/ballistica/shared/ballistica.h b/src/ballistica/shared/ballistica.h
index 9fe8712c..0d7114ea 100644
--- a/src/ballistica/shared/ballistica.h
+++ b/src/ballistica/shared/ballistica.h
@@ -104,8 +104,10 @@ class CoreConfig;
// enough that avoiding the extra class includes seems like an overall
// compile-time/convenience win.
+#if BA_MONOLITHIC_BUILD
/// Entry point for standard monolithic builds. Handles all initing and running.
auto MonolithicMain(const core::CoreConfig& config) -> int;
+#endif // BA_MONOLITHIC_BUILD
// Print a momentary message on the screen.
void ScreenMessage(const std::string& msg);
diff --git a/src/ballistica/shared/buildconfig/buildconfig_cmake.h b/src/ballistica/shared/buildconfig/buildconfig_cmake.h
index 8b3a2e0b..5d57c40a 100644
--- a/src/ballistica/shared/buildconfig/buildconfig_cmake.h
+++ b/src/ballistica/shared/buildconfig/buildconfig_cmake.h
@@ -64,7 +64,9 @@
// Allow stdin commands too.
#define BA_ENABLE_STDIO_CONSOLE 1
+#ifndef BA_DEFINE_MAIN
#define BA_DEFINE_MAIN 1
+#endif
#if !BA_DEBUG_BUILD
diff --git a/src/ballistica/shared/buildconfig/buildconfig_common.h b/src/ballistica/shared/buildconfig/buildconfig_common.h
index 35e175e0..c3691395 100644
--- a/src/ballistica/shared/buildconfig/buildconfig_common.h
+++ b/src/ballistica/shared/buildconfig/buildconfig_common.h
@@ -23,8 +23,10 @@ namespace ballistica {
// can override any of these before this is included.
// Monolithic builds consist of a single binary that inits and manages
-// Python itself, as opposed to modular builds which are made up of
-// Python binary modules which are run under a standard Python runtime.
+// Python itself, as opposed to modular builds which are made up of Python
+// binary modules which are run under a standard Python runtime. This will
+// be 0 for both modular (.so) builds of the engine as well as for static
+// libraries such as baplus intended to be linked to either version.
#ifndef BA_MONOLITHIC_BUILD
#define BA_MONOLITHIC_BUILD 1
#endif
diff --git a/src/ballistica/shared/foundation/event_loop.cc b/src/ballistica/shared/foundation/event_loop.cc
index 0d0e3c05..c5c6f9ef 100644
--- a/src/ballistica/shared/foundation/event_loop.cc
+++ b/src/ballistica/shared/foundation/event_loop.cc
@@ -7,6 +7,7 @@
#include "ballistica/core/support/base_soft.h"
#include "ballistica/shared/foundation/fatal_error.h"
#include "ballistica/shared/python/python.h"
+#include "ballistica/shared/python/python_sys.h"
namespace ballistica {
@@ -224,7 +225,7 @@ void EventLoop::WaitForNextEvent(bool single_cycle) {
// While we're waiting, allow other python threads to run.
if (acquires_python_gil_) {
- g_core->python->ReleaseGIL();
+ ReleaseGIL();
}
// If we've got active timers, wait for messages with a timeout so we can
@@ -257,7 +258,7 @@ void EventLoop::WaitForNextEvent(bool single_cycle) {
}
if (acquires_python_gil_) {
- g_core->python->AcquireGIL();
+ AcquireGIL();
}
}
@@ -451,7 +452,7 @@ void EventLoop::SetAcquiresPythonGIL() {
assert(!acquires_python_gil_); // This should be called exactly once.
assert(ThreadIsCurrent());
acquires_python_gil_ = true;
- g_core->python->AcquireGIL();
+ AcquireGIL();
}
// Explicitly kill the main thread.
@@ -784,4 +785,30 @@ auto EventLoop::CheckPushRunnableSafety() -> bool {
return thread_messages_.size() < kThreadMessageSafetyThreshold;
}
+void EventLoop::AcquireGIL() {
+ assert(g_base_soft && g_base_soft->InLogicThread());
+ auto debug_timing{g_core->core_config().debug_timing};
+ millisecs_t startms{debug_timing ? core::CorePlatform::GetCurrentMillisecs()
+ : 0};
+
+ if (py_thread_state_) {
+ PyEval_RestoreThread(py_thread_state_);
+ py_thread_state_ = nullptr;
+ }
+
+ if (debug_timing) {
+ auto duration{core::CorePlatform::GetCurrentMillisecs() - startms};
+ if (duration > (1000 / 120)) {
+ Log(LogLevel::kInfo,
+ "GIL acquire took too long (" + std::to_string(duration) + " ms).");
+ }
+ }
+}
+
+void EventLoop::ReleaseGIL() {
+ assert(g_base_soft && g_base_soft->InLogicThread());
+ assert(py_thread_state_ == nullptr);
+ py_thread_state_ = PyEval_SaveThread();
+}
+
} // namespace ballistica
diff --git a/src/ballistica/shared/foundation/event_loop.h b/src/ballistica/shared/foundation/event_loop.h
index ff93384b..0a30544d 100644
--- a/src/ballistica/shared/foundation/event_loop.h
+++ b/src/ballistica/shared/foundation/event_loop.h
@@ -164,6 +164,9 @@ class EventLoop {
void RunPauseCallbacks();
void RunResumeCallbacks();
+ void AcquireGIL();
+ void ReleaseGIL();
+
bool bootstrapped_{};
std::list> runnables_;
std::list pause_callbacks_;
@@ -174,6 +177,7 @@ class EventLoop {
std::condition_variable client_listener_cv_;
std::mutex client_listener_mutex_;
std::list> data_to_client_;
+ PyThreadState* py_thread_state_{};
TimerList timers_;
};
diff --git a/src/ballistica/shared/python/python.cc b/src/ballistica/shared/python/python.cc
index 414e2ccc..05226da1 100644
--- a/src/ballistica/shared/python/python.cc
+++ b/src/ballistica/shared/python/python.cc
@@ -377,21 +377,16 @@ void Python::MarkReachedEndOfModule(PyObject* module) {
class Python::ScopedInterpreterLock::Impl {
public:
Impl() {
- if (need_lock_) {
- // Grab the python GIL.
- gstate_ = PyGILState_Ensure();
- }
+ // Grab the python GIL.
+ gil_state_ = PyGILState_Ensure();
}
~Impl() {
- if (need_lock_) {
- // Release the python GIL.
- PyGILState_Release(gstate_);
- }
+ // Release the python GIL.
+ PyGILState_Release(gil_state_);
}
private:
- bool need_lock_{true};
- PyGILState_STATE gstate_{PyGILState_UNLOCKED};
+ PyGILState_STATE gil_state_{PyGILState_UNLOCKED};
};
Python::ScopedInterpreterLock::ScopedInterpreterLock()
@@ -401,6 +396,31 @@ Python::ScopedInterpreterLock::ScopedInterpreterLock()
Python::ScopedInterpreterLock::~ScopedInterpreterLock() { delete impl_; }
+class Python::ScopedInterpreterLockRelease::Impl {
+ public:
+ Impl() {
+ assert(HaveGIL());
+ // Release the GIL.
+ thread_state_ = PyEval_SaveThread();
+ }
+ ~Impl() {
+ // Restore the GIL.
+ PyEval_RestoreThread(thread_state_);
+ }
+
+ private:
+ PyThreadState* thread_state_{};
+};
+
+Python::ScopedInterpreterLockRelease::ScopedInterpreterLockRelease()
+ : impl_{new Python::ScopedInterpreterLockRelease::Impl()}
+// impl_{std::make_unique()}
+{}
+
+Python::ScopedInterpreterLockRelease::~ScopedInterpreterLockRelease() {
+ delete impl_;
+}
+
// (some stuff borrowed from python's source code - used in our overriding of
// objects' dir() results)
diff --git a/src/ballistica/shared/python/python.h b/src/ballistica/shared/python/python.h
index 40344e80..7b43a1d8 100644
--- a/src/ballistica/shared/python/python.h
+++ b/src/ballistica/shared/python/python.h
@@ -39,9 +39,9 @@ class Python {
BA_DISALLOW_CLASS_COPIES(ScopedCallLabel);
};
- /// Use this to protect Python code that may be run in cases where we don't
- /// hold the Global Interpreter Lock (GIL) (basically anything outside of the
- /// logic thread).
+ /// Use this to protect Python code that may be run in cases where we
+ /// don't hold the Global Interpreter Lock (GIL). (Basically anything
+ /// outside of the logic thread).
class ScopedInterpreterLock {
public:
ScopedInterpreterLock();
@@ -55,7 +55,20 @@ class Python {
Impl* impl_{};
};
- // static auto Create() -> Python*;
+ /// Use this for cases where we *do* hold the GIL but want to release
+ /// it for some operation.
+ class ScopedInterpreterLockRelease {
+ public:
+ ScopedInterpreterLockRelease();
+ ~ScopedInterpreterLockRelease();
+
+ private:
+ class Impl;
+ // Note: should use unique_ptr for this, but build fails on raspberry pi
+ // (gcc 8.3.0). Works on Ubuntu 9.3 so should try again later.
+ // std::unique_ptr impl_{};
+ Impl* impl_{};
+ };
/// Return whether the current thread holds the global-interpreter-lock.
/// We must always hold the GIL while running python code.
diff --git a/src/external/readlinktest.c b/src/external/readlinktest.c
new file mode 100644
index 00000000..1f65d6b3
--- /dev/null
+++ b/src/external/readlinktest.c
@@ -0,0 +1,6 @@
+#include
+
+int main(int argc, char** argv) {
+ printf("HELLO WORLD!\n");
+ return 0;
+}
diff --git a/tools/batools/assetstaging.py b/tools/batools/assetstaging.py
index a33e1e4b..490c01a6 100755
--- a/tools/batools/assetstaging.py
+++ b/tools/batools/assetstaging.py
@@ -16,26 +16,38 @@ from efrotools import PYVER
if TYPE_CHECKING:
pass
-# Suffix for the pyc files we include in stagings.
-# We're using deterministic opt pyc files; see PEP 552.
-# Note: this means anyone wanting to modify .py files in a build
-# will need to wipe out the existing .pyc files first or the changes
-# will be ignored.
+# Suffix for the pyc files we include in stagings. We're using
+# deterministic opt pyc files; see PEP 552.
+#
+# Note: this means anyone
+# wanting to modify .py files in a build will need to wipe out the
+# existing .pyc files first or the changes will be ignored.
OPT_PYC_SUFFIX = 'cpython-' + PYVER.replace('.', '') + '.opt-1.pyc'
-class Config:
- """Encapsulates command options."""
+def stage_assets(projroot: str, args: list[str] | None = None) -> None:
+ """Stage assets for a build."""
+
+ if args is None:
+ args = sys.argv
+
+ AssetStager(projroot).run(args)
+
+
+class AssetStager:
+ """Context for a run of the tool."""
def __init__(self, projroot: str) -> None:
self.projroot = projroot
# We always calc src relative to this script.
- self.src = self.projroot + '/build/assets'
+ self.src = f'{self.projroot}/build/assets'
self.dst: str | None = None
self.serverdst: str | None = None
self.win_extras_src: str | None = None
self.win_platform: str | None = None
self.win_type: str | None = None
+ self.include_python_dylib = False
+ self.include_shell_launcher = False
self.include_audio = True
self.include_meshes = True
self.include_collision_meshes = True
@@ -51,11 +63,105 @@ class Config:
self.is_payload_full = False
self.debug: bool | None = None
+ def run(self, args: list[str]) -> None:
+ """Do the thing."""
+ self._parse_args(args)
+
+ # Ok, now for every top level dir in src, come up with a nice single
+ # command to sync the needed subset of it to dst.
+
+ # We can now use simple speedy timestamp based updates since we no
+ # longer have to try to preserve timestamps to get .pyc files to
+ # behave (hooray!)
+
+ # Do our stripped down pylib dir for platforms that use that.
+ if self.include_pylib:
+ self._sync_pylib()
+ else:
+ if self.dst is not None and os.path.isdir(f'{self.dst}/pylib'):
+ subprocess.run(['rm', '-rf', f'{self.dst}/pylib'], check=True)
+
+ # Sync our server files if we're doing that.
+ if self.serverdst is not None:
+ self._sync_server_files()
+
+ # On windows we need to pull in some dlls and this and that (we also
+ # include a non-stripped-down set of Python libs).
+ if self.win_extras_src is not None:
+ self._sync_windows_extras()
+
+ # Standard stuff in ba_data.
+ self._sync_ba_data()
+
+ # On Android we need to build a payload file so it knows what to
+ # pull out of the apk.
+ if self.include_payload_file:
+ assert self.dst is not None
+ _write_payload_file(self.dst, self.is_payload_full)
+
+ def _parse_args(self, args: list[str]) -> None:
+ """Parse args and apply to ourself."""
+ if len(args) < 1:
+ raise RuntimeError('Expected at least one argument.')
+ platform_arg = args[0]
+
+ # Require either -debug or -release in args.
+ if '-debug' in args:
+ self.debug = True
+ assert '-release' not in args
+ elif '-release' in args:
+ self.debug = False
+ else:
+ raise RuntimeError(
+ "Expected either '-debug' or '-release' in args."
+ )
+
+ if platform_arg == '-android':
+ self._parse_android_args(args)
+ elif platform_arg.startswith('-win'):
+ self._parse_win_args(platform_arg, args)
+ elif platform_arg == '-cmake':
+ self.dst = args[-1]
+ self.tex_suffix = '.dds'
+ elif platform_arg == '-cmakemodular':
+ self.dst = args[-1]
+ self.tex_suffix = '.dds'
+ self.include_python_dylib = True
+ self.include_shell_launcher = True
+ elif platform_arg == '-cmakeserver':
+ self.dst = os.path.join(args[-1], 'dist')
+ self.serverdst = args[-1]
+ self.include_textures = False
+ self.include_audio = False
+ self.include_meshes = False
+
+ elif platform_arg == '-xcode-mac':
+ self.src = os.environ['SOURCE_ROOT'] + '/build/assets'
+ self.dst = (
+ os.environ['TARGET_BUILD_DIR']
+ + '/'
+ + os.environ['UNLOCALIZED_RESOURCES_FOLDER_PATH']
+ )
+ self.include_pylib = True
+ self.pylib_src_name = 'pylib-apple'
+ self.tex_suffix = '.dds'
+ elif platform_arg == '-xcode-ios':
+ self.src = os.environ['SOURCE_ROOT'] + '/build/assets'
+ self.dst = (
+ os.environ['TARGET_BUILD_DIR']
+ + '/'
+ + os.environ['UNLOCALIZED_RESOURCES_FOLDER_PATH']
+ )
+ self.include_pylib = True
+ self.pylib_src_name = 'pylib-apple'
+ self.tex_suffix = '.pvr'
+ else:
+ raise RuntimeError('No valid platform arg provided.')
+
def _parse_android_args(self, args: list[str]) -> None:
- # On Android we get nitpicky with what
- # we want to copy in since we can speed up
- # iterations by installing stripped down
- # apks.
+ # On Android we get nitpicky with exactly what we want to copy
+ # in since we can speed up iterations by installing stripped
+ # down apks.
self.dst = 'assets/ballistica_files'
self.pylib_src_name = 'pylib-android'
self.include_payload_file = True
@@ -98,9 +204,9 @@ class Config:
elif arg == '-audio':
self.include_audio = True
- def _parse_win_platform(self, platform: str, args: list[str]) -> None:
+ def _parse_win_args(self, platform: str, args: list[str]) -> None:
"""Parse sub-args in the windows platform string."""
- winempty, wintype, winplt, wincfg = platform.split('-')
+ winempty, wintype, winplt = platform.split('-')
self.win_platform = winplt
self.win_type = wintype
assert winempty == ''
@@ -115,78 +221,375 @@ class Config:
self.include_audio = False
self.include_meshes = False
else:
- raise RuntimeError(f'Invalid wintype: "{wintype}"')
+ raise RuntimeError(f"Invalid wintype: '{wintype}'.")
if winplt == 'Win32':
- self.win_extras_src = self.projroot + '/build/assets/windows/Win32'
+ self.win_extras_src = f'{self.projroot}/build/assets/windows/Win32'
elif winplt == 'x64':
- self.win_extras_src = self.projroot + '/build/assets/windows/x64'
+ self.win_extras_src = f'{self.projroot}/build/assets/windows/x64'
else:
- raise RuntimeError(f'Invalid winplt: "{winplt}"')
+ raise RuntimeError(f"Invalid winplt: '{winplt}'.")
- if wincfg == 'Debug':
- self.debug = True
- elif wincfg == 'Release':
- self.debug = False
+ def _sync_windows_extras(self) -> None:
+ # pylint: disable=too-many-branches
+ assert self.win_extras_src is not None
+ assert self.win_platform is not None
+ assert self.win_type is not None
+ if not os.path.isdir(self.win_extras_src):
+ raise RuntimeError(
+ f"Win extras src dir not found: '{self.win_extras_src}'."
+ )
+
+ # Ok, lets do full syncs on each subdir we find so we properly
+ # delete anything in dst that disappeared from src. Lastly we'll
+ # sync over the remaining top level files. Note: technically it'll
+ # be possible to leave orphaned top level files in dst, so when
+ # building packages/etc. we should always start from scratch.
+ assert self.dst is not None
+ assert self.debug is not None
+ pyd_rules: list[str]
+ if self.debug:
+ pyd_rules = ['--include', '*_d.pyd']
else:
- raise RuntimeError(f'Invalid wincfg: "{wincfg}"')
+ pyd_rules = ['--exclude', '*_d.pyd', '--include', '*.pyd']
- def parse_args(self, args: list[str]) -> None:
- """Parse args and apply to the cfg."""
- if len(args) < 1:
- raise RuntimeError('Expected a platform argument.')
- platform = args[0]
- if platform == '-android':
- self._parse_android_args(args)
- elif platform.startswith('-win'):
- self._parse_win_platform(platform, args)
- elif platform == '-cmake':
- self.dst = args[1]
- self.tex_suffix = '.dds'
- elif '-cmakeserver' in args:
- self.dst = os.path.join(args[-1], 'dist')
- self.serverdst = args[-1]
- self.include_textures = False
- self.include_audio = False
- self.include_meshes = False
+ for dirname in ('DLLs', 'Lib'):
+ # EWW: seems Windows Python currently sets its path to ./lib but
+ # it comes with Lib. Windows is normally case-insensitive but
+ # this messes it up when running under WSL. Let's install it as
+ # lib for now.
+ dstdirname = 'lib' if dirname == 'Lib' else dirname
+ os.makedirs(f'{self.dst}/{dstdirname}', exist_ok=True)
+ cmd: list[str] = (
+ [
+ 'rsync',
+ '--recursive',
+ '--times',
+ '--delete',
+ '--delete-excluded',
+ '--prune-empty-dirs',
+ '--include',
+ '*.ico',
+ '--include',
+ '*.cat',
+ '--include',
+ '*.dll',
+ ]
+ + pyd_rules
+ + [
+ '--include',
+ '*.py',
+ '--include',
+ f'*.{OPT_PYC_SUFFIX}',
+ '--include',
+ '*/',
+ '--exclude',
+ '*',
+ f'{os.path.join(self.win_extras_src, dirname)}/',
+ f'{self.dst}/{dstdirname}/',
+ ]
+ )
+ subprocess.run(cmd, check=True)
- # Require either -debug or -release in args.
- # FIXME: should require this for all platforms for consistency.
- if '-debug' in args:
- self.debug = True
- assert '-release' not in args
- elif '-release' in args:
- self.debug = False
+ # Now sync the top level individual files that we want. We could
+ # technically copy everything over but this keeps staging dirs a bit
+ # tidier.
+ dbgsfx = '_d' if self.debug else ''
+
+ # Note: Needs updating when Python version changes (currently 3.11).
+ toplevelfiles: list[str] = [f'python311{dbgsfx}.dll']
+
+ if self.win_type == 'win':
+ toplevelfiles += [
+ 'libvorbis.dll',
+ 'libvorbisfile.dll',
+ 'ogg.dll',
+ 'OpenAL32.dll',
+ 'SDL2.dll',
+ ]
+ elif self.win_type == 'winserver':
+ toplevelfiles += [f'python{dbgsfx}.exe']
+
+ # Include debug dlls so folks without msvc can run them.
+ if self.debug:
+ if self.win_platform == 'x64':
+ toplevelfiles += [
+ 'msvcp140d.dll',
+ 'vcruntime140d.dll',
+ 'vcruntime140_1d.dll',
+ 'ucrtbased.dll',
+ ]
else:
- raise RuntimeError(
- "Expected either '-debug' or '-release' in args."
- )
- elif '-xcode-mac' in args:
- self.src = os.environ['SOURCE_ROOT'] + '/build/assets'
- self.dst = (
- os.environ['TARGET_BUILD_DIR']
- + '/'
- + os.environ['UNLOCALIZED_RESOURCES_FOLDER_PATH']
- )
- self.include_pylib = True
- self.pylib_src_name = 'pylib-apple'
- self.tex_suffix = '.dds'
- elif '-xcode-ios' in args:
- self.src = os.environ['SOURCE_ROOT'] + '/build/assets'
- self.dst = (
- os.environ['TARGET_BUILD_DIR']
- + '/'
- + os.environ['UNLOCALIZED_RESOURCES_FOLDER_PATH']
- )
- self.include_pylib = True
- self.pylib_src_name = 'pylib-apple'
- self.tex_suffix = '.pvr'
+ toplevelfiles += [
+ 'msvcp140d.dll',
+ 'vcruntime140d.dll',
+ 'ucrtbased.dll',
+ ]
+
+ # Include the runtime redistributables in release builds.
+ if not self.debug:
+ if self.win_platform == 'x64':
+ toplevelfiles.append('vc_redist.x64.exe')
+ elif self.win_platform == 'Win32':
+ toplevelfiles.append('vc_redist.x86.exe')
+ else:
+ raise RuntimeError(f'Invalid win_platform {self.win_platform}')
+
+ cmd2 = (
+ ['rsync', '--times']
+ + [os.path.join(self.win_extras_src, f) for f in toplevelfiles]
+ + [f'{self.dst}/']
+ )
+ subprocess.run(cmd2, check=True)
+
+ # If we're running under WSL we won't be able to launch these .exe
+ # files unless they're marked executable, so do that here. Update:
+ # gonna try simply setting this flag on the source side.
+ # _run(f'chmod +x {self.dst}/*.exe')
+
+ def _sync_pylib(self) -> None:
+ assert self.pylib_src_name is not None
+ assert self.dst is not None
+ os.makedirs(f'{self.dst}/pylib', exist_ok=True)
+ cmd: list[str] = [
+ 'rsync',
+ '--recursive',
+ '--times',
+ '--delete',
+ '--delete-excluded',
+ '--prune-empty-dirs',
+ '--include',
+ '*.py',
+ '--include',
+ f'*.{OPT_PYC_SUFFIX}',
+ '--include',
+ '*/',
+ '--exclude',
+ '*',
+ f'{self.src}/{self.pylib_src_name}/',
+ f'{self.dst}/pylib/',
+ ]
+ subprocess.run(cmd, check=True)
+
+ def _sync_ba_data(self) -> None:
+ assert self.dst is not None
+ os.makedirs(f'{self.dst}/ba_data', exist_ok=True)
+ cmd: list[str] = [
+ 'rsync',
+ '--recursive',
+ '--times',
+ '--delete',
+ '--prune-empty-dirs',
+ ]
+
+ # Normally we use --delete-excluded so that we can do sparse
+ # syncs for quick iteration on android apks/etc. However for our
+ # modular builds we need to avoid that flag because we do a
+ # second pass after to sync in our python-dylib stuff and with
+ # that flag it all gets blown on the first pass.
+ if not self.include_python_dylib:
+ cmd.append('--delete-excluded')
else:
- raise RuntimeError('No valid platform arg provided.')
+ # Shouldn't be trying to do sparse stuff.
+ assert (
+ self.include_textures
+ and self.include_audio
+ and self.include_fonts
+ and self.include_json
+ and self.include_meshes
+ and self.include_collision_meshes
+ )
+ # Keep rsync from trying to prune this as an 'empty' dir.
+ cmd += ['--exclude', '/python-dylib']
+
+ if self.include_scripts:
+ cmd += [
+ '--include',
+ '*.py',
+ '--include',
+ '*.pem',
+ '--include',
+ f'*.{OPT_PYC_SUFFIX}',
+ ]
+
+ if self.include_textures:
+ assert self.tex_suffix is not None
+ cmd += ['--include', f'*{self.tex_suffix}']
+
+ if self.include_audio:
+ cmd += ['--include', '*.ogg']
+
+ if self.include_fonts:
+ cmd += ['--include', '*.fdata']
+
+ if self.include_json:
+ cmd += ['--include', '*.json']
+
+ if self.include_meshes:
+ cmd += ['--include', '*.bob']
+
+ if self.include_collision_meshes:
+ cmd += ['--include', '*.cob']
+
+ # By default we want to include all dirs and exclude all files.
+ cmd += [
+ '--include',
+ '*/',
+ '--exclude',
+ '*',
+ f'{self.src}/ba_data/',
+ f'{self.dst}/ba_data/',
+ ]
+ subprocess.run(cmd, check=True)
+
+ if self.include_python_dylib:
+ self._sync_python_dylib()
+
+ if self.include_shell_launcher:
+ self._sync_shell_launcher()
+
+ def _sync_shell_launcher(self) -> None:
+ path = f'{self.dst}/ballisticakit'
+
+ # For now this is so simple we just do an ad-hoc write each time;
+ # not worth setting up files and syncs.
+ if self.debug:
+ optstuff = 'export PYTHONDEVMODE=1\nexport PYTHONOPTIMIZE=0\n'
+ else:
+ optstuff = 'export PYTHONDEVMODE=0\nexport PYTHONOPTIMIZE=1\n'
+
+ optnm = 'DEBUG' if self.debug else 'RELEASE'
+ with open(path, 'w', encoding='utf-8') as outfile:
+ outfile.write(
+ '#!/bin/sh\n'
+ '\n'
+ '# We should error if anything here errors.\n'
+ 'set -e\n'
+ '\n'
+ '# We want Python to use UTF-8 everywhere for consistency.\n'
+ '# (This will be the default in the future; see PEP 686).\n'
+ f'export PYTHONUTF8=1\n'
+ '\n'
+ f'# This is a Ballistica {optnm} build; set Python to match.\n'
+ f'{optstuff}'
+ '\n'
+ '# Run the app, forwarding along all arguments.\n'
+ '# Basically this does:\n'
+ '# import baenv; baenv.configure();'
+ ' import babase; babase.app.run().\n'
+ 'python3.11 ba_data/python/baenv.py $@\n'
+ )
+ subprocess.run(['chmod', '+x', path], check=True)
+
+ def _sync_python_dylib(self) -> None:
+ # pylint: disable=too-many-locals
+ from batools.featureset import FeatureSet
+
+ # Note: we're technically not *syncing* quite so much as
+ # *constructing* here.
+
+ dylib_staging_dir = f'{self.dst}/ba_data/python-dylib'
+
+ # Name of our single shared library containing all our stuff.
+ soname = 'ballisticakit.so'
+
+ # All featuresets in the project with binary modules.
+ bmodfeaturesets = {
+ f.name: f
+ for f in FeatureSet.get_all_for_project(self.projroot)
+ if f.has_python_binary_module
+ }
+
+ # Map of featureset names (foo) to module filenames (_foo.so).
+ fsetmfilenames = {
+ f.name: f'{f.name_python_binary_module}.so'
+ for f in bmodfeaturesets.values()
+ }
+
+ # Set of all module filenames (_foo.so, etc.) we should have.
+ fsetmfilenamevals = set(fsetmfilenames.values())
+
+ if not os.path.exists(dylib_staging_dir):
+ os.makedirs(dylib_staging_dir, exist_ok=True)
+
+ # Create a symlink to our original built so. NOTE: Anyone
+ # building final app packages/etc. should replace this with the
+ # actual file. This is just for development.
+
+ # FIXME - currently assuming our built .so lives one dir above
+ # our staging dir; should not be making that assumption.
+ built_so_path = f'{self.dst}/../{soname}'
+ staged_so_path = f'{dylib_staging_dir}/{soname}'
+
+ if not os.path.islink(staged_so_path):
+ relpath = os.path.relpath(built_so_path, dylib_staging_dir)
+ subprocess.run(['ln', '-sf', relpath, staged_so_path], check=True)
+
+ # Ok, now we want to create symlinks for each of our featureset
+ # Python modules. All of our stuff lives in the same .so and we
+ # can use symlinks to help Python find them all there. See the
+ # following:
+ # https://peps.python.org/pep-0489/#multiple-modules-in-one-library
+ for fsetname, featureset in bmodfeaturesets.items():
+ if featureset.has_python_binary_module:
+ mfilename = fsetmfilenames[fsetname]
+ instpath = f'{dylib_staging_dir}/{mfilename}'
+ if not os.path.islink(instpath):
+ subprocess.run(['ln', '-sf', soname, instpath], check=True)
+
+ # Lastly, blow away anything in that dir that's not something we
+ # just made (clears out featuresets that get renamed or
+ # disabled, etc).
+ fnames = os.listdir(dylib_staging_dir)
+ for fname in fnames:
+ if not fname in fsetmfilenamevals and fname != soname:
+ fpath = f'{dylib_staging_dir}/{fname}'
+ print(f"Pruning orphaned dylib path: '{fpath}'.")
+ subprocess.run(['rm', '-rf', fpath], check=True)
+
+ def _sync_server_files(self) -> None:
+ assert self.serverdst is not None
+ assert self.debug is not None
+ modeval = 'debug' if self.debug else 'release'
+
+ # NOTE: staging these directly from src; not build.
+ _stage_server_file(
+ projroot=self.projroot,
+ mode=modeval,
+ infilename=f'{self.projroot}/src/assets/server_package/'
+ 'ballisticakit_server.py',
+ outfilename=os.path.join(
+ self.serverdst,
+ 'ballisticakit_server.py'
+ if self.win_type is not None
+ else 'ballisticakit_server',
+ ),
+ )
+ _stage_server_file(
+ projroot=self.projroot,
+ mode=modeval,
+ infilename=f'{self.projroot}/src/assets/server_package/README.txt',
+ outfilename=os.path.join(self.serverdst, 'README.txt'),
+ )
+ _stage_server_file(
+ projroot=self.projroot,
+ mode=modeval,
+ infilename=f'{self.projroot}/src/assets/server_package/'
+ 'config_template.yaml',
+ outfilename=os.path.join(self.serverdst, 'config_template.yaml'),
+ )
+ if self.win_type is not None:
+ fname = 'launch_ballisticakit_server.bat'
+ _stage_server_file(
+ projroot=self.projroot,
+ mode=modeval,
+ infilename=f'{self.projroot}/src/assets/server_package/{fname}',
+ outfilename=os.path.join(self.serverdst, fname),
+ )
-def md5sum(filename: str) -> str:
- """Generate an md5sum given a filename."""
+def _filehash(filename: str) -> str:
+ """Generate a hash for a file."""
md5 = hashlib.md5()
with open(filename, mode='rb') as infile:
for buf in iter(partial(infile.read, 1024), b''):
@@ -194,18 +597,9 @@ def md5sum(filename: str) -> str:
return md5.hexdigest()
-def _run(cmd: str, echo: bool = False) -> None:
- """Run an os command; raise Exception on non-zero return value."""
- if echo:
- print(cmd)
- result = os.system(cmd)
- if result != 0:
- raise RuntimeError(f"Error running cmd: '{cmd}'.")
-
-
def _write_payload_file(assets_root: str, full: bool) -> None:
if not assets_root.endswith('/'):
- assets_root = assets_root + '/'
+ assets_root = f'{assets_root}/'
# Now construct a payload file if we have any files.
file_list = []
@@ -222,226 +616,24 @@ def _write_payload_file(assets_root: str, full: bool) -> None:
raise RuntimeError(
f"Invalid filename (contains spaces): '{fpathshort}'"
)
- payload_str += fpathshort + ' ' + md5sum(fpath) + '\n'
+ payload_str += f'{fpathshort} {_filehash(fpath)}\n'
file_list.append(fpathshort)
- payload_path = assets_root + '/payload_info'
+ payload_path = f'{assets_root}/payload_info'
if file_list:
- # Write the file count, whether this is a 'full' payload, and finally
- # the file list.
- payload_str = (
- str(len(file_list))
- + '\n'
- + ('1' if full else '0')
- + '\n'
- + payload_str
- )
+ # Write the file count, whether this is a 'full' payload, and
+ # finally the file list.
+ fullstr = '1' if full else '0'
+ payload_str = f'{len(file_list)}\n{fullstr}\n{payload_str}'
with open(payload_path, 'w', encoding='utf-8') as outfile:
outfile.write(payload_str)
else:
- # Remove the payload file; this will cause the game to completely
- # skip the payload processing step.
+ # Remove the payload file; this will cause the game to
+ # completely skip the payload processing step.
if os.path.exists(payload_path):
os.unlink(payload_path)
-def _sync_windows_extras(cfg: Config) -> None:
- # pylint: disable=too-many-branches
- assert cfg.win_extras_src is not None
- assert cfg.win_platform is not None
- assert cfg.win_type is not None
- if not os.path.isdir(cfg.win_extras_src):
- raise RuntimeError(
- "Win extras src dir not found: '{cfg.win_extras_src}'."
- )
-
- # Ok, lets do full syncs on each subdir we find so we
- # properly delete anything in dst that disappeared from src.
- # Lastly we'll sync over the remaining top level files.
- # Note: technically it'll be possible to leave orphaned top level
- # files in dst, so when building packages/etc. we should always start
- # from scratch.
- assert cfg.dst is not None
- assert cfg.debug is not None
- if cfg.debug:
- pyd_rules = "--include '*_d.pyd'"
- else:
- pyd_rules = "--exclude '*_d.pyd' --include '*.pyd'"
-
- for dirname in ('DLLs', 'Lib'):
- # EWW: seems windows python currently sets its path to ./lib but it
- # comes with Lib. Windows is normally case-insensitive but this messes
- # it up when running under WSL. Let's install it as lib for now.
- dstdirname = 'lib' if dirname == 'Lib' else dirname
- _run(f'mkdir -p "{cfg.dst}/{dstdirname}"')
- cmd = (
- 'rsync --recursive --update --delete --delete-excluded '
- ' --prune-empty-dirs'
- " --include '*.ico' --include '*.cat'"
- f" --include '*.dll' {pyd_rules}"
- " --include '*.py' --include '*." + OPT_PYC_SUFFIX + "'"
- " --include '*/' --exclude '*' \""
- + os.path.join(cfg.win_extras_src, dirname)
- + '/" '
- '"' + cfg.dst + '/' + dstdirname + '/"'
- )
- _run(cmd)
-
- # Now sync the top level individual files that we want.
- # We could technically copy everything over but this keeps staging
- # dirs a bit tidier.
- dbgsfx = '_d' if cfg.debug else ''
- # Note: Below needs updating when Python version changes (currently 3.11)
- toplevelfiles: list[str] = [f'python311{dbgsfx}.dll']
-
- if cfg.win_type == 'win':
- toplevelfiles += [
- 'libvorbis.dll',
- 'libvorbisfile.dll',
- 'ogg.dll',
- 'OpenAL32.dll',
- 'SDL2.dll',
- ]
- elif cfg.win_type == 'winserver':
- toplevelfiles += [f'python{dbgsfx}.exe']
-
- # Include debug dlls so folks without msvc can run them.
- if cfg.debug:
- if cfg.win_platform == 'x64':
- toplevelfiles += [
- 'msvcp140d.dll',
- 'vcruntime140d.dll',
- 'vcruntime140_1d.dll',
- 'ucrtbased.dll',
- ]
- else:
- toplevelfiles += [
- 'msvcp140d.dll',
- 'vcruntime140d.dll',
- 'ucrtbased.dll',
- ]
-
- # Include the runtime redistributables in release builds.
- if not cfg.debug:
- if cfg.win_platform == 'x64':
- toplevelfiles.append('vc_redist.x64.exe')
- elif cfg.win_platform == 'Win32':
- toplevelfiles.append('vc_redist.x86.exe')
- else:
- raise RuntimeError(f'Invalid win_platform {cfg.win_platform}')
-
- cmd2 = (
- ['rsync', '--update']
- + [os.path.join(cfg.win_extras_src, f) for f in toplevelfiles]
- + [cfg.dst + '/']
- )
- subprocess.run(cmd2, check=True)
-
- # If we're running under WSL we won't be able to launch these .exe files
- # unless they're marked executable, so do that here.
- # Update: gonna try simply setting this flag on the source side.
- # _run(f'chmod +x {cfg.dst}/*.exe')
-
-
-def _sync_pylib(cfg: Config) -> None:
- assert cfg.pylib_src_name is not None
- assert cfg.dst is not None
- _run(f'mkdir -p "{cfg.dst}/pylib"')
- cmd = (
- f'rsync --recursive --update --delete --delete-excluded '
- f' --prune-empty-dirs'
- f" --include '*.py' --include '*.{OPT_PYC_SUFFIX}'"
- f" --include '*/' --exclude '*'"
- f' "{cfg.src}/{cfg.pylib_src_name}/" '
- f'"{cfg.dst}/pylib/"'
- )
- _run(cmd)
-
-
-def _sync_standard_game_data(cfg: Config) -> None:
- assert cfg.dst is not None
- _run('mkdir -p "' + cfg.dst + '/ba_data"')
- cmd = (
- 'rsync --recursive --update --delete --delete-excluded'
- ' --prune-empty-dirs'
- )
-
- if cfg.include_scripts:
- cmd += (
- f" --include '*.py' --include '*.pem'"
- f" --include '*.{OPT_PYC_SUFFIX}'"
- )
-
- if cfg.include_textures:
- assert cfg.tex_suffix is not None
- cmd += " --include '*" + cfg.tex_suffix + "'"
-
- if cfg.include_audio:
- cmd += " --include '*.ogg'"
-
- if cfg.include_fonts:
- cmd += " --include '*.fdata'"
-
- if cfg.include_json:
- cmd += " --include '*.json'"
-
- if cfg.include_meshes:
- cmd += " --include '*.bob'"
-
- if cfg.include_collision_meshes:
- cmd += " --include '*.cob'"
-
- cmd += (
- " --include='*/' --exclude='*' \""
- + cfg.src
- + '/ba_data/" "'
- + cfg.dst
- + '/ba_data/"'
- )
- _run(cmd)
-
-
-def _sync_server_files(cfg: Config) -> None:
- assert cfg.serverdst is not None
- assert cfg.debug is not None
- modeval = 'debug' if cfg.debug else 'release'
-
- # NOTE: staging these directly from src; not build.
- stage_server_file(
- projroot=cfg.projroot,
- mode=modeval,
- infilename=f'{cfg.projroot}/src/assets/server_package/'
- 'ballisticakit_server.py',
- outfilename=os.path.join(
- cfg.serverdst,
- 'ballisticakit_server.py'
- if cfg.win_type is not None
- else 'ballisticakit_server',
- ),
- )
- stage_server_file(
- projroot=cfg.projroot,
- mode=modeval,
- infilename=f'{cfg.projroot}/src/assets/server_package/README.txt',
- outfilename=os.path.join(cfg.serverdst, 'README.txt'),
- )
- stage_server_file(
- projroot=cfg.projroot,
- mode=modeval,
- infilename=f'{cfg.projroot}/src/assets/server_package/'
- 'config_template.yaml',
- outfilename=os.path.join(cfg.serverdst, 'config_template.yaml'),
- )
- if cfg.win_type is not None:
- fname = 'launch_ballisticakit_server.bat'
- stage_server_file(
- projroot=cfg.projroot,
- mode=modeval,
- infilename=f'{cfg.projroot}/src/assets/server_package/{fname}',
- outfilename=os.path.join(cfg.serverdst, fname),
- )
-
-
def _write_if_changed(
path: str, contents: str, make_executable: bool = False
) -> None:
@@ -459,7 +651,7 @@ def _write_if_changed(
subprocess.run(['chmod', '+x', path], check=True)
-def stage_server_file(
+def _stage_server_file(
projroot: str, mode: str, infilename: str, outfilename: str
) -> None:
"""Stage files for the server environment with some filtering."""
@@ -528,45 +720,3 @@ def stage_server_file(
_write_if_changed(outfilename, '\n'.join(lines) + '\n')
else:
raise RuntimeError(f"Unknown server file for staging: '{basename}'.")
-
-
-def main(projroot: str, args: list[str] | None = None) -> None:
- """Stage assets for a build."""
-
- if args is None:
- args = sys.argv
-
- cfg = Config(projroot)
- cfg.parse_args(args)
-
- # Ok, now for every top level dir in src, come up with a nice single
- # command to sync the needed subset of it to dst.
-
- # We can now use simple speedy timestamp based updates since
- # we no longer have to try to preserve timestamps to get .pyc files
- # to behave (hooray!)
-
- # Do our stripped down pylib dir for platforms that use that.
- if cfg.include_pylib:
- _sync_pylib(cfg)
- else:
- if cfg.dst is not None and os.path.isdir(cfg.dst + '/pylib'):
- subprocess.run(['rm', '-rf', cfg.dst + '/pylib'], check=True)
-
- # Sync our server files if we're doing that.
- if cfg.serverdst is not None:
- _sync_server_files(cfg)
-
- # On windows we need to pull in some dlls and this and that
- # (we also include a non-stripped-down set of python libs).
- if cfg.win_extras_src is not None:
- _sync_windows_extras(cfg)
-
- # Standard stuff in ba_data
- _sync_standard_game_data(cfg)
-
- # On Android we need to build a payload file so it knows
- # what to pull out of the apk.
- if cfg.include_payload_file:
- assert cfg.dst is not None
- _write_payload_file(cfg.dst, cfg.is_payload_full)
diff --git a/tools/batools/dummymodule.py b/tools/batools/dummymodule.py
index 87d24b69..52705cd5 100755
--- a/tools/batools/dummymodule.py
+++ b/tools/batools/dummymodule.py
@@ -937,7 +937,7 @@ def generate_dummy_modules(projroot: str) -> None:
# Generate a dummy module for each featureset that has a binary module.
featuresets = FeatureSet.get_all_for_project(project_root=projroot)
- featuresets = [f for f in featuresets if f.has_native_python_module]
+ featuresets = [f for f in featuresets if f.has_python_binary_module]
mnames: list[str] = [fs.name_python_binary_module for fs in featuresets]
builddir = 'build/dummymodules'
diff --git a/tools/batools/featureset.py b/tools/batools/featureset.py
index 42351f5f..fd5dc394 100644
--- a/tools/batools/featureset.py
+++ b/tools/batools/featureset.py
@@ -54,7 +54,7 @@ class FeatureSet:
# its C++ code. The build process will try to create dummy
# modules for all native modules, so to avoid errors you must
# tell it if you don't have one.
- self.has_native_python_module = True
+ self.has_python_binary_module = True
# If True, for feature-set 'foo_bar', the build system will
# define a 'babase.app.foo_bar' attr which points to a lazy
diff --git a/tools/batools/pcommand.py b/tools/batools/pcommand.py
index e84651d5..46475442 100644
--- a/tools/batools/pcommand.py
+++ b/tools/batools/pcommand.py
@@ -806,11 +806,13 @@ def efro_gradle() -> None:
def stage_assets() -> None:
"""Stage assets for a build."""
- from batools.assetstaging import main
+ import batools.assetstaging
from efro.error import CleanError
try:
- main(projroot=str(PROJROOT), args=sys.argv[2:])
+ batools.assetstaging.stage_assets(
+ projroot=str(PROJROOT), args=sys.argv[2:]
+ )
except CleanError as exc:
exc.pretty_print()
sys.exit(1)
diff --git a/tools/batools/pcommand2.py b/tools/batools/pcommand2.py
index 4abbf51f..f90756e5 100644
--- a/tools/batools/pcommand2.py
+++ b/tools/batools/pcommand2.py
@@ -25,16 +25,48 @@ def gen_monolithic_register_modules() -> None:
featuresets = FeatureSet.get_all_for_project(str(PROJROOT))
# Filter out ones without native modules.
- featuresets = [f for f in featuresets if f.has_native_python_module]
+ featuresets = [f for f in featuresets if f.has_python_binary_module]
pymodulenames = sorted(f.name_python_binary_module for f in featuresets)
+ def initname(mname: str) -> str:
+ # plus is a special case since we need to define that symbol
+ # ourself.
+ return f'DoPyInit_{mname}' if mname == '_baplus' else f'PyInit_{mname}'
+
extern_def_code = '\n'.join(
- f'auto PyInit_{n}() -> PyObject*;' for n in pymodulenames
+ f'auto {initname(n)}() -> PyObject*;' for n in pymodulenames
)
+
py_register_code = '\n'.join(
- f'PyImport_AppendInittab("{n}", &PyInit_{n});' for n in pymodulenames
+ f'PyImport_AppendInittab("{n}", &{initname(n)});' for n in pymodulenames
)
+
+ if '_baplus' in pymodulenames:
+ init_plus_code = (
+ '\n'
+ '// Slight hack: because we are currently building baplus as a'
+ ' static module\n'
+ '// and linking it in, symbols exported there (namely'
+ ' PyInit__baplus) do not\n'
+ '// seem to be available through us when we are compiled as'
+ ' a dynamic\n'
+ '// library. This leads to Python being unable to load baplus.'
+ ' While I\'m sure\n'
+ '// there is some way to get those symbols exported, I\'m worried'
+ ' it might be\n'
+ '// a messy platform-specific affair. So instead we\'re just'
+ ' defining that\n'
+ '// function here when baplus is present and forwarding it through'
+ ' to the\n'
+ '// static library version.\n'
+ 'extern "C" auto PyInit__baplus() -> PyObject* {\n'
+ ' return DoPyInit__baplus();\n'
+ '}\n'
+ )
+ else:
+ init_plus_code = ''
+
base_code = """
// Released under the MIT License. See LICENSE for details.
@@ -43,13 +75,12 @@ def gen_monolithic_register_modules() -> None:
// THIS CODE IS AUTOGENERATED BY META BUILD; DO NOT EDIT BY HAND.
+ #include "ballistica/shared/ballistica.h"
#include "ballistica/shared/python/python_sys.h"
- #if BA_MONOLITHIC_BUILD
extern "C" {
${EXTERN_DEF_CODE}
}
- #endif // BA_MONOLITHIC_BUILD
namespace ballistica {
@@ -58,15 +89,15 @@ def gen_monolithic_register_modules() -> None:
/// binary modules get located as .so files on disk as per regular
/// Python behavior.
void MonolithicRegisterPythonModules() {
- #if BA_MONOLITHIC_BUILD
+ if (g_buildconfig.monolithic_build()) {
${PY_REGISTER_CODE}
- #else
- FatalError(
- "MonolithicRegisterPythonModules should not be called"
- " in modular builds.");
- #endif // BA_MONOLITHIC_BUILD
+ } else {
+ FatalError(
+ "MonolithicRegisterPythonModules should not be called"
+ " in modular builds.");
+ }
}
-
+ ${PY_INIT_PLUS}
} // namespace ballistica
#endif // BALLISTICA_CORE_MGEN_PYTHON_MODULES_MONOLITHIC_H_
@@ -74,7 +105,10 @@ def gen_monolithic_register_modules() -> None:
out = (
textwrap.dedent(base_code)
.replace('${EXTERN_DEF_CODE}', extern_def_code)
- .replace('${PY_REGISTER_CODE}', textwrap.indent(py_register_code, ' '))
+ .replace(
+ '${PY_REGISTER_CODE}', textwrap.indent(py_register_code, ' ')
+ )
+ .replace('${PY_INIT_PLUS}', init_plus_code)
.strip()
+ '\n'
)
@@ -84,19 +118,6 @@ def gen_monolithic_register_modules() -> None:
outfile.write(out)
-def stage_server_file() -> None:
- """Stage files for the server environment with some filtering."""
- from efro.error import CleanError
- import batools.assetstaging
-
- if len(sys.argv) != 5:
- raise CleanError('Expected 3 args (mode, infile, outfile).')
- mode, infilename, outfilename = sys.argv[2], sys.argv[3], sys.argv[4]
- batools.assetstaging.stage_server_file(
- str(PROJROOT), mode, infilename, outfilename
- )
-
-
def py_examine() -> None:
"""Run a python examination at a given point in a given file."""
import os
@@ -244,7 +265,7 @@ def update_cmake_prefab_lib() -> None:
)
suffix = '_server' if buildtype == 'server' else '_gui'
target = (
- f'build/prefab/lib/{platform}{suffix}/{mode}/' f'libballistica_plus.a'
+ f'build/prefab/lib/{platform}{suffix}/{mode}/' f'libballisticaplus.a'
)
# Build the target and then copy it to dst if it doesn't exist there yet
@@ -252,7 +273,7 @@ def update_cmake_prefab_lib() -> None:
subprocess.run(['make', target], check=True)
libdir = os.path.join(builddir, 'prefablib')
- libpath = os.path.join(libdir, 'libballistica_plus.a')
+ libpath = os.path.join(libdir, 'libballisticaplus.a')
update = True
time1 = os.path.getmtime(target)
diff --git a/tools/batools/spinoff/_context.py b/tools/batools/spinoff/_context.py
index 2a5eee3c..f32f31b1 100644
--- a/tools/batools/spinoff/_context.py
+++ b/tools/batools/spinoff/_context.py
@@ -788,7 +788,7 @@ class SpinoffContext:
# Strip precompiled plus library out of the cmake file.
text = replace_exact(
text,
- '${CMAKE_CURRENT_BINARY_DIR}/prefablib/libballistica_plus.a'
+ '${CMAKE_CURRENT_BINARY_DIR}/prefablib/libballisticaplus.a'
' ode ',
'ode ',
label=src_path,
diff --git a/tools/efrotools/pybuild.py b/tools/efrotools/pybuild.py
index db0082a3..35301f2a 100644
--- a/tools/efrotools/pybuild.py
+++ b/tools/efrotools/pybuild.py
@@ -714,42 +714,77 @@ def _patch_py_wreadlink_test() -> None:
fname = 'Python/fileutils.c'
txt = readfile(fname)
+ # Final fix for this problem.
+ # It seems that readlink() might be broken in android at the moment,
+ # returning an int while claiming it to be a ssize_t value. This makes
+ # the error case (-1) actually come out as 4294967295. When cast back
+ # to an int it is -1, so that's what we do. This should be fine to do
+ # even on a fixed version.
txt = replace_exact(
txt,
' res = readlink(cpath, cbuf, cbuf_len);\n',
- (
- ' res = readlink(cpath, cbuf, cbuf_len);\n'
- ' const wchar_t *path2 = path;\n'
- ' int path2len = 0;\n'
- ' while (*path2) {\n'
- ' path2++;\n'
- ' path2len++;\n'
- ' }\n'
- ' char dlog1[512];\n'
- ' if (res >= 0) {\n'
- ' snprintf(dlog1, sizeof(dlog1), "ValsA1 pathlen=%d slen=%d'
- ' path=\'%s\'", path2len, strlen(cpath), cpath);\n'
- ' } else {\n'
- ' snprintf(dlog1, sizeof(dlog1), "ValsA2 pathlen=%d",'
- ' path2len);\n'
- ' }\n'
- ' Py_BallisticaLowLevelDebugLog(dlog1);\n'
- ),
+ ' res = (int)readlink(cpath, cbuf, cbuf_len);\n',
)
- txt = replace_exact(
- txt,
- " cbuf[res] = '\\0'; /* buf will be null terminated */",
- (
- ' char dlog[512];\n'
- ' snprintf(dlog, sizeof(dlog), "ValsB res=%d resx=%X'
- ' eq1=%d eq2=%d",'
- ' (int)res, res, (int)(res == -1),'
- ' (int)((size_t)res == cbuf_len));\n'
- ' Py_BallisticaLowLevelDebugLog(dlog);\n'
- " cbuf[res] = '\\0'; /* buf will be null terminated */"
- ),
- )
+ # Verbose problem exploration:
+ # txt = replace_exact(
+ # txt,
+ # '#include // mbstowcs()\n',
+ # '#include // mbstowcs()\n'
+ # '#include \n',
+ # )
+
+ # txt = replace_exact(txt, ' Py_ssize_t res;\n', '')
+
+ # txt = replace_exact(
+ # txt,
+ # ' res = readlink(cpath, cbuf, cbuf_len);\n',
+ # (
+ # ' Py_ssize_t res = readlink(cpath, cbuf, cbuf_len);\n'
+ # ' Py_ssize_t res2 = readlink(cpath, cbuf, cbuf_len);\n'
+ # ' ssize_t res3 = readlink(cpath, cbuf, cbuf_len);\n'
+ # ' ssize_t res4 = readlinkat(AT_FDCWD, cpath,
+ # cbuf, cbuf_len);\n'
+ # ' int res5 = syscall(SYS_readlinkat, AT_FDCWD, cpath,'
+ # ' cbuf, cbuf_len);\n'
+ # ' ssize_t res6 = syscall(SYS_readlinkat, AT_FDCWD, cpath,'
+ # ' cbuf, cbuf_len);\n'
+ # ' char dlog[512];\n'
+ # ' snprintf(dlog, sizeof(dlog),'
+ # ' "res=%zd res2=%zd res3=%zd res4=%zd res5=%d res6=%zd"\n'
+ # ' " (res == -1)=%d (res2 == -1)=%d (res3 == -1)=%d'
+ # ' (res4 == -1)=%d (res5 == -1)=%d (res6 == -1)=%d",\n'
+ # ' res, res2, res3, res4, res5, res6,\n'
+ # ' (res == -1), (res2 == -1), (res3 == -1),'
+ # ' (res4 == -1), (res5 == -1), (res6 == -1));\n'
+ # ' Py_BallisticaLowLevelDebugLog(dlog);\n'
+ # '\n'
+ # ' char dlog1[512];\n'
+ # ' ssize_t st1;\n'
+ # ' Py_ssize_t st2;\n'
+ # ' snprintf(dlog1, sizeof(dlog1),
+ # "ValsA1 sz1=%zu sz2=%zu res=%zd'
+ # ' res_hex=%lX res_cmp=%d res_cmp_2=%d pathlen=%d slen=%d'
+ # ' path=\'%s\'", sizeof(st1), sizeof(st2), res,'
+ # ' res, (int)(res == -1), (int)((int)res == -1),'
+ # ' (int)wcslen(path), (int)strlen(cpath), cpath);\n'
+ # ' Py_BallisticaLowLevelDebugLog(dlog1);\n'
+ # ),
+ # )
+
+ # txt = replace_exact(
+ # txt,
+ # " cbuf[res] = '\\0'; /* buf will be null terminated */",
+ # (
+ # ' char dlog[512];\n'
+ # ' snprintf(dlog, sizeof(dlog), "ValsB res=%d resx=%lX'
+ # ' eq1=%d eq2=%d",'
+ # ' (int)res, res, (int)(res == -1),'
+ # ' (int)((size_t)res == cbuf_len));\n'
+ # ' Py_BallisticaLowLevelDebugLog(dlog);\n'
+ # " cbuf[res] = '\\0'; /* buf will be null terminated */"
+ # ),
+ # )
writefile(fname, txt)
diff --git a/tools/pcommand b/tools/pcommand
index 094a4a5a..21c45233 100755
--- a/tools/pcommand
+++ b/tools/pcommand
@@ -114,7 +114,6 @@ from batools.pcommand import (
from batools.pcommand2 import (
gen_python_init_module,
gen_monolithic_register_modules,
- stage_server_file,
py_examine,
clean_orphaned_assets,
win_ci_install_prereqs,