mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-19 13:25:31 +08:00
1.7.35 work in progress
This commit is contained in:
parent
5ebdccb2b4
commit
671b26efa7
128
.efrocachemap
generated
128
.efrocachemap
generated
@ -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",
|
||||
|
||||
42
CHANGELOG.md
42
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)),
|
||||
(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
@ -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':
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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_;
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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_;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()...");
|
||||
|
||||
1551
src/external/httprequest/httprequest.hpp
vendored
1551
src/external/httprequest/httprequest.hpp
vendored
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."""
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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('=')
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user