mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-02-06 15:34:42 +08:00
enabling workspaces
This commit is contained in:
parent
670bd9d4fd
commit
616dd21265
@ -420,7 +420,7 @@
|
|||||||
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/dc/d2/160fc27fcaff10793327ac2c70fd",
|
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/dc/d2/160fc27fcaff10793327ac2c70fd",
|
||||||
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/11/7a/87d6bca0acfb877fd4fd8fe3a598",
|
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/11/7a/87d6bca0acfb877fd4fd8fe3a598",
|
||||||
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/44/f5/c943c9075abb3e1835d2408a1ef8",
|
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/44/f5/c943c9075abb3e1835d2408a1ef8",
|
||||||
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/74/3f/0f47e4d22a9f17adc91ed1a9d426",
|
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/01/df/c8793a0f23e024f44ff5203122f1",
|
||||||
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/ca/75/3de74bd6e498113b99bbf9eda645",
|
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/ca/75/3de74bd6e498113b99bbf9eda645",
|
||||||
"assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/61/03/89070ca765e06da3a419a579f503",
|
"assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/61/03/89070ca765e06da3a419a579f503",
|
||||||
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/61/ee/f19a0aacec20e49dd16c206b6c69",
|
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/61/ee/f19a0aacec20e49dd16c206b6c69",
|
||||||
@ -429,17 +429,17 @@
|
|||||||
"assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/87/84/9f3d39610453b3bf350698a23316",
|
"assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/87/84/9f3d39610453b3bf350698a23316",
|
||||||
"assets/build/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/3f/46/e4da3c1d2b0ebf916df55c608b28",
|
"assets/build/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/3f/46/e4da3c1d2b0ebf916df55c608b28",
|
||||||
"assets/build/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/68/93/da8e9874f41a786edf52ba4ccaad",
|
"assets/build/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/68/93/da8e9874f41a786edf52ba4ccaad",
|
||||||
"assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/99/2a/bdcfa0932cf73e5cf63fd8113b1b",
|
"assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/e2/7e/d09eefebb683de87d3845f1f74f4",
|
||||||
"assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/4c/c7/0184b8178869d1a3827a1bfcd5bb",
|
"assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/4c/c7/0184b8178869d1a3827a1bfcd5bb",
|
||||||
"assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/de/5c/631a09d9192e40c99c07c6191b7c",
|
"assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/de/5c/631a09d9192e40c99c07c6191b7c",
|
||||||
"assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/b6/e0/37dd30b686f475733ccc4b3cab49",
|
"assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/b6/e0/37dd30b686f475733ccc4b3cab49",
|
||||||
"assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/20/3f/198dcc5cfed5789042e1595bd048",
|
"assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/20/3f/198dcc5cfed5789042e1595bd048",
|
||||||
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/03/6a/4db89c5bf1ced8eb5a5615a4ae64",
|
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/f9/c2/d9889ac09067d7f073f5ee005740",
|
||||||
"assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/82/eb/37ff44af76812097f9c98f05c730",
|
"assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/82/eb/37ff44af76812097f9c98f05c730",
|
||||||
"assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/08/3b/68cea4d16f7020d932829af85323",
|
"assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/08/3b/68cea4d16f7020d932829af85323",
|
||||||
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/b0/48/e1ebe08bfdfc94fcb61a16b851e5",
|
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/b0/48/e1ebe08bfdfc94fcb61a16b851e5",
|
||||||
"assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/75/70/e33e6ee95830052e8f36cd2135f7",
|
"assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/75/70/e33e6ee95830052e8f36cd2135f7",
|
||||||
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/c7/16/e31ce16d1b4150c271401669f24f",
|
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/94/2d/4f17fc4b73260e99453ee3122c0c",
|
||||||
"assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/07/37/ab65ccee3a555bd40e9661860c58",
|
"assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/07/37/ab65ccee3a555bd40e9661860c58",
|
||||||
"assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/02/ab/e310f81582b6dc2ae93348d45166",
|
"assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/02/ab/e310f81582b6dc2ae93348d45166",
|
||||||
"assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/d5/fe/422745cdbe51ccb4f2ced6f5554a",
|
"assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/d5/fe/422745cdbe51ccb4f2ced6f5554a",
|
||||||
@ -450,7 +450,7 @@
|
|||||||
"assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/f9/4b/d9f01814224066856695452ef57c",
|
"assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/f9/4b/d9f01814224066856695452ef57c",
|
||||||
"assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/87/5d/d36a8a2e9cb0f02731a3fd7af000",
|
"assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/87/5d/d36a8a2e9cb0f02731a3fd7af000",
|
||||||
"assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/91/0a/35c4baf539d5951fc03a794c0e0b",
|
"assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/91/0a/35c4baf539d5951fc03a794c0e0b",
|
||||||
"assets/build/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/97/40/ed284d7ae135b9aff73295cf1f9c",
|
"assets/build/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/fe/f8/0f2b437ae04a7a7126db98d752fe",
|
||||||
"assets/build/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/9d/51/f699dbd4beb88bc3cff699a287a7",
|
"assets/build/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/9d/51/f699dbd4beb88bc3cff699a287a7",
|
||||||
"assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/0a/4f/90fcd63bd12a7648b2a1e9b01586",
|
"assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/0a/4f/90fcd63bd12a7648b2a1e9b01586",
|
||||||
"assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/7f/bb/6239adeb551be5e09f3457d7b411",
|
"assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/7f/bb/6239adeb551be5e09f3457d7b411",
|
||||||
@ -3992,50 +3992,50 @@
|
|||||||
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
|
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
|
||||||
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34",
|
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34",
|
||||||
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
|
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
|
||||||
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/5e/d0/559c7203ab5e64e2425b971162b9",
|
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/3a/f5/0452e780221e2532926f21a2d0e8",
|
||||||
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/dc/e6/cdef333cc7117b19a5e10ed36de0",
|
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c6/35/10bcc2d9f1e00a74c078fc8629b0",
|
||||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/02/03/c4f8ec99521e7e17676b248cf4aa",
|
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ee/f2/e11b8729e97a1510a26824e7e524",
|
||||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/09/ac/e9ff2d475c39d838ed1841e38616",
|
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/80/b9/e5d4926246c0d4af6e960764b15a",
|
||||||
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/13/2a/853133ad6f735dc18f02474c1ef4",
|
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/89/80/7ad1910183297112e330b7c7084e",
|
||||||
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a7/d7/ef66ddf0d143691238b6a6f380ca",
|
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/82/21/da5e39f43bfc17b4359df24c74ac",
|
||||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/77/20/c42c00d6d900cfa93b4be9c0d272",
|
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7e/52/d0ef2911ac29e6ebe975ac9ad530",
|
||||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b5/2c/393c5a2e020f8ba53c4ee6d64273",
|
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/bd/63/cee7a7dc0254a7581c999eade01d",
|
||||||
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f9/be/a4637598a814486b22a63cc73d73",
|
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9a/ef/7afd8c004af35a8d1abca6297173",
|
||||||
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/19/97/b576a0683093a41ca9de4ded529c",
|
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b4/c6/acabf51f5f726884a5578eedb541",
|
||||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d5/1e/fff6a170c50f40b21ce89cfba0c6",
|
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/56/9d/2ada3cb96ba96f1deaef17e3c010",
|
||||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e5/5e/3aae36d786cf5fceeb92ef08be09",
|
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d0/ae/86503d64be9a5bac79a1d3a803b8",
|
||||||
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/94/cc/380b688c36b5ab71d58c2ffadce8",
|
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d7/9e/a67204955a89c53edd9d06f20ede",
|
||||||
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c3/dc/f1bbcd1052ca7625108cdf5ea651",
|
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/35/5e/5d36c2e57c4e5187d34db3d6b238",
|
||||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/19/bb/92ab2df3c40578d6b55439af3a7a",
|
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/84/82/aa8d5e3eeb709bb1cbc993b463d4",
|
||||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1b/c6/48b825be351cd70902713413484f",
|
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a8/dc/c711def084cef4494339f142ffae",
|
||||||
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/b0/d5/046011a1c101ff4444f258757650",
|
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/8d/9a/63957973d87bd83310ed1bca4869",
|
||||||
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/bc/be/52a4a6f939cda096692e9d9e9420",
|
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/38/d4/444ccd05b95225d7e0f94e6e647a",
|
||||||
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/7d/5e/e174ad517b1b56aeda59cc946d23",
|
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/2c/04/46698326a0a1363bff09c33930df",
|
||||||
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/6d/da/8deb6def85dee6da1df55ab0d61e",
|
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/9e/2a/f5d9a3f66b5132796f711c625618",
|
||||||
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7a/d7/9868328009e75fd5262b750b2f9f",
|
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/22/b3/1dbcde06de6e6107f05bc054c63f",
|
||||||
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b9/e0/488348d0a48c6d2eaec9f166a68a",
|
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/64/e2/0ed77a64f73a88087b3e59afe068",
|
||||||
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e4/c6/bd920458d567ff5519cc402ef0bc",
|
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f6/4d/a6df77b6554a6d2fcce0da8cd6f9",
|
||||||
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/97/3e/8d862f251f36e3d43f83372e52e8",
|
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/39/15/b5f145e0f448b44488a459437eed",
|
||||||
"build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/15/1d/82e224d5a165e01e0a2b675a9f3d",
|
"build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/36/c4/dd9af330599b72e1de2fe26f591f",
|
||||||
"build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/70/28/c5da6acadd31a12098c0d90ced65",
|
"build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d8/4d/cd07ed88ac012e0a2814ccce55b0",
|
||||||
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c2/dd/6b8bbbe6cd25382a64d114f8b985",
|
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e8/9c/26d18be4d4f732b30cfaf00839d7",
|
||||||
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/73/16/fffb5234d28b5f331db364c1cf6b",
|
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c6/ed/823544fb91e49f7b00aaee549c1d",
|
||||||
"build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3d/e8/b425dd250a1ae01a4677baff7582",
|
"build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3f/5f/acc2a8b7faeece7d3eb4bd3e85db",
|
||||||
"build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b5/aa/a7fd6fbe303107be212ca18d19da",
|
"build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/33/a9/df1834efe475f2b074e0385cf43a",
|
||||||
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/bb/da/d37b08e2130ac35ae4857948ce6f",
|
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/66/ab/53203f38e7e00e464a00cd08fd81",
|
||||||
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8b/e7/a4f4fb02098dce00f89214003293",
|
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ee/6d/2d5d14ce2fd784ecd9008d635041",
|
||||||
"build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e1/f7/a49851aac3e3fa58da5471ae9db8",
|
"build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d3/d8/f4c1aa22d06c293afc699eccbfaf",
|
||||||
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0a/aa/ac941fe32bcdfa985949c5e5bf79",
|
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/96/47/a95bf58c8ee9b1a6cf16c0bce38e",
|
||||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/36/f4/fe7557615bd02e750e54f6c18443",
|
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/65/aa/0b3de0dec8931143e93095030061",
|
||||||
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a5/de/a7047d5d833f8ebb62e383e5eaf4",
|
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fe/e9/59f07ea132235d66772728a3f444",
|
||||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/ce/9c/e473fc15f9d3a0bedfb3913d1962",
|
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/fe/32/e853d0c6babe75a9cd13c3d1ad74",
|
||||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/3f/ed/28da613b3e324d1430b237fb08a6",
|
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/9c/c4/051b70035701c59410806b4a88f4",
|
||||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/0a/7f/2cdc32fd26567e699debe0bc2664",
|
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/8a/c7/df085272821c0ecd94285b3b7c8c",
|
||||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/a8/df/b80403b15d88e26fd33c37c0941f",
|
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/23/f9/e19797b50315d1c1db90aae74252",
|
||||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/48/98/24c3d9f63d6909320ee6152b2225",
|
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/ef/5c/d76a648c4113f0cac7f8025d8151",
|
||||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/3d/1e/dc89ecda37bcf6ce72e8d19452d9",
|
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/89/6b/2d5abd4ed36aacc160578ba1346c",
|
||||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/e9/ce/8b528bcfdc5dabd974c4693ccd63",
|
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/49/a7/1b287c00b6c0797347556a5ce4bf",
|
||||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/34/33/2d5a7cff960d15324c98199a2d39",
|
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/43/0d/f0edc7b954a32d06f2b6f8f473d0",
|
||||||
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/6e/6f/004b696e9a13b083069374e4bb6a",
|
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/6e/6f/004b696e9a13b083069374e4bb6a",
|
||||||
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/d3/db/e73d4dcf1280d5f677c3cf8b47c3"
|
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/d3/db/e73d4dcf1280d5f677c3cf8b47c3"
|
||||||
}
|
}
|
||||||
5
.idea/dictionaries/ericf.xml
generated
5
.idea/dictionaries/ericf.xml
generated
@ -58,6 +58,7 @@
|
|||||||
<w>adisp</w>
|
<w>adisp</w>
|
||||||
<w>advertizing</w>
|
<w>advertizing</w>
|
||||||
<w>aidl</w>
|
<w>aidl</w>
|
||||||
|
<w>aint</w>
|
||||||
<w>aioloop</w>
|
<w>aioloop</w>
|
||||||
<w>aiomain</w>
|
<w>aiomain</w>
|
||||||
<w>alarmsound</w>
|
<w>alarmsound</w>
|
||||||
@ -513,6 +514,7 @@
|
|||||||
<w>createtime</w>
|
<w>createtime</w>
|
||||||
<w>creationflags</w>
|
<w>creationflags</w>
|
||||||
<w>creditslist</w>
|
<w>creditslist</w>
|
||||||
|
<w>credstr</w>
|
||||||
<w>cresult</w>
|
<w>cresult</w>
|
||||||
<w>cryptmodule</w>
|
<w>cryptmodule</w>
|
||||||
<w>cryptosimple</w>
|
<w>cryptosimple</w>
|
||||||
@ -2364,6 +2366,7 @@
|
|||||||
<w>strs</w>
|
<w>strs</w>
|
||||||
<w>strt</w>
|
<w>strt</w>
|
||||||
<w>strval</w>
|
<w>strval</w>
|
||||||
|
<w>stuttery</w>
|
||||||
<w>subargs</w>
|
<w>subargs</w>
|
||||||
<w>subc</w>
|
<w>subc</w>
|
||||||
<w>subclassof</w>
|
<w>subclassof</w>
|
||||||
@ -2753,6 +2756,8 @@
|
|||||||
<w>woooo</w>
|
<w>woooo</w>
|
||||||
<w>workdir</w>
|
<w>workdir</w>
|
||||||
<w>workflows</w>
|
<w>workflows</w>
|
||||||
|
<w>workspaceid</w>
|
||||||
|
<w>workspacename</w>
|
||||||
<w>woutdir</w>
|
<w>woutdir</w>
|
||||||
<w>wpath</w>
|
<w>wpath</w>
|
||||||
<w>wprjp</w>
|
<w>wprjp</w>
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
### 1.7.2 (20604, 2022-06-15)
|
### 1.7.2 (20610, 2022-06-21)
|
||||||
- Minor fixes in some minigames (Thanks Droopy!)
|
- Minor fixes in some minigames (Thanks Droopy!)
|
||||||
- Fixed a bug preventing 'clients' arg from working in _ba.chatmessage (Thanks imayushsaini!)
|
- Fixed a bug preventing 'clients' arg from working in _ba.chatmessage (Thanks imayushsaini!)
|
||||||
- Fixed a bug where ba.Player.getdelegate(doraise=True) could return None instead of raising a ba.DelegateNotFoundError (thanks Dliwk!)
|
- Fixed a bug where ba.Player.getdelegate(doraise=True) could return None instead of raising a ba.DelegateNotFoundError (thanks Dliwk!)
|
||||||
- Lots of Romanian language improvements (Thanks Meryu!)
|
- Lots of Romanian language improvements (Thanks Meryu!)
|
||||||
|
- Workspaces are now functional. They require signing in with a V2 account, which currently is limited to explicitly created email/password logins. See ballistica.net to create such an account or create/edit a workspace. This is bleeding edge stuff so please holler with any bugs you come across or if anything seems unintuitive.
|
||||||
|
- Newly detected Plugins are now enabled by default in all cases; not just headless builds. (Though a restart is still required before they run). Some builds (headless, iiRcade) can't easily access gui settings so this makes Plugins more usable there and keeps things consistent. The user still has the opportunity to deactivate newly detected plugins before restarting if they don't want to use them.
|
||||||
|
- Reworked app states for the new workspace system, with a new `loading` stage that comes after `launching` and before `running`. The `loading` stage consists of an initial account log-in (or lack thereof) and any workspace/asset downloading related to that. This allows the app to ensure that the latest workspace state is synced for the active account before running plugin loads and meta scans, allowing those bits to work as seamlessly in workspaces as they do for traditional local manual installs.
|
||||||
|
- Plugins now have an `on_app_running` call instead of `on_app_launch`, allowing them to work seamlessly with workspaces (see previous entry).
|
||||||
|
- Errors running/loading plugins now show up as screen-messages. This can be ugly but hopefully provides a bit of debugging capability for anyone testing code on a phone or somewhere with no access to full log output. Once we can add logging features to the workspaces web ui we can perhaps scale back on this.
|
||||||
|
- Api version increased from 6 to 7 due to the aforementioned plugin changes (`on_app_launch` becoming `on_app_running`, etc.)
|
||||||
|
|
||||||
### 1.7.1 (20597, 2022-06-04)
|
### 1.7.1 (20597, 2022-06-04)
|
||||||
- V2 account logic fixes
|
- V2 account logic fixes
|
||||||
|
|||||||
@ -60,6 +60,7 @@
|
|||||||
"ba_data/python/ba/__pycache__/_tips.cpython-310.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/_tips.cpython-310.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/_tournament.cpython-310.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/_tournament.cpython-310.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/_ui.cpython-310.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/_ui.cpython-310.opt-1.pyc",
|
||||||
|
"ba_data/python/ba/__pycache__/_workspace.cpython-310.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/deprecated.cpython-310.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/deprecated.cpython-310.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/internal.cpython-310.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/internal.cpython-310.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/macmusicapp.cpython-310.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/macmusicapp.cpython-310.opt-1.pyc",
|
||||||
@ -128,6 +129,7 @@
|
|||||||
"ba_data/python/ba/_tips.py",
|
"ba_data/python/ba/_tips.py",
|
||||||
"ba_data/python/ba/_tournament.py",
|
"ba_data/python/ba/_tournament.py",
|
||||||
"ba_data/python/ba/_ui.py",
|
"ba_data/python/ba/_ui.py",
|
||||||
|
"ba_data/python/ba/_workspace.py",
|
||||||
"ba_data/python/ba/deprecated.py",
|
"ba_data/python/ba/deprecated.py",
|
||||||
"ba_data/python/ba/internal.py",
|
"ba_data/python/ba/internal.py",
|
||||||
"ba_data/python/ba/macmusicapp.py",
|
"ba_data/python/ba/macmusicapp.py",
|
||||||
|
|||||||
@ -195,6 +195,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
|
|||||||
build/ba_data/python/ba/_tips.py \
|
build/ba_data/python/ba/_tips.py \
|
||||||
build/ba_data/python/ba/_tournament.py \
|
build/ba_data/python/ba/_tournament.py \
|
||||||
build/ba_data/python/ba/_ui.py \
|
build/ba_data/python/ba/_ui.py \
|
||||||
|
build/ba_data/python/ba/_workspace.py \
|
||||||
build/ba_data/python/ba/deprecated.py \
|
build/ba_data/python/ba/deprecated.py \
|
||||||
build/ba_data/python/ba/internal.py \
|
build/ba_data/python/ba/internal.py \
|
||||||
build/ba_data/python/ba/macmusicapp.py \
|
build/ba_data/python/ba/macmusicapp.py \
|
||||||
@ -442,6 +443,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
|
|||||||
build/ba_data/python/ba/__pycache__/_tips.cpython-310.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/_tips.cpython-310.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_tournament.cpython-310.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/_tournament.cpython-310.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_ui.cpython-310.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/_ui.cpython-310.opt-1.pyc \
|
||||||
|
build/ba_data/python/ba/__pycache__/_workspace.cpython-310.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/deprecated.cpython-310.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/deprecated.cpython-310.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/internal.cpython-310.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/internal.cpython-310.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/macmusicapp.cpython-310.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/macmusicapp.cpython-310.opt-1.pyc \
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
217115660358712006436605949517410465969
|
109029534501022699603510994431677623146
|
||||||
@ -1924,6 +1924,16 @@ def get_v2_fleet() -> str:
|
|||||||
return str()
|
return str()
|
||||||
|
|
||||||
|
|
||||||
|
def get_volatile_data_directory() -> str:
|
||||||
|
"""(internal)
|
||||||
|
|
||||||
|
Return the path to the app volatile data directory.
|
||||||
|
This directory is for data generated by the app that does not
|
||||||
|
need to be backed up and can be recreated if necessary.
|
||||||
|
"""
|
||||||
|
return str()
|
||||||
|
|
||||||
|
|
||||||
# Show that our return type varies based on "doraise" value:
|
# Show that our return type varies based on "doraise" value:
|
||||||
@overload
|
@overload
|
||||||
def getactivity(doraise: Literal[True] = True) -> ba.Activity:
|
def getactivity(doraise: Literal[True] = True) -> ba.Activity:
|
||||||
|
|||||||
@ -6,8 +6,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import _ba
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Optional
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AccountV2Subsystem:
|
class AccountV2Subsystem:
|
||||||
@ -18,10 +20,20 @@ class AccountV2Subsystem:
|
|||||||
Access the single shared instance of this class at 'ba.app.accounts_v2'.
|
Access the single shared instance of this class at 'ba.app.accounts_v2'.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
|
||||||
|
# Whether or not everything related to an initial login
|
||||||
|
# (or lack thereof) has completed. This includes things like
|
||||||
|
# workspace syncing. Completion of this is what flips the app
|
||||||
|
# into 'running' state.
|
||||||
|
self._initial_login_completed = False
|
||||||
|
|
||||||
|
self._kicked_off_workspace_load = False
|
||||||
|
|
||||||
def on_app_launch(self) -> None:
|
def on_app_launch(self) -> None:
|
||||||
"""Should be called at standard on_app_launch time."""
|
"""Should be called at standard on_app_launch time."""
|
||||||
|
|
||||||
def set_primary_credentials(self, credentials: Optional[str]) -> None:
|
def set_primary_credentials(self, credentials: str | None) -> None:
|
||||||
"""Set credentials for the primary app account."""
|
"""Set credentials for the primary app account."""
|
||||||
raise RuntimeError('This should be overridden.')
|
raise RuntimeError('This should be overridden.')
|
||||||
|
|
||||||
@ -35,17 +47,74 @@ class AccountV2Subsystem:
|
|||||||
raise RuntimeError('This should be overridden.')
|
raise RuntimeError('This should be overridden.')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def primary(self) -> Optional[AccountV2Handle]:
|
def primary(self) -> AccountV2Handle | None:
|
||||||
"""The primary account for the app, or None if not logged in."""
|
"""The primary account for the app, or None if not logged in."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_primary(self) -> Optional[AccountV2Handle]:
|
def do_get_primary(self) -> AccountV2Handle | None:
|
||||||
"""Internal - should be overridden by subclass."""
|
"""Internal - should be overridden by subclass."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def on_primary_account_changed(self,
|
||||||
|
account: AccountV2Handle | None) -> None:
|
||||||
|
"""Callback run after the primary account changes.
|
||||||
|
|
||||||
|
Will be called with None on log-outs or when new credentials
|
||||||
|
are set but have not yet been verified.
|
||||||
|
"""
|
||||||
|
# Currently don't do anything special on sign-outs.
|
||||||
|
if account is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# If this new account has a workspace, update it and ask to be
|
||||||
|
# informed when that process completes.
|
||||||
|
if account.workspaceid is not None:
|
||||||
|
assert account.workspacename is not None
|
||||||
|
if (not self._initial_login_completed
|
||||||
|
and not self._kicked_off_workspace_load):
|
||||||
|
self._kicked_off_workspace_load = True
|
||||||
|
_ba.app.workspaces.set_active_workspace(
|
||||||
|
workspaceid=account.workspaceid,
|
||||||
|
workspacename=account.workspacename,
|
||||||
|
on_completed=self._on_set_active_workspace_completed)
|
||||||
|
else:
|
||||||
|
# Don't activate workspaces if we've already told the game
|
||||||
|
# that initial-log-in is done or if we've already kicked
|
||||||
|
# off a workspace load.
|
||||||
|
_ba.screenmessage(
|
||||||
|
f'\'{account.workspacename}\''
|
||||||
|
f' will be activated at next app launch.',
|
||||||
|
color=(1, 1, 0))
|
||||||
|
_ba.playsound(_ba.getsound('error'))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ok; no workspace to worry about; carry on.
|
||||||
|
if not self._initial_login_completed:
|
||||||
|
self._initial_login_completed = True
|
||||||
|
_ba.app.on_initial_login_completed()
|
||||||
|
|
||||||
|
def on_no_initial_primary_account(self) -> None:
|
||||||
|
"""Callback run if the app has no primary account after launch.
|
||||||
|
|
||||||
|
Either this callback or on_primary_account_changed will be called
|
||||||
|
within a few seconds of app launch; the app can move forward
|
||||||
|
with the startup sequence at that point.
|
||||||
|
"""
|
||||||
|
if not self._initial_login_completed:
|
||||||
|
self._initial_login_completed = True
|
||||||
|
_ba.app.on_initial_login_completed()
|
||||||
|
|
||||||
|
def _on_set_active_workspace_completed(self) -> None:
|
||||||
|
if not self._initial_login_completed:
|
||||||
|
self._initial_login_completed = True
|
||||||
|
_ba.app.on_initial_login_completed()
|
||||||
|
|
||||||
|
|
||||||
class AccountV2Handle:
|
class AccountV2Handle:
|
||||||
"""Handle for interacting with a v2 account."""
|
"""Handle for interacting with a v2 account."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.tag = '?'
|
self.tag = '?'
|
||||||
|
|
||||||
|
self.workspacename: str | None = None
|
||||||
|
self.workspaceid: str | None = None
|
||||||
|
|||||||
@ -19,10 +19,11 @@ from ba._accountv1 import AccountV1Subsystem
|
|||||||
from ba._meta import MetadataSubsystem
|
from ba._meta import MetadataSubsystem
|
||||||
from ba._ads import AdsSubsystem
|
from ba._ads import AdsSubsystem
|
||||||
from ba._net import NetworkSubsystem
|
from ba._net import NetworkSubsystem
|
||||||
|
from ba._workspace import WorkspaceSubsystem
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Optional, Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
import ba
|
import ba
|
||||||
from ba._cloud import CloudSubsystem
|
from ba._cloud import CloudSubsystem
|
||||||
@ -49,10 +50,21 @@ class App:
|
|||||||
|
|
||||||
class State(Enum):
|
class State(Enum):
|
||||||
"""High level state the app can be in."""
|
"""High level state the app can be in."""
|
||||||
|
|
||||||
|
# Python-level systems being inited but should not interact.
|
||||||
LAUNCHING = 0
|
LAUNCHING = 0
|
||||||
RUNNING = 1
|
|
||||||
PAUSED = 2
|
# Initial account logins, workspace & asset downloads, etc.
|
||||||
SHUTTING_DOWN = 3
|
LOADING = 1
|
||||||
|
|
||||||
|
# Normal running state.
|
||||||
|
RUNNING = 2
|
||||||
|
|
||||||
|
# App is backgrounded or otherwise suspended.
|
||||||
|
PAUSED = 3
|
||||||
|
|
||||||
|
# App is shutting down.
|
||||||
|
SHUTTING_DOWN = 4
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def aioloop(self) -> asyncio.AbstractEventLoop:
|
def aioloop(self) -> asyncio.AbstractEventLoop:
|
||||||
@ -208,7 +220,9 @@ class App:
|
|||||||
|
|
||||||
self.state = self.State.LAUNCHING
|
self.state = self.State.LAUNCHING
|
||||||
|
|
||||||
self._app_launched = False
|
self._launch_completed = False
|
||||||
|
self._initial_login_completed = False
|
||||||
|
self._called_on_app_running = False
|
||||||
self._app_paused = False
|
self._app_paused = False
|
||||||
|
|
||||||
# Config.
|
# Config.
|
||||||
@ -219,7 +233,7 @@ class App:
|
|||||||
# refreshed/etc.
|
# refreshed/etc.
|
||||||
self.fg_state = 0
|
self.fg_state = 0
|
||||||
|
|
||||||
self._aioloop: Optional[asyncio.AbstractEventLoop] = None
|
self._aioloop: asyncio.AbstractEventLoop | None = None
|
||||||
|
|
||||||
self._env = _ba.env()
|
self._env = _ba.env()
|
||||||
self.protocol_version: int = self._env['protocol_version']
|
self.protocol_version: int = self._env['protocol_version']
|
||||||
@ -243,12 +257,12 @@ class App:
|
|||||||
|
|
||||||
# Misc.
|
# Misc.
|
||||||
self.tips: list[str] = []
|
self.tips: list[str] = []
|
||||||
self.stress_test_reset_timer: Optional[ba.Timer] = None
|
self.stress_test_reset_timer: ba.Timer | None = None
|
||||||
self.did_weak_call_warning = False
|
self.did_weak_call_warning = False
|
||||||
|
|
||||||
self.log_have_new = False
|
self.log_have_new = False
|
||||||
self.log_upload_timer_started = False
|
self.log_upload_timer_started = False
|
||||||
self._config: Optional[ba.AppConfig] = None
|
self._config: ba.AppConfig | None = None
|
||||||
self.printed_live_object_warning = False
|
self.printed_live_object_warning = False
|
||||||
|
|
||||||
# We include this extra hash with shared input-mapping names so
|
# We include this extra hash with shared input-mapping names so
|
||||||
@ -256,13 +270,13 @@ class App:
|
|||||||
# systems. For instance, different android devices may give different
|
# systems. For instance, different android devices may give different
|
||||||
# key values for the same controller type so we keep their mappings
|
# key values for the same controller type so we keep their mappings
|
||||||
# distinct.
|
# distinct.
|
||||||
self.input_map_hash: Optional[str] = None
|
self.input_map_hash: str | None = None
|
||||||
|
|
||||||
# Co-op Campaigns.
|
# Co-op Campaigns.
|
||||||
self.campaigns: dict[str, ba.Campaign] = {}
|
self.campaigns: dict[str, ba.Campaign] = {}
|
||||||
|
|
||||||
# Server Mode.
|
# Server Mode.
|
||||||
self.server: Optional[ba.ServerController] = None
|
self.server: ba.ServerController | None = None
|
||||||
|
|
||||||
self.meta = MetadataSubsystem()
|
self.meta = MetadataSubsystem()
|
||||||
self.accounts_v1 = AccountV1Subsystem()
|
self.accounts_v1 = AccountV1Subsystem()
|
||||||
@ -273,15 +287,16 @@ class App:
|
|||||||
self.ui = UISubsystem()
|
self.ui = UISubsystem()
|
||||||
self.ads = AdsSubsystem()
|
self.ads = AdsSubsystem()
|
||||||
self.net = NetworkSubsystem()
|
self.net = NetworkSubsystem()
|
||||||
|
self.workspaces = WorkspaceSubsystem()
|
||||||
|
|
||||||
# Lobby.
|
# Lobby.
|
||||||
self.lobby_random_profile_index: int = 1
|
self.lobby_random_profile_index: int = 1
|
||||||
self.lobby_random_char_index_offset = random.randrange(1000)
|
self.lobby_random_char_index_offset = random.randrange(1000)
|
||||||
self.lobby_account_profile_device_id: Optional[int] = None
|
self.lobby_account_profile_device_id: int | None = None
|
||||||
|
|
||||||
# Main Menu.
|
# Main Menu.
|
||||||
self.main_menu_did_initial_transition = False
|
self.main_menu_did_initial_transition = False
|
||||||
self.main_menu_last_news_fetch_time: Optional[float] = None
|
self.main_menu_last_news_fetch_time: float | None = None
|
||||||
|
|
||||||
# Spaz.
|
# Spaz.
|
||||||
self.spaz_appearances: dict[str, spazappearance.Appearance] = {}
|
self.spaz_appearances: dict[str, spazappearance.Appearance] = {}
|
||||||
@ -300,19 +315,19 @@ class App:
|
|||||||
self.did_menu_intro = False # FIXME: Move to mainmenu class.
|
self.did_menu_intro = False # FIXME: Move to mainmenu class.
|
||||||
self.main_menu_window_refresh_check_count = 0 # FIXME: Mv to mainmenu.
|
self.main_menu_window_refresh_check_count = 0 # FIXME: Mv to mainmenu.
|
||||||
self.main_menu_resume_callbacks: list = [] # Can probably go away.
|
self.main_menu_resume_callbacks: list = [] # Can probably go away.
|
||||||
self.special_offer: Optional[dict] = None
|
self.special_offer: dict | None = None
|
||||||
self.ping_thread_count = 0
|
self.ping_thread_count = 0
|
||||||
self.invite_confirm_windows: list[Any] = [] # FIXME: Don't use Any.
|
self.invite_confirm_windows: list[Any] = [] # FIXME: Don't use Any.
|
||||||
self.store_layout: Optional[dict[str, list[dict[str, Any]]]] = None
|
self.store_layout: dict[str, list[dict[str, Any]]] | None = None
|
||||||
self.store_items: Optional[dict[str, dict]] = None
|
self.store_items: dict[str, dict] | None = None
|
||||||
self.pro_sale_start_time: Optional[int] = None
|
self.pro_sale_start_time: int | None = None
|
||||||
self.pro_sale_start_val: Optional[int] = None
|
self.pro_sale_start_val: int | None = None
|
||||||
|
|
||||||
self.delegate: Optional[ba.AppDelegate] = None
|
self.delegate: ba.AppDelegate | None = None
|
||||||
self._asyncio_timer: Optional[ba.Timer] = None
|
self._asyncio_timer: ba.Timer | None = None
|
||||||
|
|
||||||
def on_app_launch(self) -> None:
|
def on_app_launch(self) -> None:
|
||||||
"""Runs after the app finishes bootstrapping.
|
"""Runs after the app finishes low level bootstrapping.
|
||||||
|
|
||||||
(internal)"""
|
(internal)"""
|
||||||
# pylint: disable=cyclic-import
|
# pylint: disable=cyclic-import
|
||||||
@ -398,19 +413,23 @@ class App:
|
|||||||
if not self.headless_mode:
|
if not self.headless_mode:
|
||||||
_ba.timer(3.0, check_special_offer, timetype=TimeType.REAL)
|
_ba.timer(3.0, check_special_offer, timetype=TimeType.REAL)
|
||||||
|
|
||||||
self.meta.on_app_launch()
|
|
||||||
self.accounts_v2.on_app_launch()
|
self.accounts_v2.on_app_launch()
|
||||||
self.accounts_v1.on_app_launch()
|
self.accounts_v1.on_app_launch()
|
||||||
self.plugins.on_app_launch()
|
|
||||||
|
|
||||||
# See note below in on_app_pause.
|
# See note below in on_app_pause.
|
||||||
if self.state != self.State.LAUNCHING:
|
if self.state != self.State.LAUNCHING:
|
||||||
logging.error('on_app_launch found state %s; expected LAUNCHING.',
|
logging.error('on_app_launch found state %s; expected LAUNCHING.',
|
||||||
self.state)
|
self.state)
|
||||||
|
|
||||||
self._app_launched = True
|
self._launch_completed = True
|
||||||
self._update_state()
|
self._update_state()
|
||||||
|
|
||||||
|
def on_app_running(self) -> None:
|
||||||
|
"""Called when initially entering the running state."""
|
||||||
|
|
||||||
|
self.meta.on_app_running()
|
||||||
|
self.plugins.on_app_running()
|
||||||
|
|
||||||
# from ba._dependency import test_depset
|
# from ba._dependency import test_depset
|
||||||
# test_depset()
|
# test_depset()
|
||||||
if bool(False):
|
if bool(False):
|
||||||
@ -420,8 +439,13 @@ class App:
|
|||||||
if self._app_paused:
|
if self._app_paused:
|
||||||
self.state = self.State.PAUSED
|
self.state = self.State.PAUSED
|
||||||
else:
|
else:
|
||||||
if self._app_launched:
|
if self._initial_login_completed:
|
||||||
self.state = self.State.RUNNING
|
self.state = self.State.RUNNING
|
||||||
|
if not self._called_on_app_running:
|
||||||
|
self._called_on_app_running = True
|
||||||
|
self.on_app_running()
|
||||||
|
elif self._launch_completed:
|
||||||
|
self.state = self.State.LOADING
|
||||||
else:
|
else:
|
||||||
self.state = self.State.LAUNCHING
|
self.state = self.State.LAUNCHING
|
||||||
|
|
||||||
@ -459,7 +483,7 @@ class App:
|
|||||||
If there's a foreground host-activity that says it's pausable, tell it
|
If there's a foreground host-activity that says it's pausable, tell it
|
||||||
to pause ..we now no longer pause if there are connected clients.
|
to pause ..we now no longer pause if there are connected clients.
|
||||||
"""
|
"""
|
||||||
activity: Optional[ba.Activity] = _ba.get_foreground_host_activity()
|
activity: ba.Activity | None = _ba.get_foreground_host_activity()
|
||||||
if (activity is not None and activity.allow_pausing
|
if (activity is not None and activity.allow_pausing
|
||||||
and not _ba.have_connected_clients()):
|
and not _ba.have_connected_clients()):
|
||||||
from ba._language import Lstr
|
from ba._language import Lstr
|
||||||
@ -523,7 +547,7 @@ class App:
|
|||||||
|
|
||||||
# If we're in a host-session, tell them to end.
|
# If we're in a host-session, tell them to end.
|
||||||
# This lets them tear themselves down gracefully.
|
# This lets them tear themselves down gracefully.
|
||||||
host_session: Optional[ba.Session] = _ba.get_foreground_host_session()
|
host_session: ba.Session | None = _ba.get_foreground_host_session()
|
||||||
if host_session is not None:
|
if host_session is not None:
|
||||||
|
|
||||||
# Kick off a little transaction so we'll hopefully have all the
|
# Kick off a little transaction so we'll hopefully have all the
|
||||||
@ -609,6 +633,17 @@ class App:
|
|||||||
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||||
_ba.playsound(_ba.getsound('error'))
|
_ba.playsound(_ba.getsound('error'))
|
||||||
|
|
||||||
|
def on_initial_login_completed(self) -> None:
|
||||||
|
"""Callback to be run after initial login process (or lack thereof).
|
||||||
|
|
||||||
|
This period includes things such as syncing account workspaces
|
||||||
|
or other data so it may take a substantial amount of time.
|
||||||
|
This should also run after a short amount of time if no login
|
||||||
|
has occurred.
|
||||||
|
"""
|
||||||
|
self._initial_login_completed = True
|
||||||
|
self._update_state()
|
||||||
|
|
||||||
def _test_https(self) -> None:
|
def _test_https(self) -> None:
|
||||||
"""Testing https support.
|
"""Testing https support.
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import _ba
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Callable, Any
|
from typing import Callable, Any
|
||||||
|
|
||||||
from efro.message import Message
|
from efro.message import Message, Response
|
||||||
import bacommon.cloud
|
import bacommon.cloud
|
||||||
|
|
||||||
# TODO: Should make it possible to define a protocol in bacommon.cloud and
|
# TODO: Should make it possible to define a protocol in bacommon.cloud and
|
||||||
@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class CloudSubsystem:
|
class CloudSubsystem:
|
||||||
"""Used for communicating with the cloud."""
|
"""Manages communication with cloud components."""
|
||||||
|
|
||||||
def is_connected(self) -> bool:
|
def is_connected(self) -> bool:
|
||||||
"""Return whether a connection to the cloud is present.
|
"""Return whether a connection to the cloud is present.
|
||||||
@ -31,7 +31,7 @@ class CloudSubsystem:
|
|||||||
return False # Needs to be overridden
|
return False # Needs to be overridden
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def send_message(
|
def send_message_cb(
|
||||||
self,
|
self,
|
||||||
msg: bacommon.cloud.LoginProxyRequestMessage,
|
msg: bacommon.cloud.LoginProxyRequestMessage,
|
||||||
on_response: Callable[
|
on_response: Callable[
|
||||||
@ -40,7 +40,7 @@ class CloudSubsystem:
|
|||||||
...
|
...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def send_message(
|
def send_message_cb(
|
||||||
self,
|
self,
|
||||||
msg: bacommon.cloud.LoginProxyStateQueryMessage,
|
msg: bacommon.cloud.LoginProxyStateQueryMessage,
|
||||||
on_response: Callable[
|
on_response: Callable[
|
||||||
@ -49,36 +49,19 @@ class CloudSubsystem:
|
|||||||
...
|
...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def send_message(
|
def send_message_cb(
|
||||||
self,
|
self,
|
||||||
msg: bacommon.cloud.LoginProxyCompleteMessage,
|
msg: bacommon.cloud.LoginProxyCompleteMessage,
|
||||||
on_response: Callable[[None | Exception], None],
|
on_response: Callable[[None | Exception], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
@overload
|
def send_message_cb(
|
||||||
def send_message(
|
|
||||||
self,
|
|
||||||
msg: bacommon.cloud.CredentialsCheckMessage,
|
|
||||||
on_response: Callable[
|
|
||||||
[bacommon.cloud.CredentialsCheckResponse | Exception], None],
|
|
||||||
) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def send_message(
|
|
||||||
self,
|
|
||||||
msg: bacommon.cloud.AccountSessionReleaseMessage,
|
|
||||||
on_response: Callable[[None | Exception], None],
|
|
||||||
) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
def send_message(
|
|
||||||
self,
|
self,
|
||||||
msg: Message,
|
msg: Message,
|
||||||
on_response: Callable[[Any], None],
|
on_response: Callable[[Any], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Asynchronously send a message to the cloud from the game thread.
|
"""Asynchronously send a message to the cloud from the logic thread.
|
||||||
|
|
||||||
The provided on_response call will be run in the logic thread
|
The provided on_response call will be run in the logic thread
|
||||||
and passed either the response or the error that occurred.
|
and passed either the response or the error that occurred.
|
||||||
@ -89,3 +72,22 @@ class CloudSubsystem:
|
|||||||
_ba.pushcall(
|
_ba.pushcall(
|
||||||
Call(on_response,
|
Call(on_response,
|
||||||
RuntimeError('Cloud functionality is not available.')))
|
RuntimeError('Cloud functionality is not available.')))
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def send_message(
|
||||||
|
self, msg: bacommon.cloud.WorkspaceFetchMessage
|
||||||
|
) -> bacommon.cloud.WorkspaceFetchResponse:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def send_message(
|
||||||
|
self,
|
||||||
|
msg: bacommon.cloud.TestMessage) -> bacommon.cloud.TestResponse:
|
||||||
|
...
|
||||||
|
|
||||||
|
def send_message(self, msg: Message) -> Response | None:
|
||||||
|
"""Synchronously send a message to the cloud.
|
||||||
|
|
||||||
|
Must be called from a background thread.
|
||||||
|
"""
|
||||||
|
raise RuntimeError('Cloud functionality is not available.')
|
||||||
|
|||||||
@ -6,22 +6,21 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import pathlib
|
|
||||||
import threading
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
import _ba
|
import _ba
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Union, Optional
|
|
||||||
import ba
|
import ba
|
||||||
|
|
||||||
# The meta api version of this build of the game.
|
# The meta api version of this build of the game.
|
||||||
# Only packages and modules requiring this exact api version
|
# Only packages and modules requiring this exact api version
|
||||||
# will be considered when scanning directories.
|
# will be considered when scanning directories.
|
||||||
# See: https://ballistica.net/wiki/Meta-Tags
|
# See: https://ballistica.net/wiki/Meta-Tags
|
||||||
CURRENT_API_VERSION = 6
|
CURRENT_API_VERSION = 7
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -43,10 +42,11 @@ class MetadataSubsystem:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.metascan: Optional[ScanResults] = None
|
self.metascan: ScanResults | None = None
|
||||||
|
self.extra_scan_dirs: list[str] = []
|
||||||
|
|
||||||
def on_app_launch(self) -> None:
|
def on_app_running(self) -> None:
|
||||||
"""Should be called when the app is done bootstrapping."""
|
"""Should be called when the app enters the running state."""
|
||||||
|
|
||||||
# Start scanning for things exposed via ba_meta.
|
# Start scanning for things exposed via ba_meta.
|
||||||
self.start_scan()
|
self.start_scan()
|
||||||
@ -58,7 +58,8 @@ class MetadataSubsystem:
|
|||||||
app = _ba.app
|
app = _ba.app
|
||||||
if self.metascan is not None:
|
if self.metascan is not None:
|
||||||
print('WARNING: meta scan run more than once.')
|
print('WARNING: meta scan run more than once.')
|
||||||
pythondirs = [app.python_directory_app, app.python_directory_user]
|
pythondirs = ([app.python_directory_app, app.python_directory_user] +
|
||||||
|
self.extra_scan_dirs)
|
||||||
thread = ScanThread(pythondirs)
|
thread = ScanThread(pythondirs)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
@ -99,16 +100,10 @@ class MetadataSubsystem:
|
|||||||
class_path=class_path,
|
class_path=class_path,
|
||||||
available=True))
|
available=True))
|
||||||
if class_path not in plugstates:
|
if class_path not in plugstates:
|
||||||
if _ba.app.headless_mode:
|
# Go ahead and enable new plugins by default, but we'll
|
||||||
# If we running in headless mode, enable plugin by default
|
# inform the user that they need to restart to pick them up.
|
||||||
# to allow server admins to get their modified build
|
# they can also disable them in settings so they never load.
|
||||||
# working 'out-of-the-box', without manually updating the
|
plugstates[class_path] = {'enabled': True}
|
||||||
# config.
|
|
||||||
plugstates[class_path] = {'enabled': True}
|
|
||||||
else:
|
|
||||||
# If we running in normal mode, disable plugin by default
|
|
||||||
# (user can enable it later).
|
|
||||||
plugstates[class_path] = {'enabled': False}
|
|
||||||
config_changed = True
|
config_changed = True
|
||||||
found_new = True
|
found_new = True
|
||||||
|
|
||||||
@ -223,18 +218,17 @@ class DirectoryScan:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Skip non-existent paths completely.
|
# Skip non-existent paths completely.
|
||||||
self.paths = [pathlib.Path(p) for p in paths if os.path.isdir(p)]
|
self.paths = [Path(p) for p in paths if os.path.isdir(p)]
|
||||||
self.results = ScanResults()
|
self.results = ScanResults()
|
||||||
|
|
||||||
def _get_path_module_entries(
|
def _get_path_module_entries(self, path: Path, subpath: str | Path,
|
||||||
self, path: pathlib.Path, subpath: Union[str, pathlib.Path],
|
modules: list[tuple[Path, Path]]) -> None:
|
||||||
modules: list[tuple[pathlib.Path, pathlib.Path]]) -> None:
|
|
||||||
"""Scan provided path and add module entries to provided list."""
|
"""Scan provided path and add module entries to provided list."""
|
||||||
try:
|
try:
|
||||||
# Special case: let's save some time and skip the whole 'ba'
|
# Special case: let's save some time and skip the whole 'ba'
|
||||||
# package since we know it doesn't contain any meta tags.
|
# package since we know it doesn't contain any meta tags.
|
||||||
fullpath = pathlib.Path(path, subpath)
|
fullpath = Path(path, subpath)
|
||||||
entries = [(path, pathlib.Path(subpath, name))
|
entries = [(path, Path(subpath, name))
|
||||||
for name in os.listdir(fullpath) if name != 'ba']
|
for name in os.listdir(fullpath) if name != 'ba']
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
# Expected sometimes.
|
# Expected sometimes.
|
||||||
@ -248,13 +242,13 @@ class DirectoryScan:
|
|||||||
for entry in entries:
|
for entry in entries:
|
||||||
if entry[1].name.endswith('.py'):
|
if entry[1].name.endswith('.py'):
|
||||||
modules.append(entry)
|
modules.append(entry)
|
||||||
elif (pathlib.Path(entry[0], entry[1]).is_dir() and pathlib.Path(
|
elif (Path(entry[0], entry[1]).is_dir()
|
||||||
entry[0], entry[1], '__init__.py').is_file()):
|
and Path(entry[0], entry[1], '__init__.py').is_file()):
|
||||||
modules.append(entry)
|
modules.append(entry)
|
||||||
|
|
||||||
def scan(self) -> None:
|
def scan(self) -> None:
|
||||||
"""Scan provided paths."""
|
"""Scan provided paths."""
|
||||||
modules: list[tuple[pathlib.Path, pathlib.Path]] = []
|
modules: list[tuple[Path, Path]] = []
|
||||||
for path in self.paths:
|
for path in self.paths:
|
||||||
self._get_path_module_entries(path, '', modules)
|
self._get_path_module_entries(path, '', modules)
|
||||||
for moduledir, subpath in modules:
|
for moduledir, subpath in modules:
|
||||||
@ -269,14 +263,13 @@ class DirectoryScan:
|
|||||||
self.results.games.sort()
|
self.results.games.sort()
|
||||||
self.results.plugins.sort()
|
self.results.plugins.sort()
|
||||||
|
|
||||||
def scan_module(self, moduledir: pathlib.Path,
|
def scan_module(self, moduledir: Path, subpath: Path) -> None:
|
||||||
subpath: pathlib.Path) -> None:
|
|
||||||
"""Scan an individual module and add the findings to results."""
|
"""Scan an individual module and add the findings to results."""
|
||||||
if subpath.name.endswith('.py'):
|
if subpath.name.endswith('.py'):
|
||||||
fpath = pathlib.Path(moduledir, subpath)
|
fpath = Path(moduledir, subpath)
|
||||||
ispackage = False
|
ispackage = False
|
||||||
else:
|
else:
|
||||||
fpath = pathlib.Path(moduledir, subpath, '__init__.py')
|
fpath = Path(moduledir, subpath, '__init__.py')
|
||||||
ispackage = True
|
ispackage = True
|
||||||
with fpath.open(encoding='utf-8') as infile:
|
with fpath.open(encoding='utf-8') as infile:
|
||||||
flines = infile.readlines()
|
flines = infile.readlines()
|
||||||
@ -305,7 +298,7 @@ class DirectoryScan:
|
|||||||
# If its a package, recurse into its subpackages.
|
# If its a package, recurse into its subpackages.
|
||||||
if ispackage:
|
if ispackage:
|
||||||
try:
|
try:
|
||||||
submodules: list[tuple[pathlib.Path, pathlib.Path]] = []
|
submodules: list[tuple[Path, Path]] = []
|
||||||
self._get_path_module_entries(moduledir, subpath, submodules)
|
self._get_path_module_entries(moduledir, subpath, submodules)
|
||||||
for submodule in submodules:
|
for submodule in submodules:
|
||||||
if submodule[1].name != '__init__.py':
|
if submodule[1].name != '__init__.py':
|
||||||
@ -315,8 +308,7 @@ class DirectoryScan:
|
|||||||
self.results.warnings += (
|
self.results.warnings += (
|
||||||
f"Error scanning '{subpath}': {traceback.format_exc()}\n")
|
f"Error scanning '{subpath}': {traceback.format_exc()}\n")
|
||||||
|
|
||||||
def _process_module_meta_tags(self, subpath: pathlib.Path,
|
def _process_module_meta_tags(self, subpath: Path, flines: list[str],
|
||||||
flines: list[str],
|
|
||||||
meta_lines: dict[int, list[str]]) -> None:
|
meta_lines: dict[int, list[str]]) -> None:
|
||||||
"""Pull data from a module based on its ba_meta tags."""
|
"""Pull data from a module based on its ba_meta tags."""
|
||||||
for lindex, mline in meta_lines.items():
|
for lindex, mline in meta_lines.items():
|
||||||
@ -360,8 +352,8 @@ class DirectoryScan:
|
|||||||
': unrecognized export type "' + exporttype +
|
': unrecognized export type "' + exporttype +
|
||||||
'" on line ' + str(lindex + 1) + '.\n')
|
'" on line ' + str(lindex + 1) + '.\n')
|
||||||
|
|
||||||
def _get_export_class_name(self, subpath: pathlib.Path, lines: list[str],
|
def _get_export_class_name(self, subpath: Path, lines: list[str],
|
||||||
lindex: int) -> Optional[str]:
|
lindex: int) -> str | None:
|
||||||
"""Given line num of an export tag, returns its operand class name."""
|
"""Given line num of an export tag, returns its operand class name."""
|
||||||
lindexorig = lindex
|
lindexorig = lindex
|
||||||
classname = None
|
classname = None
|
||||||
@ -386,9 +378,12 @@ class DirectoryScan:
|
|||||||
str(lindexorig + 1) + '.\n')
|
str(lindexorig + 1) + '.\n')
|
||||||
return classname
|
return classname
|
||||||
|
|
||||||
def get_api_requirement(self, subpath: pathlib.Path,
|
def get_api_requirement(
|
||||||
meta_lines: dict[int, list[str]],
|
self,
|
||||||
toplevel: bool) -> Optional[int]:
|
subpath: Path,
|
||||||
|
meta_lines: dict[int, list[str]],
|
||||||
|
toplevel: bool,
|
||||||
|
) -> int | None:
|
||||||
"""Return an API requirement integer or None if none present.
|
"""Return an API requirement integer or None if none present.
|
||||||
|
|
||||||
Malformed api requirement strings will be logged as warnings.
|
Malformed api requirement strings will be logged as warnings.
|
||||||
|
|||||||
@ -25,16 +25,16 @@ class PluginSubsystem:
|
|||||||
self.potential_plugins: list[ba.PotentialPlugin] = []
|
self.potential_plugins: list[ba.PotentialPlugin] = []
|
||||||
self.active_plugins: dict[str, ba.Plugin] = {}
|
self.active_plugins: dict[str, ba.Plugin] = {}
|
||||||
|
|
||||||
def on_app_launch(self) -> None:
|
def on_app_running(self) -> None:
|
||||||
"""Should be called at app launch time."""
|
"""Should be called when the app reaches the running state."""
|
||||||
# Load up our plugins and go ahead and call their on_app_launch calls.
|
# Load up our plugins and go ahead and call their on_app_running calls.
|
||||||
self.load_plugins()
|
self.load_plugins()
|
||||||
for plugin in self.active_plugins.values():
|
for plugin in self.active_plugins.values():
|
||||||
try:
|
try:
|
||||||
plugin.on_app_launch()
|
plugin.on_app_running()
|
||||||
except Exception:
|
except Exception:
|
||||||
from ba import _error
|
from ba import _error
|
||||||
_error.print_exception('Error in plugin on_app_launch()')
|
_error.print_exception('Error in plugin on_app_running()')
|
||||||
|
|
||||||
def on_app_pause(self) -> None:
|
def on_app_pause(self) -> None:
|
||||||
"""Called when the app goes to a suspended state."""
|
"""Called when the app goes to a suspended state."""
|
||||||
@ -80,16 +80,23 @@ class PluginSubsystem:
|
|||||||
try:
|
try:
|
||||||
cls = getclass(plugkey, Plugin)
|
cls = getclass(plugkey, Plugin)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_ba.log(f"Error loading plugin class '{plugkey}': {exc}",
|
_ba.playsound(_ba.getsound('error'))
|
||||||
to_server=False)
|
# TODO: Lstr.
|
||||||
|
errstr = f"Error loading plugin class '{plugkey}': {exc}"
|
||||||
|
_ba.screenmessage(errstr, color=(1, 0, 0))
|
||||||
|
_ba.log(errstr, to_server=False)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
plugin = cls()
|
plugin = cls()
|
||||||
assert plugkey not in self.active_plugins
|
assert plugkey not in self.active_plugins
|
||||||
self.active_plugins[plugkey] = plugin
|
self.active_plugins[plugkey] = plugin
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
from ba import _error
|
from ba import _error
|
||||||
_error.print_exception(f'Error loading plugin: {plugkey}')
|
_ba.playsound(_ba.getsound('error'))
|
||||||
|
# TODO: Lstr.
|
||||||
|
_ba.screenmessage(f"Error loading plugin: '{plugkey}': {exc}",
|
||||||
|
color=(1, 0, 0))
|
||||||
|
_error.print_exception(f"Error loading plugin: '{plugkey}'.")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -119,8 +126,8 @@ class Plugin:
|
|||||||
app is running in order to modify its behavior in some way.
|
app is running in order to modify its behavior in some way.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def on_app_launch(self) -> None:
|
def on_app_running(self) -> None:
|
||||||
"""Called when the app is being launched."""
|
"""Called when the app reaches the running state."""
|
||||||
|
|
||||||
def on_app_pause(self) -> None:
|
def on_app_pause(self) -> None:
|
||||||
"""Called after pausing game activity."""
|
"""Called after pausing game activity."""
|
||||||
|
|||||||
196
assets/src/ba_data/python/ba/_workspace.py
Normal file
196
assets/src/ba_data/python/ba/_workspace.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
# Released under the MIT License. See LICENSE for details.
|
||||||
|
#
|
||||||
|
"""Workspace related functionality."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Thread
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from efro.call import tpartial
|
||||||
|
from efro.error import CleanError
|
||||||
|
import _ba
|
||||||
|
import bacommon.cloud
|
||||||
|
from bacommon.transfer import DirectoryManifest
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import ba
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceSubsystem:
|
||||||
|
"""Subsystem for workspace handling in the app.
|
||||||
|
|
||||||
|
Category: **App Classes**
|
||||||
|
|
||||||
|
Access the single shared instance of this class at `ba.app.workspaces`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_active_workspace(
|
||||||
|
self,
|
||||||
|
workspaceid: str,
|
||||||
|
workspacename: str,
|
||||||
|
on_completed: Callable[[], None],
|
||||||
|
) -> None:
|
||||||
|
"""(internal)"""
|
||||||
|
|
||||||
|
# Do our work in a background thread so we don't destroy
|
||||||
|
# interactivity.
|
||||||
|
Thread(
|
||||||
|
target=lambda: self._set_active_workspace_bg(
|
||||||
|
workspaceid=workspaceid,
|
||||||
|
workspacename=workspacename,
|
||||||
|
on_completed=on_completed),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def _errmsg(self, msg: str | ba.Lstr) -> None:
|
||||||
|
_ba.screenmessage(msg, color=(1, 0, 0))
|
||||||
|
_ba.playsound(_ba.getsound('error'))
|
||||||
|
|
||||||
|
def _successmsg(self, msg: str | ba.Lstr) -> None:
|
||||||
|
_ba.screenmessage(msg, color=(0, 1, 0))
|
||||||
|
_ba.playsound(_ba.getsound('gunCocking'))
|
||||||
|
|
||||||
|
def _set_active_workspace_bg(self, workspaceid: str, workspacename: str,
|
||||||
|
on_completed: Callable[[], None]) -> None:
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
|
|
||||||
|
class _SkipSyncError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
set_path = True
|
||||||
|
wspath = Path(_ba.get_volatile_data_directory(), 'workspaces',
|
||||||
|
workspaceid)
|
||||||
|
try:
|
||||||
|
|
||||||
|
# If it seems we're offline, don't even attempt a sync,
|
||||||
|
# but allow using the previous synced state.
|
||||||
|
# (is this a good idea?)
|
||||||
|
if not _ba.app.cloud.is_connected():
|
||||||
|
raise _SkipSyncError()
|
||||||
|
|
||||||
|
manifest = DirectoryManifest.create_from_disk(wspath)
|
||||||
|
|
||||||
|
# FIXME: Should implement a way to pass account credentials in
|
||||||
|
# from the logic thread.
|
||||||
|
state = bacommon.cloud.WorkspaceFetchState(manifest=manifest)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
response = _ba.app.cloud.send_message(
|
||||||
|
bacommon.cloud.WorkspaceFetchMessage(
|
||||||
|
workspaceid=workspaceid, state=state))
|
||||||
|
state = response.state
|
||||||
|
self._handle_deletes(workspace_dir=wspath,
|
||||||
|
deletes=response.deletes)
|
||||||
|
self._handle_downloads_inline(
|
||||||
|
workspace_dir=wspath,
|
||||||
|
downloads_inline=response.downloads_inline)
|
||||||
|
if response.done:
|
||||||
|
# Server only deals in files; let's clean up any
|
||||||
|
# leftover empty dirs after the dust has cleared.
|
||||||
|
self._handle_dir_prune_empty(str(wspath))
|
||||||
|
break
|
||||||
|
state.iteration += 1
|
||||||
|
|
||||||
|
extras: list[str] = []
|
||||||
|
# Hmm; let's not show deletes for now since currently lots of
|
||||||
|
# .pyc files get deleted.
|
||||||
|
if bool(False):
|
||||||
|
if state.total_deletes:
|
||||||
|
extras.append(f'{state.total_deletes} files deleted')
|
||||||
|
if state.total_downloads:
|
||||||
|
extras.append(f'{state.total_downloads} files downloaded')
|
||||||
|
if state.total_up_to_date:
|
||||||
|
extras.append(f'{state.total_up_to_date} files up-to-date')
|
||||||
|
# Actually let's try with none of this; seems a bit excessive.
|
||||||
|
if bool(False) and extras:
|
||||||
|
extras_s = '\n' + ', '.join(extras) + '.'
|
||||||
|
else:
|
||||||
|
extras_s = ''
|
||||||
|
_ba.pushcall(tpartial(self._successmsg,
|
||||||
|
f'{workspacename} activated.{extras_s}'),
|
||||||
|
from_other_thread=True)
|
||||||
|
|
||||||
|
except _SkipSyncError:
|
||||||
|
_ba.pushcall(tpartial(
|
||||||
|
self._errmsg, f'Can\'t sync {workspacename}'
|
||||||
|
f'. Reusing previous synced version.'),
|
||||||
|
from_other_thread=True)
|
||||||
|
|
||||||
|
except CleanError as exc:
|
||||||
|
# Avoid reusing existing if we fail in the middle; could
|
||||||
|
# be in wonky state.
|
||||||
|
set_path = False
|
||||||
|
_ba.pushcall(tpartial(self._errmsg, str(exc)),
|
||||||
|
from_other_thread=True)
|
||||||
|
except Exception:
|
||||||
|
# Ditto.
|
||||||
|
set_path = False
|
||||||
|
logging.exception('Error syncing workspace.')
|
||||||
|
# TODO: Lstr.
|
||||||
|
_ba.pushcall(tpartial(
|
||||||
|
self._errmsg, 'Error syncing workspace. See log for details.'),
|
||||||
|
from_other_thread=True)
|
||||||
|
|
||||||
|
if set_path and wspath.is_dir():
|
||||||
|
# Add to Python paths and also to list of stuff to be scanned
|
||||||
|
# for meta tags.
|
||||||
|
sys.path.insert(0, str(wspath))
|
||||||
|
_ba.app.meta.extra_scan_dirs.append(str(wspath))
|
||||||
|
|
||||||
|
# Job's done!
|
||||||
|
_ba.pushcall(on_completed, from_other_thread=True)
|
||||||
|
|
||||||
|
def _handle_deletes(self, workspace_dir: Path, deletes: list[str]) -> None:
|
||||||
|
"""Handle file deletes."""
|
||||||
|
for fname in deletes:
|
||||||
|
fname = os.path.join(workspace_dir, fname)
|
||||||
|
# Server shouldn't be sending us dir paths here.
|
||||||
|
assert not os.path.isdir(fname)
|
||||||
|
os.unlink(fname)
|
||||||
|
|
||||||
|
def _handle_downloads_inline(
|
||||||
|
self,
|
||||||
|
workspace_dir: Path,
|
||||||
|
downloads_inline: dict[str, bytes],
|
||||||
|
) -> None:
|
||||||
|
"""Handle inline file data to be saved to the client."""
|
||||||
|
for fname, fdata in downloads_inline.items():
|
||||||
|
fname = os.path.join(workspace_dir, fname)
|
||||||
|
# If there's a directory where we want our file to go, clear it
|
||||||
|
# out first. File deletes should have run before this so
|
||||||
|
# everything under it should be empty and thus killable via rmdir.
|
||||||
|
if os.path.isdir(fname):
|
||||||
|
for basename, dirnames, _fn in os.walk(fname, topdown=False):
|
||||||
|
for dirname in dirnames:
|
||||||
|
os.rmdir(os.path.join(basename, dirname))
|
||||||
|
os.rmdir(fname)
|
||||||
|
|
||||||
|
dirname = os.path.dirname(fname)
|
||||||
|
if dirname:
|
||||||
|
os.makedirs(dirname, exist_ok=True)
|
||||||
|
with open(fname, 'wb') as outfile:
|
||||||
|
outfile.write(fdata)
|
||||||
|
|
||||||
|
def _handle_dir_prune_empty(self, prunedir: str) -> None:
|
||||||
|
"""Handle pruning empty directories."""
|
||||||
|
# Walk the tree bottom-up so we can properly kill recursive empty dirs.
|
||||||
|
for basename, dirnames, filenames in os.walk(prunedir, topdown=False):
|
||||||
|
# It seems that child dirs we kill during the walk are still
|
||||||
|
# listed when the parent dir is visited, so lets make sure
|
||||||
|
# to only acknowledge still-existing ones.
|
||||||
|
dirnames = [
|
||||||
|
d for d in dirnames
|
||||||
|
if os.path.exists(os.path.join(basename, d))
|
||||||
|
]
|
||||||
|
if not dirnames and not filenames and basename != prunedir:
|
||||||
|
os.rmdir(basename)
|
||||||
@ -2,4 +2,4 @@
|
|||||||
#
|
#
|
||||||
"""Ballistica standard library: games, UI, etc."""
|
"""Ballistica standard library: games, UI, etc."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Defines assault minigame."""
|
"""Defines assault minigame."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Defines a capture-the-flag game."""
|
"""Defines a capture-the-flag game."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Provides the chosen-one mini-game."""
|
"""Provides the chosen-one mini-game."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Provides the Conquest game."""
|
"""Provides the Conquest game."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""DeathMatch game and support classes."""
|
"""DeathMatch game and support classes."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Provides an easter egg hunt game."""
|
"""Provides an easter egg hunt game."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Elimination mini-game."""
|
"""Elimination mini-game."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Implements football games (both co-op and teams varieties)."""
|
"""Implements football games (both co-op and teams varieties)."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Hockey game and support classes."""
|
"""Hockey game and support classes."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Defines a keep-away game type."""
|
"""Defines a keep-away game type."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Defines the King of the Hill game."""
|
"""Defines the King of the Hill game."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Defines a bomb-dodging mini-game."""
|
"""Defines a bomb-dodging mini-game."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Provides Ninja Fight mini-game."""
|
"""Provides Ninja Fight mini-game."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Defines Race mini-game."""
|
"""Defines Race mini-game."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Implements Target Practice game."""
|
"""Implements Target Practice game."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
"""Defines a default keyboards."""
|
"""Defines a default keyboards."""
|
||||||
|
|
||||||
# ba_meta require api 6
|
# ba_meta require api 7
|
||||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@ -63,9 +63,9 @@ class V2SignInWindow(ba.Window):
|
|||||||
self._update_timer: Optional[ba.Timer] = None
|
self._update_timer: Optional[ba.Timer] = None
|
||||||
|
|
||||||
# Ask the cloud for a proxy login id.
|
# Ask the cloud for a proxy login id.
|
||||||
ba.app.cloud.send_message(bacommon.cloud.LoginProxyRequestMessage(),
|
ba.app.cloud.send_message_cb(bacommon.cloud.LoginProxyRequestMessage(),
|
||||||
on_response=ba.WeakCall(
|
on_response=ba.WeakCall(
|
||||||
self._on_proxy_request_response))
|
self._on_proxy_request_response))
|
||||||
|
|
||||||
def _on_proxy_request_response(
|
def _on_proxy_request_response(
|
||||||
self, response: Union[bacommon.cloud.LoginProxyRequestResponse,
|
self, response: Union[bacommon.cloud.LoginProxyRequestResponse,
|
||||||
@ -135,9 +135,10 @@ class V2SignInWindow(ba.Window):
|
|||||||
def _ask_for_status(self) -> None:
|
def _ask_for_status(self) -> None:
|
||||||
assert self._proxyid is not None
|
assert self._proxyid is not None
|
||||||
assert self._proxykey is not None
|
assert self._proxykey is not None
|
||||||
ba.app.cloud.send_message(bacommon.cloud.LoginProxyStateQueryMessage(
|
ba.app.cloud.send_message_cb(
|
||||||
proxyid=self._proxyid, proxykey=self._proxykey),
|
bacommon.cloud.LoginProxyStateQueryMessage(
|
||||||
on_response=ba.WeakCall(self._got_status))
|
proxyid=self._proxyid, proxykey=self._proxykey),
|
||||||
|
on_response=ba.WeakCall(self._got_status))
|
||||||
|
|
||||||
def _got_status(
|
def _got_status(
|
||||||
self, response: Union[bacommon.cloud.LoginProxyStateQueryResponse,
|
self, response: Union[bacommon.cloud.LoginProxyStateQueryResponse,
|
||||||
@ -163,7 +164,7 @@ class V2SignInWindow(ba.Window):
|
|||||||
# so it can clean up (not a huge deal if this fails)
|
# so it can clean up (not a huge deal if this fails)
|
||||||
assert self._proxyid is not None
|
assert self._proxyid is not None
|
||||||
try:
|
try:
|
||||||
ba.app.cloud.send_message(
|
ba.app.cloud.send_message_cb(
|
||||||
bacommon.cloud.LoginProxyCompleteMessage(
|
bacommon.cloud.LoginProxyCompleteMessage(
|
||||||
proxyid=self._proxyid),
|
proxyid=self._proxyid),
|
||||||
on_response=ba.WeakCall(self._proxy_complete_response))
|
on_response=ba.WeakCall(self._proxy_complete_response))
|
||||||
|
|||||||
@ -258,6 +258,7 @@
|
|||||||
<w>crashlytics</w>
|
<w>crashlytics</w>
|
||||||
<w>createbuilddirectory</w>
|
<w>createbuilddirectory</w>
|
||||||
<w>createtime</w>
|
<w>createtime</w>
|
||||||
|
<w>credstr</w>
|
||||||
<w>cresult</w>
|
<w>cresult</w>
|
||||||
<w>crom</w>
|
<w>crom</w>
|
||||||
<w>crosswire</w>
|
<w>crosswire</w>
|
||||||
@ -1209,6 +1210,7 @@
|
|||||||
<w>strlen</w>
|
<w>strlen</w>
|
||||||
<w>strs</w>
|
<w>strs</w>
|
||||||
<w>strtof</w>
|
<w>strtof</w>
|
||||||
|
<w>stuttery</w>
|
||||||
<w>subargs</w>
|
<w>subargs</w>
|
||||||
<w>subc</w>
|
<w>subc</w>
|
||||||
<w>subclsssing</w>
|
<w>subclsssing</w>
|
||||||
@ -1385,6 +1387,8 @@
|
|||||||
<w>wofocj</w>
|
<w>wofocj</w>
|
||||||
<w>wonkiness</w>
|
<w>wonkiness</w>
|
||||||
<w>woohoo</w>
|
<w>woohoo</w>
|
||||||
|
<w>workspaceid</w>
|
||||||
|
<w>workspacename</w>
|
||||||
<w>worldspace</w>
|
<w>worldspace</w>
|
||||||
<w>woutdir</w>
|
<w>woutdir</w>
|
||||||
<w>wprjp</w>
|
<w>wprjp</w>
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
namespace ballistica {
|
namespace ballistica {
|
||||||
|
|
||||||
// These are set automatically via script; don't modify them here.
|
// These are set automatically via script; don't modify them here.
|
||||||
const int kAppBuildNumber = 20604;
|
const int kAppBuildNumber = 20610;
|
||||||
const char* kAppVersion = "1.7.2";
|
const char* kAppVersion = "1.7.2";
|
||||||
|
|
||||||
// Our standalone globals.
|
// Our standalone globals.
|
||||||
|
|||||||
@ -154,7 +154,7 @@ static auto LoadOgg(const char* file_name, std::vector<char>* buffer,
|
|||||||
static void LoadCachedOgg(const char* file_name, std::vector<char>* buffer,
|
static void LoadCachedOgg(const char* file_name, std::vector<char>* buffer,
|
||||||
ALenum* format, ALsizei* freq) {
|
ALenum* format, ALsizei* freq) {
|
||||||
std::string sound_cache_dir =
|
std::string sound_cache_dir =
|
||||||
g_platform->GetConfigDirectory() + "/audiocache";
|
g_platform->GetVolatileDataDirectory() + BA_DIRSLASH + "audiocache";
|
||||||
static bool made_sound_cache_dir = false;
|
static bool made_sound_cache_dir = false;
|
||||||
if (!made_sound_cache_dir) {
|
if (!made_sound_cache_dir) {
|
||||||
g_platform->MakeDir(sound_cache_dir);
|
g_platform->MakeDir(sound_cache_dir);
|
||||||
|
|||||||
@ -19,7 +19,7 @@ class PlatformApple : public Platform {
|
|||||||
auto GetDeviceV1AccountUUIDPrefix() -> std::string override;
|
auto GetDeviceV1AccountUUIDPrefix() -> std::string override;
|
||||||
auto GetRealLegacyDeviceUUID(std::string* uuid) -> bool override;
|
auto GetRealLegacyDeviceUUID(std::string* uuid) -> bool override;
|
||||||
auto GenerateUUID() -> std::string override;
|
auto GenerateUUID() -> std::string override;
|
||||||
auto GetDefaultConfigDir() -> std::string override;
|
auto GetDefaultConfigDirectory() -> std::string override;
|
||||||
auto GetLocale() -> std::string override;
|
auto GetLocale() -> std::string override;
|
||||||
auto DoGetDeviceName() -> std::string override;
|
auto DoGetDeviceName() -> std::string override;
|
||||||
auto DoHasTouchScreen() -> bool override;
|
auto DoHasTouchScreen() -> bool override;
|
||||||
|
|||||||
@ -205,7 +205,7 @@ auto Platform::GetDeviceUUIDInputs() -> std::list<std::string> {
|
|||||||
throw Exception("GetDeviceUUIDInputs unimplemented");
|
throw Exception("GetDeviceUUIDInputs unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Platform::GetDefaultConfigDir() -> std::string {
|
auto Platform::GetDefaultConfigDirectory() -> std::string {
|
||||||
std::string config_dir;
|
std::string config_dir;
|
||||||
// As a default, look for a HOME env var and use that if present
|
// As a default, look for a HOME env var and use that if present
|
||||||
// this will cover linux and command-line macOS.
|
// this will cover linux and command-line macOS.
|
||||||
@ -213,7 +213,7 @@ auto Platform::GetDefaultConfigDir() -> std::string {
|
|||||||
if (home) {
|
if (home) {
|
||||||
config_dir = std::string(home) + "/.ballisticacore";
|
config_dir = std::string(home) + "/.ballisticacore";
|
||||||
} else {
|
} else {
|
||||||
printf("GetDefaultConfigDir: can't get env var \"HOME\"\n");
|
printf("GetDefaultConfigDirectory: can't get env var \"HOME\"\n");
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
throw Exception();
|
throw Exception();
|
||||||
}
|
}
|
||||||
@ -257,18 +257,25 @@ void Platform::SetLowLevelConfigValue(const char* key, int value) {
|
|||||||
|
|
||||||
auto Platform::GetUserPythonDirectory() -> std::string {
|
auto Platform::GetUserPythonDirectory() -> std::string {
|
||||||
// Make sure it exists the first time we run.
|
// Make sure it exists the first time we run.
|
||||||
static bool attempted_to_make_user_scripts_dir = false;
|
if (!attempted_to_make_user_scripts_dir_) {
|
||||||
|
|
||||||
if (!attempted_to_make_user_scripts_dir) {
|
|
||||||
user_scripts_dir_ = DoGetUserPythonDirectory();
|
user_scripts_dir_ = DoGetUserPythonDirectory();
|
||||||
|
|
||||||
// Attempt to make it. (it's ok if this fails)
|
// Attempt to make it. (it's ok if this fails)
|
||||||
MakeDir(user_scripts_dir_, true);
|
MakeDir(user_scripts_dir_, true);
|
||||||
attempted_to_make_user_scripts_dir = true;
|
attempted_to_make_user_scripts_dir_ = true;
|
||||||
}
|
}
|
||||||
return user_scripts_dir_;
|
return user_scripts_dir_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Platform::GetVolatileDataDirectory() -> std::string {
|
||||||
|
if (!made_volatile_data_dir_) {
|
||||||
|
volatile_data_dir_ = GetConfigDirectory() + BA_DIRSLASH + "vdata";
|
||||||
|
MakeDir(volatile_data_dir_);
|
||||||
|
made_volatile_data_dir_ = true;
|
||||||
|
}
|
||||||
|
return volatile_data_dir_;
|
||||||
|
}
|
||||||
|
|
||||||
auto Platform::GetAppPythonDirectory() -> std::string {
|
auto Platform::GetAppPythonDirectory() -> std::string {
|
||||||
static bool checked_dir = false;
|
static bool checked_dir = false;
|
||||||
if (!checked_dir) {
|
if (!checked_dir) {
|
||||||
@ -408,7 +415,7 @@ auto Platform::GetConfigDirectory() -> std::string {
|
|||||||
if (!g_app_globals->user_config_dir.empty()) {
|
if (!g_app_globals->user_config_dir.empty()) {
|
||||||
config_dir_ = g_app_globals->user_config_dir;
|
config_dir_ = g_app_globals->user_config_dir;
|
||||||
} else {
|
} else {
|
||||||
config_dir_ = GetDefaultConfigDir();
|
config_dir_ = GetDefaultConfigDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to make sure the config dir exists.
|
// Try to make sure the config dir exists.
|
||||||
@ -424,7 +431,7 @@ void Platform::MakeDir(const std::string& dir, bool quiet) {
|
|||||||
if (!exists) {
|
if (!exists) {
|
||||||
DoMakeDir(dir, quiet);
|
DoMakeDir(dir, quiet);
|
||||||
|
|
||||||
// Non-quiet call should result in directory existing.
|
// Non-quiet call should always result in the directory existing.
|
||||||
// (or an exception should have been raised)
|
// (or an exception should have been raised)
|
||||||
assert(quiet || FilePathExists(dir));
|
assert(quiet || FilePathExists(dir));
|
||||||
}
|
}
|
||||||
@ -1077,7 +1084,7 @@ void Platform::SignInV1(const std::string& account_type) {
|
|||||||
Log("SignInV1() unimplemented");
|
Log("SignInV1() unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Platform::LoginDidChange() {
|
void Platform::V1LoginDidChange() {
|
||||||
// Default is no-op.
|
// Default is no-op.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -76,7 +76,7 @@ class Platform {
|
|||||||
|
|
||||||
#pragma mark FILES -------------------------------------------------------------
|
#pragma mark FILES -------------------------------------------------------------
|
||||||
|
|
||||||
/// remove() support UTF8 strings.
|
/// remove() supporting UTF8 strings.
|
||||||
virtual auto Remove(const char* path) -> int;
|
virtual auto Remove(const char* path) -> int;
|
||||||
|
|
||||||
/// stat() supporting UTF8 strings.
|
/// stat() supporting UTF8 strings.
|
||||||
@ -93,8 +93,8 @@ class Platform {
|
|||||||
/// Simple cross-platform check for existence of a file.
|
/// Simple cross-platform check for existence of a file.
|
||||||
auto FilePathExists(const std::string& name) -> bool;
|
auto FilePathExists(const std::string& name) -> bool;
|
||||||
|
|
||||||
/// Attempt to make a directory; raise an Exception if unable,
|
/// Attempt to make a directory. Raise an Exception if unable,
|
||||||
/// unless quiet is true.
|
/// unless quiet is true. Succeeds if the directory already exists.
|
||||||
auto MakeDir(const std::string& dir, bool quiet = false) -> void;
|
auto MakeDir(const std::string& dir, bool quiet = false) -> void;
|
||||||
|
|
||||||
/// Return the current working directory.
|
/// Return the current working directory.
|
||||||
@ -145,22 +145,49 @@ class Platform {
|
|||||||
// With it off, we loop in Main() ourself.
|
// With it off, we loop in Main() ourself.
|
||||||
virtual auto IsEventPushMode() -> bool;
|
virtual auto IsEventPushMode() -> bool;
|
||||||
|
|
||||||
// Return the interface type based on the environment (phone, tablet, etc).
|
/// Return the interface type based on the environment (phone, tablet, etc).
|
||||||
virtual auto GetUIScale() -> UIScale;
|
virtual auto GetUIScale() -> UIScale;
|
||||||
|
|
||||||
|
/// Get the root config directory. This dir contains the app config file
|
||||||
|
/// and other data considered essential to the app install. This directory
|
||||||
|
/// should be included in OS backups.
|
||||||
auto GetConfigDirectory() -> std::string;
|
auto GetConfigDirectory() -> std::string;
|
||||||
|
|
||||||
|
/// Get the path of the app config file.
|
||||||
auto GetConfigFilePath() -> std::string;
|
auto GetConfigFilePath() -> std::string;
|
||||||
|
|
||||||
|
/// Get a directory where the app can store internal generated data.
|
||||||
|
/// This directory should not be included in backups and the app
|
||||||
|
/// should remain functional if this directory is completely cleared
|
||||||
|
/// between runs (though it is expected that things stay intact here
|
||||||
|
/// *while* the app is running).
|
||||||
|
auto GetVolatileDataDirectory() -> std::string;
|
||||||
|
|
||||||
|
/// Return a directory where the local user can manually place Python files
|
||||||
|
/// where they will be accessible by the app. When possible, this directory
|
||||||
|
/// should be in a place easily accessible to the user.
|
||||||
auto GetUserPythonDirectory() -> std::string;
|
auto GetUserPythonDirectory() -> std::string;
|
||||||
|
|
||||||
|
/// Return the directory where the app expects to find its bundled Python
|
||||||
|
/// files.
|
||||||
auto GetAppPythonDirectory() -> std::string;
|
auto GetAppPythonDirectory() -> std::string;
|
||||||
|
|
||||||
|
/// Return the directory where bundled 3rd party Python files live.
|
||||||
auto GetSitePythonDirectory() -> std::string;
|
auto GetSitePythonDirectory() -> std::string;
|
||||||
|
|
||||||
|
/// Return the directory where game replay files live.
|
||||||
auto GetReplaysDir() -> std::string;
|
auto GetReplaysDir() -> std::string;
|
||||||
|
|
||||||
// Return en_US or whatnot.
|
/// Return en_US or whatnot.
|
||||||
virtual auto GetLocale() -> std::string;
|
virtual auto GetLocale() -> std::string;
|
||||||
virtual void SetupDataDirectory();
|
|
||||||
virtual auto GetUserAgentString() -> std::string;
|
virtual auto GetUserAgentString() -> std::string;
|
||||||
virtual auto GetOSVersionString() -> std::string;
|
virtual auto GetOSVersionString() -> std::string;
|
||||||
|
|
||||||
|
// Chdir to wherever our bundled data lives.
|
||||||
|
// (note to self: should rejigger this to avoid the chdir).
|
||||||
|
virtual auto SetupDataDirectory() -> void;
|
||||||
|
|
||||||
/// Set an environment variable as utf8, overwriting if it already exists.
|
/// Set an environment variable as utf8, overwriting if it already exists.
|
||||||
/// Raises an exception on errors.
|
/// Raises an exception on errors.
|
||||||
virtual void SetEnv(const std::string& name, const std::string& value);
|
virtual void SetEnv(const std::string& name, const std::string& value);
|
||||||
@ -308,7 +335,7 @@ class Platform {
|
|||||||
virtual auto SignOutV1() -> void;
|
virtual auto SignOutV1() -> void;
|
||||||
|
|
||||||
virtual auto GameCenterLogin() -> void;
|
virtual auto GameCenterLogin() -> void;
|
||||||
virtual auto LoginDidChange() -> void;
|
virtual auto V1LoginDidChange() -> void;
|
||||||
|
|
||||||
/// Returns the ID to use for the device account.
|
/// Returns the ID to use for the device account.
|
||||||
auto GetDeviceV1AccountID() -> std::string;
|
auto GetDeviceV1AccountID() -> std::string;
|
||||||
@ -513,8 +540,7 @@ class Platform {
|
|||||||
virtual auto DoGetDeviceName() -> std::string;
|
virtual auto DoGetDeviceName() -> std::string;
|
||||||
|
|
||||||
/// Attempt to actually create a directory.
|
/// Attempt to actually create a directory.
|
||||||
/// Should not raise Exceptions if it already exists or
|
/// Should *not* raise Exceptions if it already exists or if quiet is true.
|
||||||
/// if quiet is true.
|
|
||||||
virtual auto DoMakeDir(const std::string& dir, bool quiet) -> void;
|
virtual auto DoMakeDir(const std::string& dir, bool quiet) -> void;
|
||||||
|
|
||||||
/// Attempt to actually get an abs path. This will only be called if
|
/// Attempt to actually get an abs path. This will only be called if
|
||||||
@ -526,7 +552,7 @@ class Platform {
|
|||||||
virtual auto DoGetUserPythonDirectory() -> std::string;
|
virtual auto DoGetUserPythonDirectory() -> std::string;
|
||||||
|
|
||||||
/// Return the default config directory for this platform.
|
/// Return the default config directory for this platform.
|
||||||
virtual auto GetDefaultConfigDir() -> std::string;
|
virtual auto GetDefaultConfigDirectory() -> std::string;
|
||||||
|
|
||||||
/// Generate a random UUID string.
|
/// Generate a random UUID string.
|
||||||
virtual auto GenerateUUID() -> std::string;
|
virtual auto GenerateUUID() -> std::string;
|
||||||
@ -545,11 +571,14 @@ class Platform {
|
|||||||
bool is_tegra_k1_{};
|
bool is_tegra_k1_{};
|
||||||
bool have_clipboard_is_supported_{};
|
bool have_clipboard_is_supported_{};
|
||||||
bool clipboard_is_supported_{};
|
bool clipboard_is_supported_{};
|
||||||
|
bool attempted_to_make_user_scripts_dir_{};
|
||||||
|
bool made_volatile_data_dir_{};
|
||||||
|
bool have_device_uuid_{};
|
||||||
millisecs_t starttime_{};
|
millisecs_t starttime_{};
|
||||||
std::string legacy_device_uuid_;
|
std::string legacy_device_uuid_;
|
||||||
bool have_device_uuid_{};
|
|
||||||
std::string config_dir_;
|
std::string config_dir_;
|
||||||
std::string user_scripts_dir_;
|
std::string user_scripts_dir_;
|
||||||
|
std::string volatile_data_dir_;
|
||||||
std::string app_python_dir_;
|
std::string app_python_dir_;
|
||||||
std::string site_python_dir_;
|
std::string site_python_dir_;
|
||||||
std::string replays_dir_;
|
std::string replays_dir_;
|
||||||
|
|||||||
@ -166,7 +166,7 @@ std::string PlatformWindows::GenerateUUID() {
|
|||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string PlatformWindows::GetDefaultConfigDir() {
|
std::string PlatformWindows::GetDefaultConfigDirectory() {
|
||||||
std::string config_dir;
|
std::string config_dir;
|
||||||
wchar_t* path;
|
wchar_t* path;
|
||||||
auto result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path);
|
auto result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path);
|
||||||
|
|||||||
@ -18,7 +18,7 @@ class PlatformWindows : public Platform {
|
|||||||
auto GetDeviceV1AccountUUIDPrefix() -> std::string override { return "w"; }
|
auto GetDeviceV1AccountUUIDPrefix() -> std::string override { return "w"; }
|
||||||
auto GetDeviceUUIDInputs() -> std::list<std::string> override;
|
auto GetDeviceUUIDInputs() -> std::list<std::string> override;
|
||||||
auto GenerateUUID() -> std::string override;
|
auto GenerateUUID() -> std::string override;
|
||||||
auto GetDefaultConfigDir() -> std::string override;
|
auto GetDefaultConfigDirectory() -> std::string override;
|
||||||
auto Remove(const char* path) -> int;
|
auto Remove(const char* path) -> int;
|
||||||
auto Stat(const char* path, struct BA_STAT* buffer) -> int;
|
auto Stat(const char* path, struct BA_STAT* buffer) -> int;
|
||||||
auto Rename(const char* oldname, const char* newname) -> int;
|
auto Rename(const char* oldname, const char* newname) -> int;
|
||||||
|
|||||||
@ -549,6 +549,13 @@ auto PyGetLogFilePath(PyObject* self, PyObject* args) -> PyObject* {
|
|||||||
BA_PYTHON_CATCH;
|
BA_PYTHON_CATCH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto PyGetVolatileDataDirectory(PyObject* self, PyObject* args) -> PyObject* {
|
||||||
|
BA_PYTHON_TRY;
|
||||||
|
Platform::SetLastPyCall("get_volatile_data_directory");
|
||||||
|
return PyUnicode_FromString(g_platform->GetVolatileDataDirectory().c_str());
|
||||||
|
BA_PYTHON_CATCH;
|
||||||
|
}
|
||||||
|
|
||||||
auto PyIsLogFull(PyObject* self, PyObject* args) -> PyObject* {
|
auto PyIsLogFull(PyObject* self, PyObject* args) -> PyObject* {
|
||||||
BA_PYTHON_TRY;
|
BA_PYTHON_TRY;
|
||||||
Platform::SetLastPyCall("is_log_full");
|
Platform::SetLastPyCall("is_log_full");
|
||||||
@ -966,6 +973,15 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
|
|||||||
"\n"
|
"\n"
|
||||||
"Return the path to the app log file."},
|
"Return the path to the app log file."},
|
||||||
|
|
||||||
|
{"get_volatile_data_directory", PyGetVolatileDataDirectory, METH_VARARGS,
|
||||||
|
"get_volatile_data_directory() -> str\n"
|
||||||
|
"\n"
|
||||||
|
"(internal)\n"
|
||||||
|
"\n"
|
||||||
|
"Return the path to the app volatile data directory.\n"
|
||||||
|
"This directory is for data generated by the app that does not\n"
|
||||||
|
"need to be backed up and can be recreated if necessary."},
|
||||||
|
|
||||||
{"set_platform_misc_read_vals", (PyCFunction)PySetPlatformMiscReadVals,
|
{"set_platform_misc_read_vals", (PyCFunction)PySetPlatformMiscReadVals,
|
||||||
METH_VARARGS | METH_KEYWORDS,
|
METH_VARARGS | METH_KEYWORDS,
|
||||||
"set_platform_misc_read_vals(mode: str) -> None\n"
|
"set_platform_misc_read_vals(mode: str) -> None\n"
|
||||||
|
|||||||
@ -787,8 +787,10 @@ def test_full_pipeline() -> None:
|
|||||||
outdict['_sidecar_data'] = getattr(msg, '_sidecar_data')
|
outdict['_sidecar_data'] = getattr(msg, '_sidecar_data')
|
||||||
|
|
||||||
@msg.decode_filter_method
|
@msg.decode_filter_method
|
||||||
def _decode_filter(self, indata: dict, response: Response) -> None:
|
def _decode_filter(self, message: Message, indata: dict,
|
||||||
|
response: Response) -> None:
|
||||||
"""Filter our incoming responses."""
|
"""Filter our incoming responses."""
|
||||||
|
del message # Unused.
|
||||||
if self.test_sidecar:
|
if self.test_sidecar:
|
||||||
setattr(response, '_sidecar_data', indata['_sidecar_data'])
|
setattr(response, '_sidecar_data', indata['_sidecar_data'])
|
||||||
|
|
||||||
@ -830,8 +832,10 @@ def test_full_pipeline() -> None:
|
|||||||
setattr(message, '_sidecar_data', indata['_sidecar_data'])
|
setattr(message, '_sidecar_data', indata['_sidecar_data'])
|
||||||
|
|
||||||
@receiver.encode_filter_method
|
@receiver.encode_filter_method
|
||||||
def _encode_filter(self, response: Response, outdict: dict) -> None:
|
def _encode_filter(self, message: Message | None, response: Response,
|
||||||
|
outdict: dict) -> None:
|
||||||
"""Filter our outgoing responses."""
|
"""Filter our outgoing responses."""
|
||||||
|
del message # Unused.
|
||||||
if self.test_sidecar:
|
if self.test_sidecar:
|
||||||
outdict['_sidecar_data'] = getattr(response, '_sidecar_data')
|
outdict['_sidecar_data'] = getattr(response, '_sidecar_data')
|
||||||
|
|
||||||
|
|||||||
@ -3,12 +3,13 @@
|
|||||||
"""Functionality related to cloud functionality."""
|
"""Functionality related to cloud functionality."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import TYPE_CHECKING, Annotated, Optional
|
from typing import TYPE_CHECKING, Annotated, Optional
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from efro.message import Message, Response
|
from efro.message import Message, Response
|
||||||
from efro.dataclassio import ioprepped, IOAttrs
|
from efro.dataclassio import ioprepped, IOAttrs
|
||||||
|
from bacommon.transfer import DirectoryManifest
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
pass
|
pass
|
||||||
@ -77,27 +78,57 @@ class LoginProxyCompleteMessage(Message):
|
|||||||
|
|
||||||
@ioprepped
|
@ioprepped
|
||||||
@dataclass
|
@dataclass
|
||||||
class AccountSessionReleaseMessage(Message):
|
class TestMessage(Message):
|
||||||
"""We're done using this particular session."""
|
"""Can I get some of that workspace action?"""
|
||||||
token: Annotated[str, IOAttrs('tk')]
|
testfoo: Annotated[int, IOAttrs('f')]
|
||||||
|
|
||||||
|
|
||||||
@ioprepped
|
|
||||||
@dataclass
|
|
||||||
class CredentialsCheckMessage(Message):
|
|
||||||
"""Are our current credentials valid?"""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_response_types(cls) -> list[type[Response]]:
|
def get_response_types(cls) -> list[type[Response]]:
|
||||||
return [CredentialsCheckResponse]
|
return [TestResponse]
|
||||||
|
|
||||||
|
|
||||||
@ioprepped
|
@ioprepped
|
||||||
@dataclass
|
@dataclass
|
||||||
class CredentialsCheckResponse(Response):
|
class TestResponse(Response):
|
||||||
"""Info returned when checking credentials."""
|
"""Here's that workspace you asked for, boss."""
|
||||||
|
|
||||||
verified: Annotated[bool, IOAttrs('v')]
|
testfoo: Annotated[int, IOAttrs('f')]
|
||||||
|
|
||||||
# Current account tag (good time to check if it has changed).
|
|
||||||
tag: Annotated[str, IOAttrs('t')]
|
@ioprepped
|
||||||
|
@dataclass
|
||||||
|
class WorkspaceFetchState:
|
||||||
|
"""Common state data for a workspace fetch."""
|
||||||
|
manifest: Annotated[DirectoryManifest, IOAttrs('m')]
|
||||||
|
iteration: Annotated[int, IOAttrs('i')] = 0
|
||||||
|
total_deletes: Annotated[int, IOAttrs('tdels')] = 0
|
||||||
|
total_downloads: Annotated[int, IOAttrs('tdlds')] = 0
|
||||||
|
total_up_to_date: Annotated[int | None, IOAttrs('tunmd')] = None
|
||||||
|
|
||||||
|
|
||||||
|
@ioprepped
|
||||||
|
@dataclass
|
||||||
|
class WorkspaceFetchMessage(Message):
|
||||||
|
"""Can I get some of that workspace action?"""
|
||||||
|
workspaceid: Annotated[str, IOAttrs('w')]
|
||||||
|
state: Annotated[WorkspaceFetchState, IOAttrs('s')]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_response_types(cls) -> list[type[Response]]:
|
||||||
|
return [WorkspaceFetchResponse]
|
||||||
|
|
||||||
|
|
||||||
|
@ioprepped
|
||||||
|
@dataclass
|
||||||
|
class WorkspaceFetchResponse(Response):
|
||||||
|
"""Here's that workspace you asked for, boss."""
|
||||||
|
|
||||||
|
state: Annotated[WorkspaceFetchState, IOAttrs('s')]
|
||||||
|
deletes: Annotated[list[str],
|
||||||
|
IOAttrs('dlt', store_default=False)] = field(
|
||||||
|
default_factory=list)
|
||||||
|
downloads_inline: Annotated[dict[str, bytes],
|
||||||
|
IOAttrs('dinl', store_default=False)] = field(
|
||||||
|
default_factory=dict)
|
||||||
|
|
||||||
|
done: Annotated[bool, IOAttrs('d')] = False
|
||||||
|
|||||||
@ -34,7 +34,6 @@ class DirectoryManifest:
|
|||||||
def create_from_disk(cls, path: Path) -> DirectoryManifest:
|
def create_from_disk(cls, path: Path) -> DirectoryManifest:
|
||||||
"""Create a manifest from a directory on disk."""
|
"""Create a manifest from a directory on disk."""
|
||||||
import hashlib
|
import hashlib
|
||||||
from multiprocessing import cpu_count
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
pathstr = str(path)
|
pathstr = str(path)
|
||||||
@ -65,7 +64,10 @@ class DirectoryManifest:
|
|||||||
filesize=filesize))
|
filesize=filesize))
|
||||||
|
|
||||||
# Now use all procs to hash the files efficiently.
|
# Now use all procs to hash the files efficiently.
|
||||||
with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
|
cpus = os.cpu_count()
|
||||||
|
if cpus is None:
|
||||||
|
cpus = 4
|
||||||
|
with ThreadPoolExecutor(max_workers=cpus) as executor:
|
||||||
return cls(files=dict(executor.map(_get_file_info, paths)))
|
return cls(files=dict(executor.map(_get_file_info, paths)))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@ -118,8 +118,9 @@ class _Outputter:
|
|||||||
if isinstance(extra_attrs, dict):
|
if isinstance(extra_attrs, dict):
|
||||||
if not _is_valid_for_codec(extra_attrs, self._codec):
|
if not _is_valid_for_codec(extra_attrs, self._codec):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f'Extra attrs on {fieldpath} contains data type(s)'
|
f'Extra attrs on \'{fieldpath}\' contains data type(s)'
|
||||||
f' not supported by json.')
|
f' not supported by \'{self._codec.value}\' codec:'
|
||||||
|
f' {extra_attrs}.')
|
||||||
if self._create:
|
if self._create:
|
||||||
assert out is not None
|
assert out is not None
|
||||||
out.update(extra_attrs)
|
out.update(extra_attrs)
|
||||||
|
|||||||
@ -42,12 +42,6 @@ class Response:
|
|||||||
# Some standard response types:
|
# Some standard response types:
|
||||||
|
|
||||||
|
|
||||||
class ErrorType(Enum):
|
|
||||||
"""Type of error that occurred in remote message handling."""
|
|
||||||
OTHER = 0
|
|
||||||
CLEAN = 1
|
|
||||||
|
|
||||||
|
|
||||||
@ioprepped
|
@ioprepped
|
||||||
@dataclass
|
@dataclass
|
||||||
class ErrorResponse(Response):
|
class ErrorResponse(Response):
|
||||||
@ -56,6 +50,13 @@ class ErrorResponse(Response):
|
|||||||
This type is unique in that it is not returned to the user; it
|
This type is unique in that it is not returned to the user; it
|
||||||
instead results in a local exception being raised.
|
instead results in a local exception being raised.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class ErrorType(Enum):
|
||||||
|
"""Type of error that occurred in remote message handling."""
|
||||||
|
OTHER = 0
|
||||||
|
CLEAN = 1
|
||||||
|
LOCAL = 2
|
||||||
|
|
||||||
error_message: Annotated[str, IOAttrs('m')]
|
error_message: Annotated[str, IOAttrs('m')]
|
||||||
error_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.OTHER
|
error_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.OTHER
|
||||||
|
|
||||||
|
|||||||
@ -15,8 +15,7 @@ from efro.error import CleanError
|
|||||||
from efro.dataclassio import (is_ioprepped_dataclass, dataclass_to_dict,
|
from efro.dataclassio import (is_ioprepped_dataclass, dataclass_to_dict,
|
||||||
dataclass_from_dict)
|
dataclass_from_dict)
|
||||||
from efro.message._message import (Message, Response, ErrorResponse,
|
from efro.message._message import (Message, Response, ErrorResponse,
|
||||||
EmptyResponse, ErrorType,
|
EmptyResponse, UnregisteredMessageIDError)
|
||||||
UnregisteredMessageIDError)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
@ -141,11 +140,11 @@ class MessageProtocol:
|
|||||||
# If anything goes wrong, return a ErrorResponse instead.
|
# If anything goes wrong, return a ErrorResponse instead.
|
||||||
if isinstance(exc, CleanError) and self.preserve_clean_errors:
|
if isinstance(exc, CleanError) and self.preserve_clean_errors:
|
||||||
return ErrorResponse(error_message=str(exc),
|
return ErrorResponse(error_message=str(exc),
|
||||||
error_type=ErrorType.CLEAN)
|
error_type=ErrorResponse.ErrorType.CLEAN)
|
||||||
return ErrorResponse(
|
return ErrorResponse(
|
||||||
error_message=(traceback.format_exc() if self.trusted_sender else
|
error_message=(traceback.format_exc() if self.trusted_sender else
|
||||||
'An unknown error has occurred.'),
|
'An unknown error has occurred.'),
|
||||||
error_type=ErrorType.OTHER)
|
error_type=ErrorResponse.ErrorType.OTHER)
|
||||||
|
|
||||||
def _to_dict(self, message: Any, ids_by_type: dict[type, int],
|
def _to_dict(self, message: Any, ids_by_type: dict[type, int],
|
||||||
opname: str) -> dict:
|
opname: str) -> dict:
|
||||||
|
|||||||
@ -15,7 +15,7 @@ from efro.message._message import (Message, Response, EmptyResponse,
|
|||||||
ErrorResponse, UnregisteredMessageIDError)
|
ErrorResponse, UnregisteredMessageIDError)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Any, Callable, Optional, Union, Awaitable
|
from typing import Any, Callable, Awaitable
|
||||||
|
|
||||||
from efro.message._protocol import MessageProtocol
|
from efro.message._protocol import MessageProtocol
|
||||||
|
|
||||||
@ -50,19 +50,19 @@ class MessageReceiver:
|
|||||||
def __init__(self, protocol: MessageProtocol) -> None:
|
def __init__(self, protocol: MessageProtocol) -> None:
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self._handlers: dict[type[Message], Callable] = {}
|
self._handlers: dict[type[Message], Callable] = {}
|
||||||
self._decode_filter_call: Optional[Callable[[Any, dict, Message],
|
self._decode_filter_call: Callable[[Any, dict, Message],
|
||||||
None]] = None
|
None] | None = None
|
||||||
self._encode_filter_call: Optional[Callable[[Any, Response, dict],
|
self._encode_filter_call: Callable[
|
||||||
None]] = None
|
[Any, Message | None, Response, dict], None] | None = None
|
||||||
|
|
||||||
# TODO: don't currently have async encode equivalent
|
# TODO: don't currently have async encode equivalent
|
||||||
# or either for sender; can add as needed.
|
# or either for sender; can add as needed.
|
||||||
self._decode_filter_async_call: Optional[Callable[
|
self._decode_filter_async_call: Callable[[Any, dict, Message],
|
||||||
[Any, dict, Message], Awaitable[None]]] = None
|
Awaitable[None]] | None = None
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
def register_handler(
|
def register_handler(
|
||||||
self, call: Callable[[Any, Message], Optional[Response]]) -> None:
|
self, call: Callable[[Any, Message], Response | None]) -> None:
|
||||||
"""Register a handler call.
|
"""Register a handler call.
|
||||||
|
|
||||||
The message type handled by the call is determined by its
|
The message type handled by the call is determined by its
|
||||||
@ -106,7 +106,7 @@ class MessageReceiver:
|
|||||||
assert issubclass(msgtype, Message)
|
assert issubclass(msgtype, Message)
|
||||||
|
|
||||||
ret = anns.get('return')
|
ret = anns.get('return')
|
||||||
responsetypes: tuple[Union[type[Any], type[None]], ...]
|
responsetypes: tuple[type[Any] | type[None], ...]
|
||||||
|
|
||||||
# Return types can be a single type or a union of types.
|
# Return types can be a single type or a union of types.
|
||||||
if isinstance(ret, (_GenericAlias, types.UnionType)):
|
if isinstance(ret, (_GenericAlias, types.UnionType)):
|
||||||
@ -178,8 +178,8 @@ class MessageReceiver:
|
|||||||
return call
|
return call
|
||||||
|
|
||||||
def encode_filter_method(
|
def encode_filter_method(
|
||||||
self, call: Callable[[Any, Response, dict], None]
|
self, call: Callable[[Any, Message | None, Response, dict], None]
|
||||||
) -> Callable[[Any, Response, dict], None]:
|
) -> Callable[[Any, Message | None, Response, dict], None]:
|
||||||
"""Function decorator for defining an encode filter.
|
"""Function decorator for defining an encode filter.
|
||||||
|
|
||||||
Encode filters can be used to add extra data to the message
|
Encode filters can be used to add extra data to the message
|
||||||
@ -202,42 +202,38 @@ class MessageReceiver:
|
|||||||
else:
|
else:
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
def _decode_incoming_message_base(
|
def _decode_incoming_message_base(self, bound_obj: Any,
|
||||||
self, bound_obj: Any,
|
msg: str) -> tuple[Any, dict, Message]:
|
||||||
msg: str) -> tuple[Any, dict, Message, type[Message]]:
|
|
||||||
# Decode the incoming message.
|
# Decode the incoming message.
|
||||||
msg_dict = self.protocol.decode_dict(msg)
|
msg_dict = self.protocol.decode_dict(msg)
|
||||||
msg_decoded = self.protocol.message_from_dict(msg_dict)
|
msg_decoded = self.protocol.message_from_dict(msg_dict)
|
||||||
msgtype = type(msg_decoded)
|
assert isinstance(msg_decoded, Message)
|
||||||
assert issubclass(msgtype, Message)
|
|
||||||
if self._decode_filter_call is not None:
|
if self._decode_filter_call is not None:
|
||||||
self._decode_filter_call(bound_obj, msg_dict, msg_decoded)
|
self._decode_filter_call(bound_obj, msg_dict, msg_decoded)
|
||||||
return bound_obj, msg_dict, msg_decoded, msgtype
|
return bound_obj, msg_dict, msg_decoded
|
||||||
|
|
||||||
def _decode_incoming_message(self, bound_obj: Any,
|
def _decode_incoming_message(self, bound_obj: Any, msg: str) -> Message:
|
||||||
msg: str) -> tuple[Message, type[Message]]:
|
bound_obj, _msg_dict, msg_decoded = (
|
||||||
bound_obj, _msg_dict, msg_decoded, msgtype = (
|
|
||||||
self._decode_incoming_message_base(bound_obj=bound_obj, msg=msg))
|
self._decode_incoming_message_base(bound_obj=bound_obj, msg=msg))
|
||||||
|
|
||||||
# If they've set an async filter but are calling sync
|
# If they've set an async filter but are calling sync
|
||||||
# handle_raw_message() its likely a bug.
|
# handle_raw_message() its likely a bug.
|
||||||
assert self._decode_filter_async_call is None
|
assert self._decode_filter_async_call is None
|
||||||
|
|
||||||
return msg_decoded, msgtype
|
return msg_decoded
|
||||||
|
|
||||||
async def _decode_incoming_message_async(
|
async def _decode_incoming_message_async(self, bound_obj: Any,
|
||||||
self, bound_obj: Any, msg: str) -> tuple[Message, type[Message]]:
|
msg: str) -> Message:
|
||||||
bound_obj, msg_dict, msg_decoded, msgtype = (
|
bound_obj, msg_dict, msg_decoded = (self._decode_incoming_message_base(
|
||||||
self._decode_incoming_message_base(bound_obj=bound_obj, msg=msg))
|
bound_obj=bound_obj, msg=msg))
|
||||||
|
|
||||||
if self._decode_filter_async_call is not None:
|
if self._decode_filter_async_call is not None:
|
||||||
await self._decode_filter_async_call(bound_obj, msg_dict,
|
await self._decode_filter_async_call(bound_obj, msg_dict,
|
||||||
msg_decoded)
|
msg_decoded)
|
||||||
return msg_decoded, msgtype
|
return msg_decoded
|
||||||
|
|
||||||
def encode_user_response(self, bound_obj: Any,
|
def encode_user_response(self, bound_obj: Any, message: Message,
|
||||||
response: Optional[Response],
|
response: Response | None) -> str:
|
||||||
msgtype: type[Message]) -> str:
|
|
||||||
"""Encode a response provided by the user for sending."""
|
"""Encode a response provided by the user for sending."""
|
||||||
|
|
||||||
# A return value of None equals EmptyResponse.
|
# A return value of None equals EmptyResponse.
|
||||||
@ -247,18 +243,21 @@ class MessageReceiver:
|
|||||||
assert isinstance(response, Response)
|
assert isinstance(response, Response)
|
||||||
# (user should never explicitly return error-responses)
|
# (user should never explicitly return error-responses)
|
||||||
assert not isinstance(response, ErrorResponse)
|
assert not isinstance(response, ErrorResponse)
|
||||||
assert type(response) in msgtype.get_response_types()
|
assert type(response) in message.get_response_types()
|
||||||
response_dict = self.protocol.response_to_dict(response)
|
response_dict = self.protocol.response_to_dict(response)
|
||||||
if self._encode_filter_call is not None:
|
if self._encode_filter_call is not None:
|
||||||
self._encode_filter_call(bound_obj, response, response_dict)
|
self._encode_filter_call(bound_obj, message, response,
|
||||||
|
response_dict)
|
||||||
return self.protocol.encode_dict(response_dict)
|
return self.protocol.encode_dict(response_dict)
|
||||||
|
|
||||||
def encode_error_response(self, bound_obj: Any, exc: Exception) -> str:
|
def encode_error_response(self, bound_obj: Any, message: Message | None,
|
||||||
|
exc: Exception) -> str:
|
||||||
"""Given an error, return a response ready for sending."""
|
"""Given an error, return a response ready for sending."""
|
||||||
response = self.protocol.error_to_response(exc)
|
response = self.protocol.error_to_response(exc)
|
||||||
response_dict = self.protocol.response_to_dict(response)
|
response_dict = self.protocol.response_to_dict(response)
|
||||||
if self._encode_filter_call is not None:
|
if self._encode_filter_call is not None:
|
||||||
self._encode_filter_call(bound_obj, response, response_dict)
|
self._encode_filter_call(bound_obj, message, response,
|
||||||
|
response_dict)
|
||||||
return self.protocol.encode_dict(response_dict)
|
return self.protocol.encode_dict(response_dict)
|
||||||
|
|
||||||
def handle_raw_message(self,
|
def handle_raw_message(self,
|
||||||
@ -273,21 +272,22 @@ class MessageReceiver:
|
|||||||
error responses returned to the sender.
|
error responses returned to the sender.
|
||||||
"""
|
"""
|
||||||
assert not self.is_async, "can't call sync handler on async receiver"
|
assert not self.is_async, "can't call sync handler on async receiver"
|
||||||
|
msg_decoded: Message | None = None
|
||||||
try:
|
try:
|
||||||
msg_decoded, msgtype = self._decode_incoming_message(
|
msg_decoded = self._decode_incoming_message(bound_obj, msg)
|
||||||
bound_obj, msg)
|
msgtype = type(msg_decoded)
|
||||||
handler = self._handlers.get(msgtype)
|
handler = self._handlers.get(msgtype)
|
||||||
if handler is None:
|
if handler is None:
|
||||||
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
|
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
|
||||||
response = handler(bound_obj, msg_decoded)
|
response = handler(bound_obj, msg_decoded)
|
||||||
assert isinstance(response, (Response, type(None)))
|
assert isinstance(response, (Response, type(None)))
|
||||||
return self.encode_user_response(bound_obj, response, msgtype)
|
return self.encode_user_response(bound_obj, msg_decoded, response)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if (raise_unregistered
|
if (raise_unregistered
|
||||||
and isinstance(exc, UnregisteredMessageIDError)):
|
and isinstance(exc, UnregisteredMessageIDError)):
|
||||||
raise
|
raise
|
||||||
return self.encode_error_response(bound_obj, exc)
|
return self.encode_error_response(bound_obj, msg_decoded, exc)
|
||||||
|
|
||||||
async def handle_raw_message_async(
|
async def handle_raw_message_async(
|
||||||
self,
|
self,
|
||||||
@ -299,21 +299,23 @@ class MessageReceiver:
|
|||||||
The return value is the raw response to the message.
|
The return value is the raw response to the message.
|
||||||
"""
|
"""
|
||||||
assert self.is_async, "can't call async handler on sync receiver"
|
assert self.is_async, "can't call async handler on sync receiver"
|
||||||
|
msg_decoded: Message | None = None
|
||||||
try:
|
try:
|
||||||
msg_decoded, msgtype = await self._decode_incoming_message_async(
|
msg_decoded = await self._decode_incoming_message_async(
|
||||||
bound_obj, msg)
|
bound_obj, msg)
|
||||||
|
msgtype = type(msg_decoded)
|
||||||
handler = self._handlers.get(msgtype)
|
handler = self._handlers.get(msgtype)
|
||||||
if handler is None:
|
if handler is None:
|
||||||
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
|
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
|
||||||
response = await handler(bound_obj, msg_decoded)
|
response = await handler(bound_obj, msg_decoded)
|
||||||
assert isinstance(response, (Response, type(None)))
|
assert isinstance(response, (Response, type(None)))
|
||||||
return self.encode_user_response(bound_obj, response, msgtype)
|
return self.encode_user_response(bound_obj, msg_decoded, response)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if (raise_unregistered
|
if (raise_unregistered
|
||||||
and isinstance(exc, UnregisteredMessageIDError)):
|
and isinstance(exc, UnregisteredMessageIDError)):
|
||||||
raise
|
raise
|
||||||
return self.encode_error_response(bound_obj, exc)
|
return self.encode_error_response(bound_obj, msg_decoded, exc)
|
||||||
|
|
||||||
|
|
||||||
class BoundMessageReceiver:
|
class BoundMessageReceiver:
|
||||||
@ -334,5 +336,12 @@ class BoundMessageReceiver:
|
|||||||
return self._receiver.protocol
|
return self._receiver.protocol
|
||||||
|
|
||||||
def encode_error_response(self, exc: Exception) -> str:
|
def encode_error_response(self, exc: Exception) -> str:
|
||||||
"""Given an error, return a response ready to send."""
|
"""Given an error, return a response ready to send.
|
||||||
return self._receiver.encode_error_response(self._obj, exc)
|
|
||||||
|
This should be used for any errors that happen outside of
|
||||||
|
of standard handle_raw_message calls. Any errors within those
|
||||||
|
calls should be automatically returned as encoded strings.
|
||||||
|
"""
|
||||||
|
# Passing None for Message here; we would only have that available
|
||||||
|
# for things going wrong in the handler (which this is not for).
|
||||||
|
return self._receiver.encode_error_response(self._obj, None, exc)
|
||||||
|
|||||||
@ -6,13 +6,14 @@ Supports static typing for message types and possible return types.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, TypeVar
|
from typing import TYPE_CHECKING, TypeVar
|
||||||
|
|
||||||
from efro.error import CleanError, RemoteError
|
from efro.error import CleanError, RemoteError
|
||||||
from efro.message._message import (EmptyResponse, ErrorResponse, ErrorType)
|
from efro.message._message import EmptyResponse, ErrorResponse
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Any, Callable, Optional, Awaitable
|
from typing import Any, Callable, Awaitable
|
||||||
|
|
||||||
from efro.message._message import Message, Response
|
from efro.message._message import Message, Response
|
||||||
from efro.message._protocol import MessageProtocol
|
from efro.message._protocol import MessageProtocol
|
||||||
@ -42,13 +43,13 @@ class MessageSender:
|
|||||||
|
|
||||||
def __init__(self, protocol: MessageProtocol) -> None:
|
def __init__(self, protocol: MessageProtocol) -> None:
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self._send_raw_message_call: Optional[Callable[[Any, str], str]] = None
|
self._send_raw_message_call: Callable[[Any, str], str] | None = None
|
||||||
self._send_async_raw_message_call: Optional[Callable[
|
self._send_async_raw_message_call: Callable[
|
||||||
[Any, str], Awaitable[str]]] = None
|
[Any, str], Awaitable[str]] | None = None
|
||||||
self._encode_filter_call: Optional[Callable[[Any, Message, dict],
|
self._encode_filter_call: Callable[[Any, Message, dict],
|
||||||
None]] = None
|
None] | None = None
|
||||||
self._decode_filter_call: Optional[Callable[[Any, dict, Response],
|
self._decode_filter_call: Callable[[Any, Message, dict, Response],
|
||||||
None]] = None
|
None] | None = None
|
||||||
|
|
||||||
def send_method(
|
def send_method(
|
||||||
self, call: Callable[[Any, str],
|
self, call: Callable[[Any, str],
|
||||||
@ -79,8 +80,8 @@ class MessageSender:
|
|||||||
return call
|
return call
|
||||||
|
|
||||||
def decode_filter_method(
|
def decode_filter_method(
|
||||||
self, call: Callable[[Any, dict, Response], None]
|
self, call: Callable[[Any, Message, dict, Response], None]
|
||||||
) -> Callable[[Any, dict, Response], None]:
|
) -> Callable[[Any, Message, dict, Response], None]:
|
||||||
"""Function decorator for defining a decode filter.
|
"""Function decorator for defining a decode filter.
|
||||||
|
|
||||||
Decode filters can be used to extract extra data from incoming
|
Decode filters can be used to extract extra data from incoming
|
||||||
@ -90,71 +91,137 @@ class MessageSender:
|
|||||||
self._decode_filter_call = call
|
self._decode_filter_call = call
|
||||||
return call
|
return call
|
||||||
|
|
||||||
def send(self, bound_obj: Any, message: Message) -> Optional[Response]:
|
def send(self, bound_obj: Any, message: Message) -> Response | None:
|
||||||
"""Send a message and receive a response.
|
"""Send a message synchronously."""
|
||||||
|
return self.send_split_part_2(
|
||||||
|
message=message,
|
||||||
|
raw_response=self.send_split_part_1(
|
||||||
|
bound_obj=bound_obj,
|
||||||
|
message=message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
Will encode the message for transport and call dispatch_raw_message()
|
async def send_async(self, bound_obj: Any,
|
||||||
|
message: Message) -> Response | None:
|
||||||
|
"""Send a message asynchronously."""
|
||||||
|
return self.send_split_part_2(
|
||||||
|
message=message,
|
||||||
|
raw_response=await self.send_split_part_1_async(
|
||||||
|
bound_obj=bound_obj,
|
||||||
|
message=message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_split_part_1(self, bound_obj: Any, message: Message) -> Response:
|
||||||
|
"""Send a message synchronously.
|
||||||
|
|
||||||
|
Generally you can just call send(); these split versions are
|
||||||
|
for when message sending and response handling need to happen
|
||||||
|
in different contexts/threads.
|
||||||
"""
|
"""
|
||||||
if self._send_raw_message_call is None:
|
if self._send_raw_message_call is None:
|
||||||
raise RuntimeError('send() is unimplemented for this type.')
|
raise RuntimeError('send() is unimplemented for this type.')
|
||||||
|
|
||||||
msg_encoded = self.encode_message(bound_obj, message)
|
msg_encoded = self._encode_message(bound_obj, message)
|
||||||
|
|
||||||
response_encoded = self._send_raw_message_call(bound_obj, msg_encoded)
|
response_encoded = self._send_raw_message_call(bound_obj, msg_encoded)
|
||||||
|
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||||
|
|
||||||
response = self.decode_response(bound_obj, response_encoded)
|
async def send_split_part_1_async(self, bound_obj: Any,
|
||||||
|
message: Message) -> Response:
|
||||||
|
"""Send a message asynchronously.
|
||||||
|
|
||||||
|
Generally you can just call send(); these split versions are
|
||||||
|
for when message sending and response handling need to happen
|
||||||
|
in different contexts/threads.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._send_async_raw_message_call is None:
|
||||||
|
raise RuntimeError('send_async() is unimplemented for this type.')
|
||||||
|
|
||||||
|
msg_encoded = self._encode_message(bound_obj, message)
|
||||||
|
response_encoded = await self._send_async_raw_message_call(
|
||||||
|
bound_obj, msg_encoded)
|
||||||
|
|
||||||
|
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||||
|
|
||||||
|
def send_split_part_2(self, message: Message,
|
||||||
|
raw_response: Response) -> Response | None:
|
||||||
|
"""Complete message sending (both sync and async).
|
||||||
|
|
||||||
|
Generally you can just call send(); these split versions are
|
||||||
|
for when message sending and response handling need to happen
|
||||||
|
in different contexts/threads.
|
||||||
|
"""
|
||||||
|
response = self._unpack_raw_response(raw_response)
|
||||||
assert (response is None
|
assert (response is None
|
||||||
or type(response) in type(message).get_response_types())
|
or type(response) in type(message).get_response_types())
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def encode_message(self, bound_obj: Any, message: Message) -> str:
|
def _encode_message(self, bound_obj: Any, message: Message) -> str:
|
||||||
"""Encode a message for sending."""
|
"""Encode a message for sending."""
|
||||||
msg_dict = self.protocol.message_to_dict(message)
|
msg_dict = self.protocol.message_to_dict(message)
|
||||||
if self._encode_filter_call is not None:
|
if self._encode_filter_call is not None:
|
||||||
self._encode_filter_call(bound_obj, message, msg_dict)
|
self._encode_filter_call(bound_obj, message, msg_dict)
|
||||||
return self.protocol.encode_dict(msg_dict)
|
return self.protocol.encode_dict(msg_dict)
|
||||||
|
|
||||||
def decode_response(self, bound_obj: Any,
|
def _decode_raw_response(self, bound_obj: Any, message: Message,
|
||||||
response_encoded: str) -> Optional[Response]:
|
response_encoded: str) -> Response:
|
||||||
"""Decode, filter, and possibly act on raw response data."""
|
"""Create a Response from returned data.
|
||||||
response_dict = self.protocol.decode_dict(response_encoded)
|
|
||||||
response = self.protocol.response_from_dict(response_dict)
|
|
||||||
if self._decode_filter_call is not None:
|
|
||||||
self._decode_filter_call(bound_obj, response_dict, response)
|
|
||||||
|
|
||||||
# Special case: if we get EmptyResponse, we simply return None.
|
These Responses may encapsulate things like remote errors and
|
||||||
if isinstance(response, EmptyResponse):
|
should not be handed directly to users. _unpack_raw_response()
|
||||||
|
should be used to translate to special values like None or raise
|
||||||
|
Exceptions. This function itself should never raise Exceptions.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response_dict = self.protocol.decode_dict(response_encoded)
|
||||||
|
response = self.protocol.response_from_dict(response_dict)
|
||||||
|
if self._decode_filter_call is not None:
|
||||||
|
self._decode_filter_call(bound_obj, message, response_dict,
|
||||||
|
response)
|
||||||
|
except Exception:
|
||||||
|
# If we got to this point, we successfully communicated
|
||||||
|
# with the other end so errors represent protocol mismatches
|
||||||
|
# or other invalid data. For now let's just log it but perhaps
|
||||||
|
# we'd want to somehow embed it in the ErrorResponse to be raised
|
||||||
|
# directly to the user later.
|
||||||
|
logging.exception('Error decoding raw response')
|
||||||
|
response = ErrorResponse(
|
||||||
|
error_message=
|
||||||
|
'Error decoding raw response; see log for details.',
|
||||||
|
error_type=ErrorResponse.ErrorType.LOCAL)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _unpack_raw_response(self, raw_response: Response) -> Response | None:
|
||||||
|
"""Given a raw Response, unpacks to special values or Exceptions.
|
||||||
|
|
||||||
|
The result of this call is what should be passed to users.
|
||||||
|
For complex messaging situations such as response callbacks
|
||||||
|
operating across different threads, this last stage should be
|
||||||
|
run such that any raised Exception is active when the callback
|
||||||
|
fires; not on the thread where the message was sent.
|
||||||
|
"""
|
||||||
|
# EmptyResponse translates to None
|
||||||
|
if isinstance(raw_response, EmptyResponse):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Special case: a remote error occurred. Raise a local Exception
|
# Some error occurred. Raise a local Exception for it.
|
||||||
# instead of returning the message.
|
if isinstance(raw_response, ErrorResponse):
|
||||||
if isinstance(response, ErrorResponse):
|
|
||||||
if (self.protocol.preserve_clean_errors
|
|
||||||
and response.error_type is ErrorType.CLEAN):
|
|
||||||
raise CleanError(response.error_message)
|
|
||||||
raise RemoteError(response.error_message)
|
|
||||||
|
|
||||||
return response
|
# If something went wrong on our end of the connection,
|
||||||
|
# don't say it was a remote error.
|
||||||
|
if raw_response.error_type is ErrorResponse.ErrorType.LOCAL:
|
||||||
|
raise RuntimeError(raw_response.error_message)
|
||||||
|
|
||||||
async def send_async(self, bound_obj: Any,
|
# If they want to support clean errors, do those.
|
||||||
message: Message) -> Optional[Response]:
|
if (self.protocol.preserve_clean_errors and
|
||||||
"""Send a message asynchronously using asyncio.
|
raw_response.error_type is ErrorResponse.ErrorType.CLEAN):
|
||||||
|
raise CleanError(raw_response.error_message)
|
||||||
|
|
||||||
The message will be encoded for transport and passed to
|
# In all other cases, just say something went wrong 'out there'.
|
||||||
dispatch_raw_message_async.
|
raise RemoteError(raw_response.error_message)
|
||||||
"""
|
|
||||||
if self._send_async_raw_message_call is None:
|
|
||||||
raise RuntimeError('send_async() is unimplemented for this type.')
|
|
||||||
|
|
||||||
msg_encoded = self.encode_message(bound_obj, message)
|
return raw_response
|
||||||
|
|
||||||
response_encoded = await self._send_async_raw_message_call(
|
|
||||||
bound_obj, msg_encoded)
|
|
||||||
|
|
||||||
response = self.decode_response(bound_obj, response_encoded)
|
|
||||||
assert (response is None
|
|
||||||
or type(response) in type(message).get_response_types())
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class BoundMessageSender:
|
class BoundMessageSender:
|
||||||
@ -171,20 +238,34 @@ class BoundMessageSender:
|
|||||||
"""Protocol associated with this sender."""
|
"""Protocol associated with this sender."""
|
||||||
return self._sender.protocol
|
return self._sender.protocol
|
||||||
|
|
||||||
def send_untyped(self, message: Message) -> Optional[Response]:
|
def send_untyped(self, message: Message) -> Response | None:
|
||||||
"""Send a message synchronously.
|
"""Send a message synchronously.
|
||||||
|
|
||||||
Whenever possible, use the send() call provided by generated
|
Whenever possible, use the send() call provided by generated
|
||||||
subclasses instead of this; it will provide better type safety.
|
subclasses instead of this; it will provide better type safety.
|
||||||
"""
|
"""
|
||||||
assert self._obj is not None
|
assert self._obj is not None
|
||||||
return self._sender.send(self._obj, message)
|
return self._sender.send(bound_obj=self._obj, message=message)
|
||||||
|
|
||||||
async def send_async_untyped(self, message: Message) -> Optional[Response]:
|
async def send_async_untyped(self, message: Message) -> Response | None:
|
||||||
"""Send a message asynchronously.
|
"""Send a message asynchronously.
|
||||||
|
|
||||||
Whenever possible, use the send_async() call provided by generated
|
Whenever possible, use the send_async() call provided by generated
|
||||||
subclasses instead of this; it will provide better type safety.
|
subclasses instead of this; it will provide better type safety.
|
||||||
"""
|
"""
|
||||||
assert self._obj is not None
|
assert self._obj is not None
|
||||||
return await self._sender.send_async(self._obj, message)
|
return await self._sender.send_async(bound_obj=self._obj,
|
||||||
|
message=message)
|
||||||
|
|
||||||
|
async def send_split_part_1_async_untyped(self,
|
||||||
|
message: Message) -> Response:
|
||||||
|
"""Split send (part 1 of 2)."""
|
||||||
|
assert self._obj is not None
|
||||||
|
return await self._sender.send_split_part_1_async(bound_obj=self._obj,
|
||||||
|
message=message)
|
||||||
|
|
||||||
|
def send_split_part_2_untyped(self, message: Message,
|
||||||
|
raw_response: Response) -> Response | None:
|
||||||
|
"""Split send (part 2 of 2)."""
|
||||||
|
return self._sender.send_split_part_2(message=message,
|
||||||
|
raw_response=raw_response)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user