1.7.35 work in progress

This commit is contained in:
Eric 2024-05-08 10:18:01 -07:00
parent 5ebdccb2b4
commit 671b26efa7
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
40 changed files with 2107 additions and 815 deletions

128
.efrocachemap generated
View File

@ -421,43 +421,43 @@
"build/assets/ba_data/audio/zoeOw.ogg": "74befe45a8417e95b6a2233c51992a26",
"build/assets/ba_data/audio/zoePickup01.ogg": "48ab8cddfcde36a750856f3f81dd20c8",
"build/assets/ba_data/audio/zoeScream01.ogg": "2b468aedfa8741090247f04eb9e6df55",
"build/assets/ba_data/data/langdata.json": "1de1f6150bd8e602b839378a669f3634",
"build/assets/ba_data/data/languages/arabic.json": "2c2915e10124bb8f69206da9c608d57c",
"build/assets/ba_data/data/languages/belarussian.json": "09954e550d13d3d9cb5a635a1d32a151",
"build/assets/ba_data/data/languages/chinese.json": "5fa538e855bcfe20e727e0ad5831efad",
"build/assets/ba_data/data/langdata.json": "582c633a37b78e3326e20d2a5b8969a0",
"build/assets/ba_data/data/languages/arabic.json": "5c27239be3d4f8daefd9f3bd7e99ff8d",
"build/assets/ba_data/data/languages/belarussian.json": "0a2b0ae82298cec42764558b5b49e4dd",
"build/assets/ba_data/data/languages/chinese.json": "fcd59e90c12e8106ce418b65b97b3db6",
"build/assets/ba_data/data/languages/chinesetraditional.json": "319565f8a15667488f48dbce59278e39",
"build/assets/ba_data/data/languages/croatian.json": "e671b9d0c012be1a30f9c15eb1b81860",
"build/assets/ba_data/data/languages/czech.json": "15be4fd59895135bad0265f79b362d5b",
"build/assets/ba_data/data/languages/danish.json": "8e57db30c5250df2abff14a822f83ea7",
"build/assets/ba_data/data/languages/dutch.json": "b0900d572c9141897d53d6574c471343",
"build/assets/ba_data/data/languages/english.json": "48fe4c6f97b07420238244309b54a61e",
"build/assets/ba_data/data/languages/english.json": "b7a0d185b50957f731db80897313a055",
"build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880",
"build/assets/ba_data/data/languages/filipino.json": "838148a9390d5a19ba2514da7c48bc98",
"build/assets/ba_data/data/languages/french.json": "917e4174d6f0eb7f00c27fd79cfbb924",
"build/assets/ba_data/data/languages/filipino.json": "5d28e03d97a3626e790481401ee894a4",
"build/assets/ba_data/data/languages/french.json": "ee2a81129519d7030a617308da8c9195",
"build/assets/ba_data/data/languages/german.json": "eaf3f1bf633566de133c61f4f5377e62",
"build/assets/ba_data/data/languages/gibberish.json": "a1afce99249645003017ebec50e716fe",
"build/assets/ba_data/data/languages/gibberish.json": "217a21b35406d1e97954b5c2dbb2c936",
"build/assets/ba_data/data/languages/greek.json": "ad3c0d38f34d809824892d6f22808dbf",
"build/assets/ba_data/data/languages/hindi.json": "90f54663e15d85a163f1848a8e9d8d07",
"build/assets/ba_data/data/languages/hungarian.json": "796a290a8c44a1e7635208c2ff5fdc6e",
"build/assets/ba_data/data/languages/hindi.json": "bb3548531daf7bc7fee4a28d48228c32",
"build/assets/ba_data/data/languages/hungarian.json": "6b08fea24b72cc805ed0dc59e11c4cd6",
"build/assets/ba_data/data/languages/indonesian.json": "9103845242b572aa8ba48e24f81ddb68",
"build/assets/ba_data/data/languages/italian.json": "59159a9ca784709e807e0855a7ba28b6",
"build/assets/ba_data/data/languages/italian.json": "abac9bc027257fdb757c5c1dc4686a47",
"build/assets/ba_data/data/languages/korean.json": "4e3524327a0174250aff5e1ef4c0c597",
"build/assets/ba_data/data/languages/malay.json": "f6ce0426d03a62612e3e436ed5d1be1f",
"build/assets/ba_data/data/languages/persian.json": "d42aa034d03f487edd15e651d6f469ab",
"build/assets/ba_data/data/languages/polish.json": "b90feb3cc20a80284ef44546df7099e6",
"build/assets/ba_data/data/languages/portuguese.json": "5dcc9a324a8e926a6d5dd109cceaee1a",
"build/assets/ba_data/data/languages/persian.json": "fbf51bb87c6f5fe63c6a3aee38713f31",
"build/assets/ba_data/data/languages/polish.json": "ac63e339b68819009300f839a9bbd3b2",
"build/assets/ba_data/data/languages/portuguese.json": "ab295421a4449ae01aeed3633426ba2f",
"build/assets/ba_data/data/languages/romanian.json": "b3e46efd6f869dbd78014570e037c290",
"build/assets/ba_data/data/languages/russian.json": "3efaaf5eac320fceef029501dec4109b",
"build/assets/ba_data/data/languages/russian.json": "cba5f250a272a4a4eea28ceece9fd549",
"build/assets/ba_data/data/languages/serbian.json": "d7452dd72ac0e51680cb39b5ebaa1c69",
"build/assets/ba_data/data/languages/slovak.json": "c00fb27cf982ffad5a4370ad3b16bd21",
"build/assets/ba_data/data/languages/spanish.json": "124e1f0073e3ee6af2de70dcd1a834d1",
"build/assets/ba_data/data/languages/slovak.json": "3c08c748c96c71bd9e1d7291fb8817b6",
"build/assets/ba_data/data/languages/spanish.json": "c380ea87d11cfb129b661dfd3781edc9",
"build/assets/ba_data/data/languages/swedish.json": "5142a96597d17d8344be96a603da64ac",
"build/assets/ba_data/data/languages/tamil.json": "b9fcc523639f55e05c7f4e7914f3321a",
"build/assets/ba_data/data/languages/thai.json": "1d665629361f302693dead39de8fa945",
"build/assets/ba_data/data/languages/turkish.json": "fcd90d63b5d3eae3eda5e94174008327",
"build/assets/ba_data/data/languages/ukrainian.json": "3378b122cea7aa9e05ad50d50809b199",
"build/assets/ba_data/data/languages/turkish.json": "270c07e826bf799246906ac919d78545",
"build/assets/ba_data/data/languages/ukrainian.json": "76ad64cb4911c8d5a3e4815b865ce5bd",
"build/assets/ba_data/data/languages/venetian.json": "c0aceb82c26a9361421479d01edaa388",
"build/assets/ba_data/data/languages/vietnamese.json": "921cd1e50f60fe3e101f246e172750ba",
"build/assets/ba_data/data/languages/vietnamese.json": "7e40fcd270b34c1e836ba51a2c6cbce7",
"build/assets/ba_data/data/maps/big_g.json": "1dd301d490643088a435ce75df971054",
"build/assets/ba_data/data/maps/bridgit.json": "6aea74805f4880cc11237c5734a24422",
"build/assets/ba_data/data/maps/courtyard.json": "4b836554c8949bcd2ae382f5e3c1a9cc",
@ -4038,50 +4038,50 @@
"build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1",
"build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "bfd58a687b408c5c6ed5bc63d97095d9",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "f73ecc7f635b3988851021b2bf7c87d7",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "5921b09e1ea841986fd0e8c348f1ba96",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "d2d5506c256a6374c9ad3ef403948849",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "84dc1f1bf91f985f3814752e305073cf",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "1697df9c8b40249705d6e597f3f38385",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "37fa3c5cf296f4751cd4fd48b5090288",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "f2bf600abed20a7bb626ba11c672af4e",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "241cb5e70f31a1bf4b837d1372dc78e1",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "8d1e211c491ae485cd5e00c27fa01e03",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "804a6819db1e8107f4e757903cdbf273",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "634597ba33aa0c29625fa81bcb50c608",
"build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "60c7782d742f24a67352cc49e4080efa",
"build/prefab/full/mac_x86_64_gui/release/ballisticakit": "5d2abee6403963b60b6b422d84d58738",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "fce8370bb7ea6b1b3208dc4efa4b20df",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "6979de360c31b792f53572182438f5b0",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "c8161cd1a54a17a4cc4e17c0b2ea0fe4",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "4982637e226891d5afa48400f7ee619b",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "ca12bee3cb430eccfa0235719a5d1048",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "94d1c2579f2fbc99f4725975f08bb150",
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "55c07828ad7fccc584dd96d1ffebd760",
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "7ca8b0b5c34766ce9df9babb6ec8311f",
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "55c07828ad7fccc584dd96d1ffebd760",
"build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "7ca8b0b5c34766ce9df9babb6ec8311f",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "6918f36d76061951f51c33d1a8dea572",
"build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "e9e4da9ad759e92741ab10212c51270a",
"build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "6918f36d76061951f51c33d1a8dea572",
"build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "e9e4da9ad759e92741ab10212c51270a",
"build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "17da2884d5ca518c84a93d3d2b0edd79",
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "52f4b8d0b8908a5261d1160feba46327",
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "17da2884d5ca518c84a93d3d2b0edd79",
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "52f4b8d0b8908a5261d1160feba46327",
"build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "0db136ec64c90a522e112acbbabfb11d",
"build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "ab81671e4e3be14b17ce721eb835b426",
"build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "6a26caeb1dd4d4871d52e8e2fb2c11ef",
"build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "ab81671e4e3be14b17ce721eb835b426",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "d3626b90791c87180f16ae80b05b088e",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "05bd119dcb343f201f2030eff9216eef",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "e1c3d622bbbd66770ba019fc92abba85",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "79cbceebbbfa08cef06358cf4ca07634",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "c66603084bf3da24a796655a84c8dd44",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "d4a2fce87510ef0a47997e04b5508c4b",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "e2df70a204ac392d5afd6ef14f656687",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "431d30bd06bbb3568a8a219c19c5817a",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "9a3a53a5a5894ed950c3d45c68d15372",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "c57a8d0742c9465ada46a01b62ad75ba",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "738d3996ff299bde2857df59dde0f5f4",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "f989d6393056783307de70a2bdfa098b",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "5fe35efb6f34e13392575b8a0b7469cc",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "b3b64e3df4ea39091e75e95a40efab0a",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "a523863d1dc98162536c43a2ec77975c",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "00bacc4b6d42688712813966df7d6a42",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "92b9c2787b61f3f2972253ab9be6309a",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "5f1c8cdcdd6ce276d039d36b3734f507",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "fef789ff0160ea56366a2463b3c6c39c",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "8ec6e4ecef744cb6fa64f3617ec49a2c",
"build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "6e00380f58d4ff96c618b454b94d7c3c",
"build/prefab/full/mac_x86_64_gui/release/ballisticakit": "bb2fac09a8e572721b48b22cd2718417",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "98e7ef0bf26e9df1088fe22da0a4286b",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "68cf87da69fa5d004c18e9661179b88e",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "509ed7a3ec78263793c20b8e4fe24cdb",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "af01eab4ab71cc0cb27cfdd6579efce2",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "7b1567efe48e0f174ca1fe6d12cce83f",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "a6e4ba2782551897e24b7f31937df01c",
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "24c1641a1bef7c56d8b3805fbd01ac30",
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "3da37afad8903a3c24c38fb698a19ce1",
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "24c1641a1bef7c56d8b3805fbd01ac30",
"build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "3da37afad8903a3c24c38fb698a19ce1",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "d8b9d06d24d68ea28f271630fe7927d8",
"build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "20e6bd566fa26ab469f18ee07301b2a5",
"build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "d8b9d06d24d68ea28f271630fe7927d8",
"build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "20e6bd566fa26ab469f18ee07301b2a5",
"build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "2cae1591b40b3e514dc8bfa53c381ca0",
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "9cb9babbe43f393f286c596c572f3687",
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "2cae1591b40b3e514dc8bfa53c381ca0",
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "9cb9babbe43f393f286c596c572f3687",
"build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "e991ed53b63acb73579097a38ba63731",
"build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "2ff4914fca4dbd5ad144b32b9d89c3fb",
"build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "a53b90db9b3d05d8048dcd63e56debd3",
"build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "2ff4914fca4dbd5ad144b32b9d89c3fb",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "c86657aaf33d885d4dbf9b88e6168012",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "ae4e9ad706f71ca3566fa6440c49ae5e",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "d1188f99c618449e49555c5d50326e6e",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "c2b3f66f80934e4ad07212a83bc5889b",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "5249d461409c214ea0a91bc9baf10599",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "e2d37edade6cd5868e342b782ceda44d",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "af49482fc819b4df01c78996110c5a80",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "2997322e5ec3233d230ab6b1d581cabb",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "b611c090513a21e2fe90e56582724e9d",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "72bfed2cce8ff19741989dec28302f3f",

View File

@ -1,4 +1,44 @@
### 1.7.35 (build 21827, api 8, 2024-04-30)
### 1.7.35 (build 21848, api 8, 2024-05-08)
- Fixed an issue where the engine would block at exit on some version of Linux
until Ctrl-D was pressed in the calling terminal.
- Reworked the 'Enter Code' dialog into a 'Send Info' dialog. The `sendinfo`
command is 99% of the reason for 'Enter Code' existing, so this simplifies
things for that use case and hopefully clarifies its purpose so I can spend
less time responding to app reviewers and more time improving the game.
- The `Network Testing` panel no longer requires being signed in (it just skips
one test if not signed in).
- Took a pass through the engine and its servers to make things more ipv6
friendly and prep for an eventual ipv6-only world (though ipv4 won't be going
anywhere for a long time). The existing half-hearted state of ipv6 support was
starting to cause problems when testing in certain ipv6-only environments, so
it was time to clean it up.
- The engine will now establish its persistent v2-transport connections to
regional servers using ipv6 when that is the fastest option based on ping
tests.
- Improved the efficiency of the `connectivity` system which determines which
regional ballistica server to establish a connection to (All V2 server
communication goes through this connection). It now takes geography into
account, so if it gets a low ping to a server in South America it won't try
pinging Warsaw, etc. Set the env var `BA_DEBUG_LOG_CONNECTIVITY=1` if you want
to watch it do it's thing and holler if you see any bad results.
- Servers can now provide their public ipv4 and ipv6 addresses in their configs.
Previously, a server's address was always determined automatically based on
how it connected to the master server, but this would only provide one of the
two forms. Now it is possible to provide both.
- (WORK IN PROGRESS) As of this version, servers are *required* to be accessible
via ipv4 to appear in the public listing. So they may need to provide an ipv4
address in their config if the automatically detected one is ipv6. This should
reduce the confusion of ipv6-only servers appearing greyed out for lots of
ipv4-only people. Pretty much everyone can connect to ipv4.
- (WORK IN PROGRESS) There is now more personalized error feedback for the
connectivity checks when poking `Make My Party Public` or when launching the
command line server. Hopefully this will help navigate the new dual ipv4/ipv6
situation.
- (WORK IN PROGRESS) The low level `ConnectionToHostUDP` class can now accept
multiple `SockAddr`s; it will attempt to contact the host on all of them and
use whichever responds first. This allows us to pass both ipv4 and ipv6
addresses when available and transparently use whichever is more performant.
### 1.7.34 (build 21823, api 8, 2024-04-26)
- Bumped Python version from 3.11 to 3.12 for all builds and project tools. One

View File

@ -386,12 +386,12 @@
"ba_data/python/bauiv1lib/__pycache__/play.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/playoptions.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/popup.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/promocode.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/purchase.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/qrcode.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/radiogroup.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/report.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/resourcetypeinfo.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/sendinfo.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/serverdialog.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/specialoffer.cpython-312.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/tabs.cpython-312.opt-1.pyc",
@ -495,12 +495,12 @@
"ba_data/python/bauiv1lib/profile/browser.py",
"ba_data/python/bauiv1lib/profile/edit.py",
"ba_data/python/bauiv1lib/profile/upgrade.py",
"ba_data/python/bauiv1lib/promocode.py",
"ba_data/python/bauiv1lib/purchase.py",
"ba_data/python/bauiv1lib/qrcode.py",
"ba_data/python/bauiv1lib/radiogroup.py",
"ba_data/python/bauiv1lib/report.py",
"ba_data/python/bauiv1lib/resourcetypeinfo.py",
"ba_data/python/bauiv1lib/sendinfo.py",
"ba_data/python/bauiv1lib/serverdialog.py",
"ba_data/python/bauiv1lib/settings/__init__.py",
"ba_data/python/bauiv1lib/settings/__pycache__/__init__.cpython-312.opt-1.pyc",

View File

@ -389,12 +389,12 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bauiv1lib/profile/browser.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/profile/edit.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/profile/upgrade.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/promocode.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/purchase.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/qrcode.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/radiogroup.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/report.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/resourcetypeinfo.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/sendinfo.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/serverdialog.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/settings/__init__.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/settings/advanced.py \
@ -665,12 +665,12 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bauiv1lib/profile/__pycache__/browser.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/profile/__pycache__/edit.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/profile/__pycache__/upgrade.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/promocode.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/purchase.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/qrcode.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/radiogroup.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/report.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/resourcetypeinfo.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/sendinfo.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/serverdialog.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/settings/__pycache__/__init__.cpython-312.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/settings/__pycache__/advanced.cpython-312.opt-1.pyc \

View File

@ -32,6 +32,8 @@ class NetworkSubsystem:
# For debugging.
self.v1_test_log: str = ''
self.v1_ctest_results: dict[int, str] = {}
self.connectivity_state = 'uninited'
self.transport_state = 'uninited'
self.server_time_offset_hours: float | None = None
@property

View File

@ -102,8 +102,8 @@ class ServerController:
self._shutdown_reason: ShutdownReason | None = None
self._executing_shutdown = False
# Make note if they want us to import a playlist;
# we'll need to do that first if so.
# Make note if they want us to import a playlist; we'll need to
# do that first if so.
self._playlist_fetch_running = self._config.playlist_code is not None
self._playlist_fetch_sent_request = False
self._playlist_fetch_got_response = False
@ -366,7 +366,8 @@ class ServerController:
raise RuntimeError(f'Unknown session type {sessiontype}')
# Need to add this in a transaction instead of just setting
# it directly or it will get overwritten by the master-server.
# it directly or it will get overwritten by the
# master-server.
plus.add_v1_account_transaction(
{
'type': 'ADD_PLAYLIST',
@ -407,7 +408,7 @@ class ServerController:
appcfg['Teams Series Length'] = self._config.teams_series_length
appcfg['FFA Series Length'] = self._config.ffa_series_length
# deprecated, left here in order to not break mods
# Deprecated; left here in order to not break mods.
classic.teams_series_length = self._config.teams_series_length
classic.ffa_series_length = self._config.ffa_series_length
@ -423,6 +424,13 @@ class ServerController:
bascenev1.set_public_party_queue_enabled(self._config.enable_queue)
bascenev1.set_public_party_name(self._config.party_name)
bascenev1.set_public_party_stats_url(self._config.stats_url)
bascenev1.set_public_party_public_address_ipv4(
self._config.public_ipv4_address
)
bascenev1.set_public_party_public_address_ipv6(
self._config.public_ipv6_address
)
bascenev1.set_public_party_enabled(self._config.party_is_public)
bascenev1.set_player_rejoin_cooldown(

View File

@ -52,7 +52,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
TARGET_BALLISTICA_BUILD = 21827
TARGET_BALLISTICA_BUILD = 21848
TARGET_BALLISTICA_VERSION = '1.7.35'

View File

@ -144,8 +144,8 @@ class CloudSubsystem(babase.AppSubsystem):
@overload
async def send_message_async(
self, msg: bacommon.cloud.PromoCodeMessage
) -> bacommon.cloud.PromoCodeResponse: ...
self, msg: bacommon.cloud.SendInfoMessage
) -> bacommon.cloud.SendInfoResponse: ...
@overload
async def send_message_async(

View File

@ -134,6 +134,8 @@ from _bascenev1 import (
set_public_party_enabled,
set_public_party_max_size,
set_public_party_name,
set_public_party_public_address_ipv4,
set_public_party_public_address_ipv6,
set_public_party_queue_enabled,
set_public_party_stats_url,
set_replay_speed_exponent,
@ -429,6 +431,8 @@ __all__ = [
'set_public_party_enabled',
'set_public_party_max_size',
'set_public_party_name',
'set_public_party_public_address_ipv4',
'set_public_party_public_address_ipv6',
'set_public_party_queue_enabled',
'set_public_party_stats_url',
'set_player_rejoin_cooldown',

View File

@ -126,10 +126,12 @@ class AccountLinkWindow(bui.Window):
plus.run_v1_account_transactions()
def _enter_code_press(self) -> None:
from bauiv1lib import promocode
from bauiv1lib.sendinfo import SendInfoWindow
promocode.PromoCodeWindow(
modal=True, origin_widget=self._enter_code_button
SendInfoWindow(
modal=True,
legacy_code_mode=True,
origin_widget=self._enter_code_button,
)
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out

View File

@ -609,7 +609,7 @@ class AccountSettingsWindow(bui.Window):
autoselect=True,
size=(button_width, 60),
label=bui.Lstr(
value='${A}${B}',
value='${A} ${B}',
subs=[
(
'${A}',
@ -654,7 +654,7 @@ class AccountSettingsWindow(bui.Window):
# in all languages. Can revisit if not true.
# https://developer.apple.com/forums/thread/725779
label=bui.Lstr(
value='${A}${B}',
value='${A} ${B}',
subs=[
(
'${A}',
@ -695,39 +695,58 @@ class AccountSettingsWindow(bui.Window):
label='',
on_activate_call=self._v2_proxy_sign_in_press,
)
# TODO: Add translation strings for these.
v2labeltext: bui.Lstr | str = (
'Sign in with an email/password'
if show_game_center_sign_in_button
# else bui.Lstr(resource=self._r + '.signInWithV2Text')
else bui.Lstr(resource=self._r + '.signInText')
)
v2infotext: bui.Lstr | str | None = None
# (
# None
# if show_game_center_sign_in_button
# else bui.Lstr(resource=self._r + '.signInWithV2InfoText')
# )
bui.textwidget(
parent=self._subcontainer,
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v + 17),
position=(
self._sub_width * 0.5,
v + (17 if v2infotext is not None else 10),
),
text=bui.Lstr(
value='${A}${B}',
value='${A} ${B}',
subs=[
('${A}', bui.charstr(bui.SpecialChar.V2_LOGO)),
(
'${B}',
bui.Lstr(resource=self._r + '.signInWithV2Text'),
v2labeltext,
),
],
),
maxwidth=button_width * 0.8,
color=(0.75, 1.0, 0.7),
)
bui.textwidget(
parent=self._subcontainer,
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v - 4),
text=bui.Lstr(resource=self._r + '.signInWithV2InfoText'),
flatness=1.0,
scale=0.57,
maxwidth=button_width * 0.9,
color=(0.55, 0.8, 0.5),
)
if v2infotext is not None:
bui.textwidget(
parent=self._subcontainer,
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v - 4),
text=v2infotext,
flatness=1.0,
scale=0.57,
maxwidth=button_width * 0.9,
color=(0.55, 0.8, 0.5),
)
if first_selectable is None:
first_selectable = btn
if bui.app.ui_v1.use_toolbars:
@ -770,7 +789,7 @@ class AccountSettingsWindow(bui.Window):
size=(0, 0),
position=(self._sub_width * 0.5, v + 17),
text=bui.Lstr(
value='${A}${B}',
value='${A} ${B}',
subs=[
('${A}', bui.charstr(bui.SpecialChar.LOCAL_ACCOUNT)),
(

View File

@ -39,17 +39,39 @@ class V2ProxySignInWindow(bui.Window):
)
)
self._loading_text = bui.textwidget(
self._state_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.5),
position=(self._width * 0.5, self._height * 0.6),
h_align='center',
v_align='center',
size=(0, 0),
scale=1.4,
maxwidth=0.9 * self._width,
text=bui.Lstr(
value='${A}...',
subs=[('${A}', bui.Lstr(resource='loadingText'))],
),
color=(1, 1, 1),
)
self._sub_state_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.55),
h_align='center',
v_align='top',
scale=0.85,
size=(0, 0),
maxwidth=0.9 * self._width,
text='',
)
self._sub_state_text2 = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.1, self._height * 0.3),
h_align='left',
v_align='top',
scale=0.7,
size=(0, 0),
maxwidth=0.9 * self._width,
text='',
)
self._cancel_button = bui.buttonwidget(
@ -66,14 +88,79 @@ class V2ProxySignInWindow(bui.Window):
edit=self._root_widget, cancel_button=self._cancel_button
)
self._update_timer: bui.AppTimer | None = None
self._message_in_flight = False
self._complete = False
# self._delay_ticks = 0
self._connection_wait = 5
# self._update_timer: bui.AppTimer | None = None
self._update_timer = bui.AppTimer(
1.23, bui.WeakCall(self._update), repeat=True
)
bui.pushcall(bui.WeakCall(self._update))
# Ask the cloud for a proxy login id.
assert bui.app.plus is not None
bui.app.plus.cloud.send_message_cb(
# assert bui.app.plus is not None
# bui.app.plus.cloud.send_message_cb(
# bacommon.cloud.LoginProxyRequestMessage(),
# on_response=bui.WeakCall(self._on_proxy_request_response),
# )
def _update(self) -> None:
# print('hello from update', time.monotonic())
if self._message_in_flight or self._complete:
return
plus = bui.app.plus
assert plus is not None
# Spin for a moment if it looks like we have no server
# connection; it might still be getting on its feed.
if not plus.cloud.connected and self._connection_wait > 0:
self._connection_wait -= 1
return
plus.cloud.send_message_cb(
bacommon.cloud.LoginProxyRequestMessage(),
on_response=bui.WeakCall(self._on_proxy_request_response),
)
self._message_in_flight = True
def _get_server_address(self) -> str:
plus = bui.app.plus
assert plus is not None
return plus.get_master_server_address(version=2)
def _set_error_state(self, error_location: str) -> None:
msaddress = self._get_server_address()
addr = msaddress.removeprefix('https://')
bui.textwidget(
edit=self._state_text,
text=f'Unable to connect to {addr}.',
color=(1, 0, 0),
)
support_email = 'support@froemling.net'
bui.textwidget(
edit=self._sub_state_text,
text=(
f'Usually this means your internet is down.\n'
f'Please contact {support_email} if this is not the case.'
),
color=(1, 0, 0),
)
bui.textwidget(
edit=self._sub_state_text2,
text=(
f'debug-info:\n'
f' error-location: {error_location}\n'
f' connectivity: {bui.app.net.connectivity_state}\n'
f' transport: {bui.app.net.transport_state}'
),
color=(0.8, 0.2, 0.3),
flatness=1.0,
shadow=0.0,
)
def _on_proxy_request_response(
self, response: bacommon.cloud.LoginProxyRequestResponse | Exception
@ -81,17 +168,51 @@ class V2ProxySignInWindow(bui.Window):
plus = bui.app.plus
assert plus is not None
# Something went wrong. Show an error message and that's it.
if isinstance(response, Exception):
bui.textwidget(
edit=self._loading_text,
text=bui.Lstr(resource='internal.unavailableNoConnectionText'),
color=(1, 0, 0),
if not self._message_in_flight:
logging.warning(
'v2proxy got _on_proxy_request_response'
' without _message_in_flight set; unexpected.'
)
self._message_in_flight = False
# if bool(True) and random.random() < 1.0:
# response = Exception('dummy')
msaddress = self._get_server_address()
# Something went wrong. Show an error message and schedule retry.
if isinstance(response, Exception):
# addr = msaddress.removeprefix('https://')
# bui.textwidget(
# edit=self._state_text,
# text=f'Unable to connect to {addr}.',
# color=(1, 0, 0),
# )
# bui.textwidget(
# edit=self._sub_state_text,
# text='Will retry in a moment...',
# color=(1, 0, 0),
# )
# self._delay_ticks = 3
self._set_error_state(f'response exc ({type(response).__name__})')
self._complete = True
# bui.textwidget(
# edit=self._state_text,
# text=bui.Lstr(
# resource='internal.unavailableNoConnectionText'),
# color=(1, 0, 0),
# )
return
self._complete = True
self._state_text.delete()
self._sub_state_text.delete()
self._sub_state_text2.delete()
# Show link(s) the user can use to sign in.
address = plus.get_master_server_address(version=2) + response.url
address = msaddress + response.url
address_pretty = address.removeprefix('https://')
assert bui.app.classic is not None
@ -172,12 +293,11 @@ class V2ProxySignInWindow(bui.Window):
def _got_status(
self, response: bacommon.cloud.LoginProxyStateQueryResponse | Exception
) -> None:
# For now, if anything goes wrong on the server-side, just abort
# with a vague error message. Can be more verbose later if need be.
if (
isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse)
and response.state is response.State.FAIL
):
logging.info('LoginProxy failed.')
bui.getsound('error').play()
bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
self._done()

View File

@ -989,8 +989,8 @@ class PrivateGatherTab(GatherTab):
bui.getsound('error').play()
return
self._debug_server_comm('got valid connect response')
assert cresult.addr is not None and cresult.port is not None
bs.connect_to_party(cresult.addr, port=cresult.port)
assert cresult.address4 is not None and cresult.port is not None
bs.connect_to_party(cresult.address4, port=cresult.port)
except Exception:
self._debug_server_comm('got connect response error')
bui.getsound('error').play()

View File

@ -7,14 +7,14 @@ from __future__ import annotations
import time
from typing import TYPE_CHECKING, override
from bauiv1lib.promocode import PromoCodeWindow
from bauiv1lib.sendinfo import SendInfoWindow
import bauiv1 as bui
if TYPE_CHECKING:
from typing import Any, Callable
class SharePlaylistImportWindow(PromoCodeWindow):
class SharePlaylistImportWindow(SendInfoWindow):
"""Window for importing a shared playlist."""
def __init__(
@ -22,7 +22,9 @@ class SharePlaylistImportWindow(PromoCodeWindow):
origin_widget: bui.Widget | None = None,
on_success_callback: Callable[[], Any] | None = None,
):
PromoCodeWindow.__init__(self, modal=True, origin_widget=origin_widget)
SendInfoWindow.__init__(
self, modal=True, legacy_code_mode=True, origin_widget=origin_widget
)
self._on_success_callback = on_success_callback
def _on_import_response(self, response: dict[str, Any] | None) -> None:

View File

@ -14,12 +14,17 @@ if TYPE_CHECKING:
from typing import Any
class PromoCodeWindow(bui.Window):
"""Window for entering promo codes."""
class SendInfoWindow(bui.Window):
"""Window for sending info to the developer."""
def __init__(
self, modal: bool = False, origin_widget: bui.Widget | None = None
self,
modal: bool = False,
legacy_code_mode: bool = False,
origin_widget: bui.Widget | None = None,
):
self._legacy_code_mode = legacy_code_mode
scale_origin: tuple[float, float] | None
if origin_widget is not None:
self._transition_out = 'out_scale'
@ -30,8 +35,8 @@ class PromoCodeWindow(bui.Window):
scale_origin = None
transition = 'in_right'
width = 450
height = 330
width = 450 if legacy_code_mode else 600
height = 200 if legacy_code_mode else 300
self._modal = modal
self._r = 'promoCodeWindow'
@ -66,56 +71,73 @@ class PromoCodeWindow(bui.Window):
)
v = height - 74
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource='codesExplainText'),
maxwidth=width * 0.9,
position=(width * 0.5, v),
color=(0.7, 0.7, 0.7, 1.0),
size=(0, 0),
scale=0.8,
h_align='center',
v_align='center',
)
v -= 60
if legacy_code_mode:
v -= 20
else:
v -= 20
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource='sendInfoDescriptionText'),
maxwidth=width * 0.9,
position=(width * 0.5, v),
color=(0.7, 0.7, 0.7, 1.0),
size=(0, 0),
scale=0.8,
h_align='center',
v_align='center',
)
v -= 20
# bui.textwidget(
# parent=self._root_widget,
# text=bui.Lstr(
# resource='supportEmailText',
# subs=[('${EMAIL}', 'support@froemling.net')],
# ),
# maxwidth=width * 0.9,
# position=(width * 0.5, v),
# color=(0.7, 0.7, 0.7, 1.0),
# size=(0, 0),
# scale=0.65,
# h_align='center',
# v_align='center',
# )
v -= 80
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(
resource='supportEmailText',
subs=[('${EMAIL}', 'support@froemling.net')],
resource=(
self._r + '.codeText'
if legacy_code_mode
else 'descriptionText'
)
),
maxwidth=width * 0.9,
position=(width * 0.5, v),
color=(0.7, 0.7, 0.7, 1.0),
size=(0, 0),
scale=0.65,
h_align='center',
v_align='center',
)
v -= 80
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource=self._r + '.codeText'),
position=(22, v),
color=(0.8, 0.8, 0.8, 1.0),
size=(90, 30),
h_align='right',
maxwidth=100,
)
v -= 8
self._text_field = bui.textwidget(
parent=self._root_widget,
position=(125, v),
size=(280, 46),
size=(280 if legacy_code_mode else 380, 46),
text='',
h_align='left',
v_align='center',
max_chars=64,
color=(0.9, 0.9, 0.9, 1.0),
description=bui.Lstr(resource=self._r + '.codeText'),
description=bui.Lstr(
resource=(
self._r + '.codeText'
if legacy_code_mode
else 'descriptionText'
)
),
editable=True,
padding=4,
on_return_press_call=self._activate_enter_button,
@ -166,6 +188,9 @@ class PromoCodeWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
plus = bui.app.plus
assert plus is not None
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
@ -180,38 +205,84 @@ class PromoCodeWindow(bui.Window):
from_window=self._root_widget,
)
code: Any = bui.textwidget(query=self._text_field)
assert isinstance(code, str)
description: Any = bui.textwidget(query=self._text_field)
assert isinstance(description, str)
bui.app.create_async_task(_run_code(code))
# Used for things like unlocking shared playlists or linking
# accounts: talk directly to V1 server via transactions.
if self._legacy_code_mode:
if plus.get_v1_account_state() != 'signed_in':
bui.screenmessage(
bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
)
bui.getsound('error').play()
else:
plus.add_v1_account_transaction(
{
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': description,
}
)
plus.run_v1_account_transactions()
else:
bui.app.create_async_task(_send_info(description))
async def _run_code(code: str) -> None:
from bacommon.cloud import PromoCodeMessage
async def _send_info(description: str) -> None:
from bacommon.cloud import SendInfoMessage
plus = bui.app.plus
assert plus is not None
try:
# If we're signed in with a V2 account, ship this to V2 server.
# Don't allow *anything* if our V2 transport connection isn't up.
if not plus.cloud.connected:
bui.screenmessage(
bui.Lstr(resource='internal.unavailableNoConnectionText'),
color=(1, 0, 0),
)
bui.getsound('error').play()
return
# Ship to V2 server, with or without account info.
if plus.accounts.primary is not None:
with plus.accounts.primary:
response = await plus.cloud.send_message_async(
PromoCodeMessage(code)
SendInfoMessage(description)
)
# If V2 handled it, we're done.
if response.valid:
# Support simple message printing from v2 server.
if response.message is not None:
bui.screenmessage(response.message, color=(0, 1, 0))
return
else:
response = await plus.cloud.send_message_async(
SendInfoMessage(description)
)
# If V2 didn't accept it (or isn't signed in) kick it over to V1.
# Support simple message printing from v2 server.
if response.message is not None:
bui.screenmessage(response.message, color=(0, 1, 0))
# If V2 handled it, we're done.
if response.handled:
return
# Ok; V2 didn't handle it. Try V1 if we're signed in there.
if plus.get_v1_account_state() != 'signed_in':
bui.screenmessage(
bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
)
bui.getsound('error').play()
return
# Push it along to v1 as an old style code. Allow v2 response to
# sub in its own code.
plus.add_v1_account_transaction(
{
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': code,
'code': (
description
if response.legacy_code is None
else response.legacy_code
),
}
)
plus.run_v1_account_transactions()

View File

@ -1,7 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality for advanced settings."""
# pylint: disable=too-many-lines
from __future__ import annotations
@ -91,7 +90,7 @@ class AdvancedSettingsWindow(bui.Window):
self._scroll_width = self._width - (100 + 2 * x_inset)
self._scroll_height = self._height - 115.0
self._sub_width = self._scroll_width * 0.95
self._sub_height = 808.0
self._sub_height = 870.0
if self._show_always_use_internal_keyboard:
self._sub_height += 62
@ -191,7 +190,7 @@ class AdvancedSettingsWindow(bui.Window):
from bauiv1lib.settings import nettesting as _unused4
from bauiv1lib import appinvite as _unused5
from bauiv1lib import account as _unused6
from bauiv1lib import promocode as _unused7
from bauiv1lib import sendinfo as _unused7
from bauiv1lib import debug as _unused8
from bauiv1lib.settings import plugins as _unused9
from bauiv1lib.settings import moddingtools as _unused10
@ -289,33 +288,16 @@ class AdvancedSettingsWindow(bui.Window):
this_button_width = 410
self._promo_code_button = bui.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
size=(this_button_width, 60),
autoselect=True,
label=bui.Lstr(resource=f'{self._r}.enterPromoCodeText'),
text_scale=1.0,
on_activate_call=self._on_promo_code_press,
)
if self._back_button is not None:
bui.widget(
edit=self._promo_code_button,
up_widget=self._back_button,
left_widget=self._back_button,
)
v -= self._extra_button_spacing * 0.8
assert bui.app.classic is not None
bui.textwidget(
parent=self._subcontainer,
position=(200, v + 10),
position=(70, v + 10),
size=(0, 0),
text=bui.Lstr(resource=f'{self._r}.languageText'),
maxwidth=150,
scale=0.95,
scale=1.2,
color=bui.app.ui_v1.title_color,
h_align='right',
h_align='left',
v_align='center',
)
@ -394,7 +376,7 @@ class AdvancedSettingsWindow(bui.Window):
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v + 10),
position=(90, v + 10),
size=(0, 0),
text=bui.Lstr(
resource=f'{self._r}.helpTranslateText',
@ -405,7 +387,7 @@ class AdvancedSettingsWindow(bui.Window):
flatness=1.0,
scale=0.65,
color=(0.4, 0.9, 0.4, 0.8),
h_align='center',
h_align='left',
v_align='center',
)
v -= self._spacing * 1.9
@ -436,7 +418,7 @@ class AdvancedSettingsWindow(bui.Window):
maxwidth=400.0,
)
self._update_lang_status()
v -= 40
v -= 50
lang_inform = plus.get_v1_account_misc_val('langInform', False)
@ -688,6 +670,17 @@ class AdvancedSettingsWindow(bui.Window):
on_activate_call=self._on_benchmark_press,
)
v -= 100
self._send_info_button = bui.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
size=(this_button_width, 60),
autoselect=True,
label=bui.Lstr(resource=f'{self._r}.sendInfoText'),
text_scale=1.0,
on_activate_call=self._on_send_info_press,
)
for child in self._subcontainer.get_children():
bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20)
@ -740,14 +733,6 @@ class AdvancedSettingsWindow(bui.Window):
if not self._root_widget or self._root_widget.transitioning_out:
return
# Net-testing requires a signed in v1 account.
if plus.get_v1_account_state() != 'signed_in':
bui.screenmessage(
bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
)
bui.getsound('error').play()
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
@ -801,9 +786,8 @@ class AdvancedSettingsWindow(bui.Window):
from_window=self._root_widget,
)
def _on_promo_code_press(self) -> None:
from bauiv1lib.promocode import PromoCodeWindow
from bauiv1lib.account import show_sign_in_prompt
def _on_send_info_press(self) -> None:
from bauiv1lib.sendinfo import SendInfoWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
@ -812,17 +796,12 @@ class AdvancedSettingsWindow(bui.Window):
plus = bui.app.plus
assert plus is not None
# We have to be logged in for promo-codes to work.
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PromoCodeWindow(
origin_widget=self._promo_code_button
SendInfoWindow(
origin_widget=self._send_info_button
).get_root_widget(),
from_window=self._root_widget,
)
@ -853,8 +832,8 @@ class AdvancedSettingsWindow(bui.Window):
sel_name = 'VRTest'
elif sel == self._net_test_button:
sel_name = 'NetTest'
elif sel == self._promo_code_button:
sel_name = 'PromoCode'
elif sel == self._send_info_button:
sel_name = 'SendInfo'
elif sel == self._benchmarks_button:
sel_name = 'Benchmarks'
elif sel == self._kick_idle_players_check_box.widget:
@ -924,8 +903,8 @@ class AdvancedSettingsWindow(bui.Window):
sel = self._vr_test_button
elif sel_name == 'NetTest':
sel = self._net_test_button
elif sel_name == 'PromoCode':
sel = self._promo_code_button
elif sel_name == 'SendInfo':
sel = self._send_info_button
elif sel_name == 'Benchmarks':
sel = self._benchmarks_button
elif sel_name == 'KickIdlePlayers':

View File

@ -162,6 +162,7 @@ class NetTestingWindow(bui.Window):
def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
from efro.util import utc_now
@ -248,8 +249,11 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None:
curv1addr = plus.get_master_server_address(version=1)
_print(f'\nUsing V1 address: {curv1addr}')
_print('\nRunning V1 transaction...')
_print_test_results(_test_v1_transaction)
if plus.get_v1_account_state() == 'signed_in':
_print('\nRunning V1 transaction...')
_print_test_results(_test_v1_transaction)
else:
_print('\nSkipping V1 transaction (Not signed into V1).')
# V2 ping
baseaddr = plus.get_master_server_address(version=2)

View File

@ -414,9 +414,8 @@ void BaseFeatureSet::OnAppShutdownComplete() {
assert(g_core);
assert(g_base);
g_core->LifecycleLog("app exiting (main thread)");
// Flag our own event loop to exit (or ask the OS to if they're managing).
g_core->LifecycleLog("app exiting (main thread)");
if (app_adapter->ManagesMainThreadEventLoop()) {
app_adapter->DoExitMainThreadEventLoop();
} else {

View File

@ -588,10 +588,12 @@ enum class SysMeshID : uint8_t {
};
// Our feature-set's globals.
// Feature-sets should NEVER directly access globals in another feature-set's
// namespace. All functionality we need from other feature-sets should be
// imported into globals in our own namespace. Generally we do this when we
// are initially imported (just as regular Python modules do).
//
// Feature-sets should NEVER directly access globals in another
// feature-set's namespace. All functionality we need from other
// feature-sets should be imported into globals in our own namespace.
// Generally we do this when we are initially imported (just as regular
// Python modules do).
extern core::CoreFeatureSet* g_core;
extern base::BaseFeatureSet* g_base;
@ -653,8 +655,6 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// their own event loop).
void RunAppToCompletion() override;
// void PrimeAppMainThreadEventPump() override;
auto CurrentContext() -> const ContextRef& {
assert(InLogicThread()); // Up to caller to ensure this.
return *context_ref;
@ -663,6 +663,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// Utility call to print 'Success!' with a happy sound.
/// Safe to call from any thread.
void SuccessScreenMessage();
/// Utility call to print 'Error.' with a beep sound.
/// Safe to call from any thread.
void ErrorScreenMessage();

View File

@ -32,7 +32,7 @@ typedef unsigned int GLenum;
typedef int GLint;
#define KTX_IDENTIFIER_REF \
{ 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }
{0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A}
#define KTX_ENDIAN_REF (0x04030201)
#define KTX_ENDIAN_REF_REV (0x01020304)
#define KTX_HEADER_SIZE (64)

View File

@ -4,6 +4,11 @@
#include <csignal>
#if !BA_OSTYPE_WINDOWS
#include <fcntl.h>
#include <poll.h>
#endif
#include "ballistica/base/base.h"
#include "ballistica/base/input/input.h"
#include "ballistica/base/logic/logic.h"
@ -229,4 +234,101 @@ void BasePlatform::OpenFileExternally(const std::string& path) {
Log(LogLevel::kError, "OpenFileExternally() unimplemented");
}
void BasePlatform::SafeStdinFGetSInit() {
#if BA_OSTYPE_WINDOWS
// Do nothing on Windows. We seem to be ok with blocking reads there.
#else
// Actually should not be necessary now that we're using poll().
// Set stdin up for non-blocking reads.
// int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
// fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
#endif // BA_OSTYPE_WINDOWS
}
auto BasePlatform::SafeStdinFGetS(char* s, int n, FILE* iop) -> char* {
#if BA_OSTYPE_WINDOWS
// Use plain old vanilla fgets on Windows since blocking stdin reads
// don't seem to prevent the app from exiting there.
return fgets(s, n, iop);
#else
// On unixy platforms, plug in a vanilla fgets() implementation (see
// https://stackoverflow.com/questions/16397832/fgets-implementation-kr)
// but replace the getc() with a custom version of our own that uses
// poll() to periodically check if we should bail while waiting for input.
int c{};
char* cs{};
cs = s;
while (--n > 0 && (c = SmartGetC_(iop)) != EOF) {
if ((*cs++ = c) == '\n') {
break;
}
}
*cs = '\0';
return (c == EOF && cs == s) ? NULL : s;
#endif // BA_OSTYPE_WINDOWS
}
int BasePlatform::SmartGetC_(FILE* stream) {
#if BA_OSTYPE_WINDOWS
return -1;
#else
// Refill our buffer if needed.
while (stdin_buffer_.empty()) {
struct pollfd fds[1];
// Initialize the pollfd structure for stdin
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
// Let's break approximately 4 times per second to see if we should
// bail.
int ret = poll(fds, 1, 287);
if (ret == 0) {
// Poll timed out. Check whether we should bail and then do it again.
// If the app is working on gracefully shutting down OR the engine has
// died (from a fatal error or whatever else), fake an EOF.
if (g_base->logic->shutting_down() || g_core->engine_done()) {
return EOF;
}
continue;
}
if (ret == -1) {
// Error in poll
perror("poll");
return EOF;
}
if (fds[0].revents & POLLIN) {
// stdin is ready for reading.
char buffer[256];
// Read characters from stdin
ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
if (bytes_read == -1) {
// Error reading from stdin
perror("read");
return EOF;
}
for (int i = 0; i < bytes_read; ++i) {
stdin_buffer_.push_back(buffer[i]);
}
}
}
auto out = stdin_buffer_.front();
stdin_buffer_.pop_front();
return out;
#endif // BA_OSTYPE_WINDOWS
}
} // namespace ballistica::base

View File

@ -3,6 +3,8 @@
#ifndef BALLISTICA_BASE_PLATFORM_BASE_PLATFORM_H_
#define BALLISTICA_BASE_PLATFORM_BASE_PLATFORM_H_
#include <deque>
#include "ballistica/base/base.h"
#include "ballistica/shared/python/python_ref.h"
@ -33,14 +35,20 @@ class BasePlatform {
virtual void OnScreenSizeChange();
virtual void DoApplyAppConfig();
/// Prepares stdin reading that won't block process exit.
virtual void SafeStdinFGetSInit();
/// Equivalent of fgets() but modified to not block process exit.
auto SafeStdinFGetS(char* s, int n, FILE* iop) -> char*;
#pragma mark IN APP PURCHASES --------------------------------------------------
void Purchase(const std::string& item);
// Restore purchases (currently only relevant on Apple platforms).
/// Restore purchases (currently only relevant on Apple platforms).
virtual void RestorePurchases();
// Purchase was ack'ed by the master-server (so can consume).
/// Purchase was ack'ed by the master-server (so can consume).
virtual void PurchaseAck(const std::string& purchase,
const std::string& order_id);
@ -119,9 +127,12 @@ class BasePlatform {
virtual ~BasePlatform();
private:
int SmartGetC_(FILE* stream);
bool ran_base_post_init_{};
PythonRef string_edit_adapter_{};
std::string public_device_uuid_;
std::deque<char> stdin_buffer_;
};
} // namespace ballistica::base

View File

@ -7,6 +7,7 @@
#include "ballistica/base/app_adapter/app_adapter.h"
#include "ballistica/base/app_mode/app_mode.h"
#include "ballistica/base/logic/logic.h"
#include "ballistica/base/platform/base_platform.h"
#include "ballistica/base/support/context.h"
#include "ballistica/core/platform/core_platform.h"
#include "ballistica/shared/foundation/event_loop.h"
@ -18,10 +19,10 @@ namespace ballistica::base {
StdioConsole::StdioConsole() = default;
void StdioConsole::Start() {
g_base->app_adapter->PushMainThreadCall([this] { StartInMainThread(); });
g_base->app_adapter->PushMainThreadCall([this] { StartInMainThread_(); });
}
void StdioConsole::StartInMainThread() {
void StdioConsole::StartInMainThread_() {
assert(g_core && g_core->InMainThread());
// Spin up our thread.
@ -32,10 +33,12 @@ void StdioConsole::StartInMainThread() {
event_loop()->PushCall([this] {
bool stdin_is_terminal = g_core->platform->is_stdin_a_terminal();
g_base->platform->SafeStdinFGetSInit();
while (true) {
// Print a prompt if we're a tty.
// We send this to the logic thread so it happens AFTER the
// results of the last script-command message we may have just sent.
// Print a prompt if we're a tty. We send this to the logic thread so
// it happens AFTER the results of the last script-command message we
// may have just sent.
if (stdin_is_terminal) {
g_base->logic->event_loop()->PushCall([] {
if (!g_base->logic->shutting_down()) {
@ -45,62 +48,57 @@ void StdioConsole::StartInMainThread() {
});
}
// Was using getline, but switched to
// new fgets based approach (more portable).
// Ideally at some point we can wire up to the Python api to get behavior
// more like the actual Python command line.
// Was using getline, but switched to new fgets based approach (more
// portable). Ideally at some point we can wire up to the Python api
// to get behavior more like the actual Python command line.
char buffer[4096];
char* val = fgets(buffer, sizeof(buffer), stdin);
char* val;
// Use our fancy safe version of fgets(); on some platforms this will
// return a fake EOF once the app/engine starts going down. This
// avoids some scenarios where regular blocking fgets() prevents the
// process from exiting (until they press Ctrl-D in the terminal).
if (explicit_bool(true)) {
val = g_base->platform->SafeStdinFGetS(buffer, sizeof(buffer), stdin);
} else {
val = fgets(buffer, sizeof(buffer), stdin);
}
if (val) {
if (val == std::string("@clear\n")) {
int retval{-1};
if (g_buildconfig.ostype_macos() || g_buildconfig.ostype_linux()) {
// Attempt to run actual clear command on unix-y systems to
// plop our prompt back at the top of the screen.
retval = core::CorePlatform::System("clear");
}
// As a fallback, just spit out a bunch of newlines.
if (retval != 0) {
std::string space;
for (int i = 0; i < 100; ++i) {
space += "\n";
}
printf("%s", space.c_str());
}
continue;
}
pending_input_ += val;
if (!pending_input_.empty()
&& pending_input_[pending_input_.size() - 1] == '\n') {
// Get rid of the last newline and ship it to the game.
pending_input_.pop_back();
PushCommand(pending_input_);
// Handle special cases ourself.
if (pending_input_ == std::string("@clear")) {
Clear_();
} else {
// Otherwise ship it off to the engine to run.
PushCommand_(pending_input_);
}
pending_input_.clear();
}
} else {
// At the moment we bail on any read error.
if (feof(stdin)) {
if (stdin_is_terminal) {
// Ok this is strange: on windows consoles, it seems that Ctrl-C in
// a terminal immediately closes our stdin even if we catch the
// interrupt, and then our python interrupt handler runs a moment
// later. This means we wind up telling the user that EOF was
// reached and they should Ctrl-C to quit right after they've hit
// Ctrl-C to quit. To hopefully avoid this, let's hold off on the
// print for a second and see if a shutdown has begun first.
// (or, more likely, just never print because the app has exited).
if (g_buildconfig.windows_console_build()) {
core::CorePlatform::SleepMillisecs(250);
}
if (!g_base->logic->shutting_down()) {
printf("Stdin EOF reached. Use Ctrl-C to quit.\n");
fflush(stdout);
}
// Bail on any error (could be actual EOF or one of our fake ones).
if (stdin_is_terminal) {
// Ok this is strange: on windows consoles, it seems that Ctrl-C
// in a terminal immediately closes our stdin even if we catch
// the interrupt, and then our Python interrupt handler runs a
// moment later. This means we wind up telling the user that EOF
// was reached and they should Ctrl-C to quit right after
// they've hit Ctrl-C to quit. To hopefully avoid this, let's
// hold off on the print for a second and see if a shutdown has
// begun first. (or, more likely, just never print because the
// app has exited).
if (g_buildconfig.windows_console_build()) {
core::CorePlatform::SleepMillisecs(250);
}
if (!g_base->logic->shutting_down()) {
printf("Stdin EOF reached. Use Ctrl-C to quit.\n");
fflush(stdout);
}
} else {
Log(LogLevel::kError, "StdioConsole got non-eof error reading stdin: "
+ std::to_string(ferror(stdin)));
}
break;
}
@ -108,7 +106,24 @@ void StdioConsole::StartInMainThread() {
});
}
void StdioConsole::PushCommand(const std::string& command) {
void StdioConsole::Clear_() {
int retval{-1};
if (g_buildconfig.ostype_macos() || g_buildconfig.ostype_linux()) {
// Attempt to run actual clear command on unix-y systems to plop
// our prompt back at the top of the screen.
retval = core::CorePlatform::System("clear");
}
// As a fallback, just spit out a bunch of newlines.
if (retval != 0) {
std::string space;
for (int i = 0; i < 100; ++i) {
space += "\n";
}
printf("%s", space.c_str());
}
}
void StdioConsole::PushCommand_(const std::string& command) {
g_base->logic->event_loop()->PushCall([command] {
// These are always run in whichever context is 'visible'.
ScopedSetContext ssc(g_base->app_mode()->GetForegroundContext());

View File

@ -3,6 +3,8 @@
#ifndef BALLISTICA_BASE_SUPPORT_STDIO_CONSOLE_H_
#define BALLISTICA_BASE_SUPPORT_STDIO_CONSOLE_H_
#include <deque>
#include "ballistica/shared/ballistica.h"
namespace ballistica::base {
@ -14,8 +16,9 @@ class StdioConsole {
auto event_loop() const -> EventLoop* { return event_loop_; }
private:
void StartInMainThread();
void PushCommand(const std::string& command);
void StartInMainThread_();
void PushCommand_(const std::string& command);
void Clear_();
EventLoop* event_loop_{};
std::string pending_input_;
};

View File

@ -92,15 +92,35 @@ auto ClassicPython::GetControllerFloatValue(
auto ClassicPython::BuildPublicPartyStateVal() -> PyObject* {
auto* appmode = scene_v1::SceneV1AppMode::GetActiveOrThrow();
auto&& public_ipv4 = appmode->public_party_public_address_ipv4();
PyObject* ipv4obj;
if (public_ipv4.has_value()) {
ipv4obj = PyUnicode_FromString(public_ipv4->c_str());
} else {
ipv4obj = Py_None;
Py_INCREF(ipv4obj);
}
auto&& public_ipv6 = appmode->public_party_public_address_ipv6();
PyObject* ipv6obj;
if (public_ipv6.has_value()) {
ipv6obj = PyUnicode_FromString(public_ipv6->c_str());
} else {
ipv6obj = Py_None;
Py_INCREF(ipv6obj);
}
return Py_BuildValue(
"(iiiiisssi)", static_cast<int>(appmode->public_party_enabled()),
"(iiiiisssiOO)", static_cast<int>(appmode->public_party_enabled()),
appmode->public_party_size(), appmode->public_party_max_size(),
appmode->public_party_player_count(),
appmode->public_party_max_player_count(),
appmode->public_party_name().c_str(),
appmode->public_party_min_league().c_str(),
appmode->public_party_stats_url().c_str(),
static_cast<int>(appmode->public_party_queue_enabled()));
static_cast<int>(appmode->public_party_queue_enabled()), ipv4obj,
ipv6obj);
}
} // namespace ballistica::classic

View File

@ -150,6 +150,13 @@ class CoreFeatureSet {
/// Should be called by a thread before it exits.
void UnregisterThread();
/// A bool set just before returning from main or calling exit() or
/// whatever is intended to be the last gasp of life for the binary. This
/// can be polled periodically by background threads that may otherwise
/// keep the process from exiting.
auto engine_done() const { return engine_done_; }
void set_engine_done() { engine_done_ = true; }
// Subsystems.
CorePython* const python;
CorePlatform* const platform;
@ -195,6 +202,7 @@ class CoreFeatureSet {
bool have_ba_env_vals_{};
bool vr_mode_{};
bool using_custom_app_python_dir_{};
bool engine_done_{};
std::thread::id main_thread_id_{};
CoreConfig core_config_;

View File

@ -216,6 +216,74 @@ static PyMethodDef PySetPublicPartyQueueEnabledDef = {
"(internal)",
};
// ----------------- set_public_party_public_address_ipv4 ----------------------
static auto PySetPublicPartyPublicAddressIPV4(PyObject* self, PyObject* args,
PyObject* keywds) -> PyObject* {
BA_PYTHON_TRY;
PyObject* address_obj;
static const char* kwlist[] = {"address", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O",
const_cast<char**>(kwlist), &address_obj)) {
return nullptr;
}
auto* appmode = SceneV1AppMode::GetActiveOrThrow();
// The call expects an empty string for the no-url option.
std::optional<std::string> address{};
if (address_obj != Py_None) {
address = Python::GetPyString(address_obj);
}
appmode->set_public_party_public_address_ipv4(address);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
static PyMethodDef PySetPublicPartyPublicAddressIPV4Def = {
"set_public_party_public_address_ipv4", // name
(PyCFunction)PySetPublicPartyPublicAddressIPV4, // method
METH_VARARGS | METH_KEYWORDS, // flags
"set_public_party_public_address_ipv4(address: str | None) -> None\n"
"\n"
"(internal)",
};
// ----------------- set_public_party_public_address_ipv6 ----------------------
static auto PySetPublicPartyPublicAddressIPV6(PyObject* self, PyObject* args,
PyObject* keywds) -> PyObject* {
BA_PYTHON_TRY;
PyObject* address_obj;
static const char* kwlist[] = {"address", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O",
const_cast<char**>(kwlist), &address_obj)) {
return nullptr;
}
auto* appmode = SceneV1AppMode::GetActiveOrThrow();
// The call expects an empty string for the no-url option.
std::optional<std::string> address{};
if (address_obj != Py_None) {
address = Python::GetPyString(address_obj);
}
appmode->set_public_party_public_address_ipv6(address);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
static PyMethodDef PySetPublicPartyPublicAddressIPV6Def = {
"set_public_party_public_address_ipv6", // name
(PyCFunction)PySetPublicPartyPublicAddressIPV6, // method
METH_VARARGS | METH_KEYWORDS, // flags
"set_public_party_public_address_ipv6(address: str | None) -> None\n"
"\n"
"(internal)",
};
// ------------------------ set_authenticate_clients ---------------------------
static auto PySetAuthenticateClients(PyObject* self, PyObject* args,
@ -834,6 +902,8 @@ auto PythonMethodsNetworking::GetMethods() -> std::vector<PyMethodDef> {
PyGetConnectionToHostInfo2Def,
PyClientInfoQueryResponseDef,
PyConnectToPartyDef,
PySetPublicPartyPublicAddressIPV4Def,
PySetPublicPartyPublicAddressIPV6Def,
PySetAuthenticateClientsDef,
PySetAdminsDef,
PySetEnableDefaultKickVotingDef,

View File

@ -191,6 +191,22 @@ class SceneV1AppMode : public base::AppMode {
return host_protocol_version_;
}
auto public_party_public_address_ipv4() const {
return public_party_public_address_ipv4_;
}
void set_public_party_public_address_ipv4(
const std::optional<std::string>& val) {
public_party_public_address_ipv4_ = val;
}
auto public_party_public_address_ipv6() const {
return public_party_public_address_ipv6_;
}
void set_public_party_public_address_ipv6(
const std::optional<std::string>& val) {
public_party_public_address_ipv6_ = val;
}
private:
SceneV1AppMode();
void PruneScanResults_();
@ -264,6 +280,8 @@ class SceneV1AppMode : public base::AppMode {
std::list<std::pair<millisecs_t, PlayerSpec> > banned_players_;
std::optional<float> idle_exit_minutes_{};
std::optional<uint32_t> internal_music_play_id_{};
std::optional<std::string> public_party_public_address_ipv4_{};
std::optional<std::string> public_party_public_address_ipv6_{};
};
} // namespace ballistica::scene_v1

View File

@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
namespace ballistica {
// These are set automatically via script; don't modify them here.
const int kEngineBuildNumber = 21827;
const int kEngineBuildNumber = 21848;
const char* kEngineVersion = "1.7.35";
const int kEngineApiVersion = 8;
@ -79,6 +79,10 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
bool success = PythonCommand(*l_core->core_config().call_command,
"<ballistica app 'command' arg>")
.Exec(true, nullptr, nullptr);
// Let anyone interested know we're trying to go down NOW.
l_core->set_engine_done();
exit(success ? 0 : 1);
}
@ -141,6 +145,8 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
if (l_base->AppManagesMainThreadEventLoop()) {
// In environments where we control the event loop, do that.
l_base->RunAppToCompletion();
// Let anyone interested know we're trying to go down NOW.
l_core->set_engine_done();
} else {
// If the environment is managing events, we now simply return and let
// it feed us those events.
@ -171,6 +177,10 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// If it's not been handled, take the app down ourself.
if (!handled) {
// Let anyone interested know we're trying to go down NOW.
if (l_core) {
l_core->set_engine_done();
}
if (try_to_exit_cleanly) {
exit(1);
} else {

View File

@ -205,6 +205,13 @@ auto FatalError::HandleFatalError(bool exit_cleanly,
if (!in_top_level_exception_handler) {
if (exit_cleanly) {
Logging::EmitLog("root", LogLevel::kCritical, "Calling exit(1)...");
// Inform anyone who cares that the engine is going down NOW.
// This value can be polled by threads that may otherwise block us
// from exiting cleanly. As an example, I've seen recent linux builds
// hang on exit because a bg thread is blocked in a read of stdin.
g_core->set_engine_done();
exit(1);
} else {
Logging::EmitLog("root", LogLevel::kCritical, "Calling abort()...");

File diff suppressed because it is too large Load Diff

View File

@ -122,24 +122,25 @@ class TestResponse(Response):
@ioprepped
@dataclass
class PromoCodeMessage(Message):
"""User is entering a promo code"""
class SendInfoMessage(Message):
"""User is using the send-info function"""
code: Annotated[str, IOAttrs('c')]
description: Annotated[str, IOAttrs('c')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [PromoCodeResponse]
return [SendInfoResponse]
@ioprepped
@dataclass
class PromoCodeResponse(Response):
"""Applied that promo code for ya, boss."""
class SendInfoResponse(Response):
"""Response to sending into the server."""
valid: Annotated[bool, IOAttrs('v')]
handled: Annotated[bool, IOAttrs('v')]
message: Annotated[str | None, IOAttrs('m', store_default=False)] = None
legacy_code: Annotated[str | None, IOAttrs('l', store_default=False)] = None
@ioprepped

View File

@ -20,6 +20,11 @@ class ServerNodeEntry:
"""Information about a specific server."""
zone: Annotated[str, IOAttrs('r')]
# TODO: Remove soft_default after all master-servers upgraded.
latlong: Annotated[
tuple[float, float] | None, IOAttrs('ll', soft_default=None)
]
address: Annotated[str, IOAttrs('a')]
port: Annotated[int, IOAttrs('p')]
@ -32,6 +37,16 @@ class ServerNodeQueryResponse:
# The current utc time on the master server.
time: Annotated[datetime.datetime, IOAttrs('t')]
# Where the master server sees the query as coming from.
latlong: Annotated[tuple[float, float] | None, IOAttrs('ll')]
ping_per_dist: Annotated[float, IOAttrs('ppd')]
max_dist: Annotated[float, IOAttrs('md')]
debug_log_seconds: Annotated[
float | None, IOAttrs('d', store_default=False)
] = None
# If present, something went wrong, and this describes it.
error: Annotated[str | None, IOAttrs('e', store_default=False)] = None
@ -78,6 +93,7 @@ class PrivatePartyConnectResult:
"""Info about a server we get back when connecting."""
error: str | None = None
addr: str | None = None
address4: Annotated[str | None, IOAttrs('addr')] = None
address6: Annotated[str | None, IOAttrs('addr6')] = None
port: int | None = None
password: str | None = None

View File

@ -22,113 +22,138 @@ class ServerConfig:
party_name: str = 'FFA'
# If True, your party will show up in the global public party list
# Otherwise it will still be joinable via LAN or connecting by IP address.
# Otherwise it will still be joinable via LAN or connecting by IP
# address.
party_is_public: bool = True
# If True, all connecting clients will be authenticated through the master
# server to screen for fake account info. Generally this should always
# be enabled unless you are hosting on a LAN with no internet connection.
# If True, all connecting clients will be authenticated through the
# master server to screen for fake account info. Generally this
# should always be enabled unless you are hosting on a LAN with no
# internet connection.
authenticate_clients: bool = True
# IDs of server admins. Server admins are not kickable through the default
# kick vote system and they are able to kick players without a vote. To get
# your account id, enter 'getaccountid' in settings->advanced->enter-code.
# IDs of server admins. Server admins are not kickable through the
# default kick vote system and they are able to kick players without
# a vote. To get your account id, enter 'getaccountid' in
# settings->advanced->enter-code.
admins: list[str] = field(default_factory=list)
# Whether the default kick-voting system is enabled.
enable_default_kick_voting: bool = True
# UDP port to host on. Change this to work around firewalls or run multiple
# servers on one machine.
# 43210 is the default and the only port that will show up in the LAN
# browser tab.
# To be included in the public server list, your server MUST be
# accessible via an ipv4 address. By default, the master server will
# try to use the address your server contacts it from, but this may
# be an ipv6 address these days so you may need to provide an ipv4
# address explicitly.
public_ipv4_address: str | None = None
# You can optionally provide an ipv6 address for your server for the
# public server list. Unlike ipv4, a server is not required to have
# an ipv6 address to appear in the list, but is still good to
# provide when available since more and more devices are using ipv6
# these days. Your server's ipv6 address will be autodetected if
# your server uses ipv6 when communicating with the master server. You
# can pass an empty string here to explicitly disable the ipv6
# address.
public_ipv6_address: str | None = None
# UDP port to host on. Change this to work around firewalls or run
# multiple servers on one machine.
#
# 43210 is the default and the only port that will show up in the
# LAN browser tab.
port: int = 43210
# Max devices in the party. Note that this does *NOT* mean max players.
# Any device in the party can have more than one player on it if they have
# multiple controllers. Also, this number currently includes the server so
# generally make it 1 bigger than you need.
# Max devices in the party. Note that this does *NOT* mean max
# players. Any device in the party can have more than one player on
# it if they have multiple controllers. Also, this number currently
# includes the server so generally make it 1 bigger than you need.
max_party_size: int = 6
# Max players that can join a session. If present this will override the
# session's preferred max_players. if a value below 0 is given player limit
# will be removed.
# Max players that can join a session. If present this will override
# the session's preferred max_players. if a value below 0 is given
# player limit will be removed.
session_max_players_override: int | None = None
# Options here are 'ffa' (free-for-all), 'teams' and 'coop' (cooperative)
# This value is ignored if you supply a playlist_code (see below).
# Options here are 'ffa' (free-for-all), 'teams' and 'coop'
# (cooperative) This value is ignored if you supply a playlist_code
# (see below).
session_type: str = 'ffa'
# Playlist-code for teams or free-for-all mode sessions.
# To host your own custom playlists, use the 'share' functionality in the
# playlist editor in the regular version of the game.
# This will give you a numeric code you can enter here to host that
# playlist.
# Playlist-code for teams or free-for-all mode sessions. To host
# your own custom playlists, use the 'share' functionality in the
# playlist editor in the regular version of the game. This will give
# you a numeric code you can enter here to host that playlist.
playlist_code: int | None = None
# Alternately, you can embed playlist data here instead of using codes.
# Make sure to set session_type to the correct type for the data here.
# Alternately, you can embed playlist data here instead of using
# codes. Make sure to set session_type to the correct type for the
# data here.
playlist_inline: list[dict[str, Any]] | None = None
# Whether to shuffle the playlist or play its games in designated order.
# Whether to shuffle the playlist or play its games in designated
# order.
playlist_shuffle: bool = True
# If True, keeps team sizes equal by disallowing joining the largest team
# (teams mode only).
# If True, keeps team sizes equal by disallowing joining the largest
# team (teams mode only).
auto_balance_teams: bool = True
# The campaign used when in co-op session mode.
# Do print(ba.app.campaigns) to see available campaign names.
# The campaign used when in co-op session mode. Do
# print(ba.app.campaigns) to see available campaign names.
coop_campaign: str = 'Easy'
# The level name within the campaign used in co-op session mode.
# For campaign name FOO, do print(ba.app.campaigns['FOO'].levels) to see
# The level name within the campaign used in co-op session mode. For
# campaign name FOO, do print(ba.app.campaigns['FOO'].levels) to see
# available level names.
coop_level: str = 'Onslaught Training'
# Whether to enable telnet access.
# IMPORTANT: This option is no longer available, as it was being used
# for exploits. Live access to the running server is still possible through
# the mgr.cmd() function in the server script. Run your server through
# tools such as 'screen' or 'tmux' and you can reconnect to it remotely
# over a secure ssh connection.
#
# IMPORTANT: This option is no longer available, as it was being
# used for exploits. Live access to the running server is still
# possible through the mgr.cmd() function in the server script. Run
# your server through tools such as 'screen' or 'tmux' and you can
# reconnect to it remotely over a secure ssh connection.
enable_telnet: bool = False
# Series length in teams mode (7 == 'best-of-7' series; a team must
# get 4 wins)
teams_series_length: int = 7
# Points to win in free-for-all mode (Points are awarded per game based on
# performance)
# Points to win in free-for-all mode (Points are awarded per game
# based on performance)
ffa_series_length: int = 24
# If you have a custom stats webpage for your server, you can use this
# to provide a convenient in-game link to it in the server-browser
# alongside the server name.
# If you have a custom stats webpage for your server, you can use
# this to provide a convenient in-game link to it in the
# server-browser alongside the server name.
#
# if ${ACCOUNT} is present in the string, it will be replaced by the
# currently-signed-in account's id. To fetch info about an account,
# your back-end server can use the following url:
# https://legacy.ballistica.net/accountquery?id=ACCOUNT_ID_HERE
stats_url: str | None = None
# If present, the server subprocess will attempt to gracefully exit after
# this amount of time. A graceful exit can occur at the end of a series
# or other opportune time. Server-managers set to auto-restart (the
# default) will then spin up a fresh subprocess. This mechanism can be
# useful to clear out any memory leaks or other accumulated bad state
# in the server subprocess.
# If present, the server subprocess will attempt to gracefully exit
# after this amount of time. A graceful exit can occur at the end of
# a series or other opportune time. Server-managers set to
# auto-restart (the default) will then spin up a fresh subprocess.
# This mechanism can be useful to clear out any memory leaks or
# other accumulated bad state in the server subprocess.
clean_exit_minutes: float | None = None
# If present, the server subprocess will shut down immediately after this
# amount of time. This can be useful as a fallback for clean_exit_time.
# The server manager will then spin up a fresh server subprocess if
# auto-restart is enabled (the default).
# If present, the server subprocess will shut down immediately after
# this amount of time. This can be useful as a fallback for
# clean_exit_time. The server manager will then spin up a fresh
# server subprocess if auto-restart is enabled (the default).
unclean_exit_minutes: float | None = None
# If present, the server subprocess will shut down immediately if this
# amount of time passes with no activity from any players. The server
# manager will then spin up a fresh server subprocess if auto-restart is
# enabled (the default).
# If present, the server subprocess will shut down immediately if
# this amount of time passes with no activity from any players. The
# server manager will then spin up a fresh server subprocess if
# auto-restart is enabled (the default).
idle_exit_minutes: float | None = None
# Should the tutorial be shown at the beginning of games?
@ -142,9 +167,9 @@ class ServerConfig:
tuple[tuple[float, float, float], tuple[float, float, float]] | None
) = None
# Whether to enable the queue where players can line up before entering
# your server. Disabling this can be used as a workaround to deal with
# queue spamming attacks.
# Whether to enable the queue where players can line up before
# entering your server. Disabling this can be used as a workaround
# to deal with queue spamming attacks.
enable_queue: bool = True
# Protocol version we host with. Currently the default is 33 which
@ -162,9 +187,9 @@ class ServerConfig:
player_rejoin_cooldown: float = 10.0
# NOTE: as much as possible, communication from the server-manager to the
# child-process should go through these and not ad-hoc Python string commands
# since this way is type safe.
# NOTE: as much as possible, communication from the server-manager to
# the child-process should go through these and not ad-hoc Python string
# commands since this way is type safe.
class ServerCommand:
"""Base class for commands that can be sent to the server."""

View File

@ -465,6 +465,8 @@ def _get_server_config_template_toml(projroot: str) -> str:
cfg.playlist_inline = []
cfg.team_names = ('Red', 'Blue')
cfg.team_colors = ((0.1, 0.25, 1.0), (1.0, 0.25, 0.2))
cfg.public_ipv4_address = '123.123.123.123'
cfg.public_ipv6_address = '123A::A123:23A1:A312:12A3:A213:2A13'
lines_in = _get_server_config_raw_contents(projroot).splitlines()

View File

@ -32,6 +32,7 @@ from efro.dataclassio._api import (
dataclass_from_dict,
dataclass_from_json,
dataclass_validate,
dataclass_hash,
)
__all__ = [
@ -47,6 +48,7 @@ __all__ = [
'dataclass_to_dict',
'dataclass_to_json',
'dataclass_validate',
'dataclass_hash',
'ioprep',
'ioprepped',
'is_ioprepped_dataclass',

View File

@ -10,6 +10,7 @@ data formats in a nondestructive manner.
from __future__ import annotations
import json
from enum import Enum
from typing import TYPE_CHECKING, TypeVar
@ -79,7 +80,6 @@ def dataclass_to_json(
By default, keys are sorted for pretty output and not otherwise, but
this can be overridden by supplying a value for the 'sort_keys' arg.
"""
import json
jdict = dataclass_to_dict(
obj=obj, coerce_to_float=coerce_to_float, codec=Codec.JSON
@ -142,11 +142,10 @@ def dataclass_from_json(
allow_unknown_attrs: bool = True,
discard_unknown_attrs: bool = False,
) -> T:
"""Utility function; return a dataclass instance given a json string.
"""Return a dataclass instance given a json string.
Basically dataclass_from_dict(json.loads(...))
"""
import json
return dataclass_from_dict(
cls=cls,
@ -167,3 +166,27 @@ def dataclass_validate(
_Outputter(
obj, create=False, codec=codec, coerce_to_float=coerce_to_float
).run()
def dataclass_hash(obj: Any, coerce_to_float: bool = True) -> str:
"""Calculate a hash for the provided dataclass.
Basically this emits json for the dataclass (with keys sorted
to keep things deterministic) and hashes the resulting string.
"""
import hashlib
from base64 import urlsafe_b64encode
json_dict = dataclass_to_dict(
obj, codec=Codec.JSON, coerce_to_float=coerce_to_float
)
# Need to sort keys to keep things deterministic.
json_str = json.dumps(json_dict, separators=(',', ':'), sort_keys=True)
sha = hashlib.sha256()
sha.update(json_str.encode())
# Go with urlsafe base64 instead of the usual hex to save some
# space, and kill those ugly padding chars at the end.
return urlsafe_b64encode(sha.digest()).decode().strip('=')

View File

@ -417,6 +417,8 @@ class DeadlockWatcher:
logger: Logger | None = None,
logextra: dict | None = None,
) -> None:
from efro.util import caller_source_location
# pylint: disable=not-context-manager
cls = type(self)
if cls.watchers_lock is None or cls.watchers is None:
@ -433,6 +435,13 @@ class DeadlockWatcher:
self.noted_expire = False
self.logger = logger
self.logextra = logextra
self.caller_source_loc = caller_source_location()
curthread = threading.current_thread()
self.thread_id = (
'<unknown>'
if curthread.ident is None
else hex(curthread.ident).removeprefix('0x')
)
with cls.watchers_lock:
cls.watchers.append(weakref.ref(self))
@ -492,8 +501,11 @@ class DeadlockWatcher:
# should check stderr for a dump.
if w.logger is not None:
w.logger.error(
'DeadlockWatcher with time %.2f expired;'
'DeadlockWatcher at %s in thread %s'
' with time %.2f expired;'
' check stderr for stack traces.',
w.caller_source_loc,
w.thread_id,
w.timeout,
extra=w.logextra,
)

View File

@ -150,6 +150,7 @@ class LogHandler(logging.Handler):
self._cache = deque[tuple[int, LogEntry]]()
self._cache_index_offset = 0
self._cache_lock = Lock()
# self._report_blocking_io_on_echo_error = False
self._printed_callback_error = False
self._thread_bootstrapped = False
self._thread = Thread(target=self._log_thread_main, daemon=True)
@ -364,13 +365,32 @@ class LogHandler(logging.Handler):
# thread because the delay can throw off command line prompts or
# make tight debugging harder.
if self._echofile is not None:
# try:
# if self._report_blocking_io_on_echo_error:
# premsg = (
# 'WARNING: BlockingIOError ON LOG ECHO OUTPUT;'
# ' YOU ARE PROBABLY MISSING LOGS\n'
# )
# self._report_blocking_io_on_echo_error = False
# else:
# premsg = ''
ends = LEVELNO_COLOR_CODES.get(record.levelno)
namepre = f'{Clr.WHT}{record.name}:{Clr.RST} '
if ends is not None:
self._echofile.write(f'{namepre}{ends[0]}{msg}{ends[1]}\n')
self._echofile.write(
f'{namepre}{ends[0]}'
f'{msg}{ends[1]}\n'
# f'{namepre}{ends[0]}' f'{premsg}{msg}{ends[1]}\n'
)
else:
self._echofile.write(f'{namepre}{msg}\n')
self._echofile.flush()
# except BlockingIOError:
# # Ran into this when doing a bunch of logging; assuming
# # this is asyncio's doing?.. For now trying to survive
# # the error but telling the user something is probably
# # missing in their output.
# self._report_blocking_io_on_echo_error = True
if __debug__:
echotime = time.monotonic()
@ -603,9 +623,23 @@ class FileLogEcho:
self._name = name
self._handler = handler
# Think this was a result of setting non-blocking stdin somehow;
# probably not needed.
# self._report_blocking_io_error = False
def write(self, output: Any) -> None:
"""Override standard write call."""
# try:
# if self._report_blocking_io_error:
# self._report_blocking_io_error = False
# self._original.write(
# 'WARNING: BlockingIOError ENCOUNTERED;'
# ' OUTPUT IS PROBABLY MISSING'
# )
self._original.write(output)
# except BlockingIOError:
# self._report_blocking_io_error = True
self._handler.file_write(self._name, output)
def flush(self) -> None:

View File

@ -711,6 +711,27 @@ def compact_id(num: int) -> str:
)
def caller_source_location() -> str:
"""Returns source file name and line of the code calling us.
Example: 'mymodule.py:23'
"""
try:
import inspect
frame = inspect.currentframe()
for _i in range(2):
if frame is None:
raise RuntimeError()
frame = frame.f_back
if frame is None:
raise RuntimeError()
fname = os.path.basename(frame.f_code.co_filename)
return f'{fname}:{frame.f_lineno}'
except Exception:
return '<unknown source location>'
def unchanging_hostname() -> str:
"""Return an unchanging name for the local device.