enabling workspaces

This commit is contained in:
Eric Froemling 2022-06-21 22:56:21 -07:00
parent 670bd9d4fd
commit 616dd21265
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
48 changed files with 847 additions and 333 deletions

View File

@ -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/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/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/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",
@ -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/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/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/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/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/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/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/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",
@ -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/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/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/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",
@ -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/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",
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/5e/d0/559c7203ab5e64e2425b971162b9",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/dc/e6/cdef333cc7117b19a5e10ed36de0",
"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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/09/ac/e9ff2d475c39d838ed1841e38616",
"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/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a7/d7/ef66ddf0d143691238b6a6f380ca",
"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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b5/2c/393c5a2e020f8ba53c4ee6d64273",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f9/be/a4637598a814486b22a63cc73d73",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/19/97/b576a0683093a41ca9de4ded529c",
"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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e5/5e/3aae36d786cf5fceeb92ef08be09",
"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/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c3/dc/f1bbcd1052ca7625108cdf5ea651",
"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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1b/c6/48b825be351cd70902713413484f",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/b0/d5/046011a1c101ff4444f258757650",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/bc/be/52a4a6f939cda096692e9d9e9420",
"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/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/6d/da/8deb6def85dee6da1df55ab0d61e",
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b9/e0/488348d0a48c6d2eaec9f166a68a",
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/97/3e/8d862f251f36e3d43f83372e52e8",
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/70/28/c5da6acadd31a12098c0d90ced65",
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/73/16/fffb5234d28b5f331db364c1cf6b",
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b5/aa/a7fd6fbe303107be212ca18d19da",
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8b/e7/a4f4fb02098dce00f89214003293",
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0a/aa/ac941fe32bcdfa985949c5e5bf79",
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a5/de/a7047d5d833f8ebb62e383e5eaf4",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/ce/9c/e473fc15f9d3a0bedfb3913d1962",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/3f/ed/28da613b3e324d1430b237fb08a6",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/0a/7f/2cdc32fd26567e699debe0bc2664",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/a8/df/b80403b15d88e26fd33c37c0941f",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/48/98/24c3d9f63d6909320ee6152b2225",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/3d/1e/dc89ecda37bcf6ce72e8d19452d9",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/e9/ce/8b528bcfdc5dabd974c4693ccd63",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/34/33/2d5a7cff960d15324c98199a2d39",
"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/c6/35/10bcc2d9f1e00a74c078fc8629b0",
"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/80/b9/e5d4926246c0d4af6e960764b15a",
"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/82/21/da5e39f43bfc17b4359df24c74ac",
"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/bd/63/cee7a7dc0254a7581c999eade01d",
"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/b4/c6/acabf51f5f726884a5578eedb541",
"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/d0/ae/86503d64be9a5bac79a1d3a803b8",
"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/35/5e/5d36c2e57c4e5187d34db3d6b238",
"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/a8/dc/c711def084cef4494339f142ffae",
"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/38/d4/444ccd05b95225d7e0f94e6e647a",
"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/9e/2a/f5d9a3f66b5132796f711c625618",
"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/64/e2/0ed77a64f73a88087b3e59afe068",
"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/39/15/b5f145e0f448b44488a459437eed",
"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/d8/4d/cd07ed88ac012e0a2814ccce55b0",
"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/c6/ed/823544fb91e49f7b00aaee549c1d",
"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/33/a9/df1834efe475f2b074e0385cf43a",
"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/ee/6d/2d5d14ce2fd784ecd9008d635041",
"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/96/47/a95bf58c8ee9b1a6cf16c0bce38e",
"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/fe/e9/59f07ea132235d66772728a3f444",
"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/9c/c4/051b70035701c59410806b4a88f4",
"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/23/f9/e19797b50315d1c1db90aae74252",
"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/89/6b/2d5abd4ed36aacc160578ba1346c",
"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/43/0d/f0edc7b954a32d06f2b6f8f473d0",
"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"
}

View File

@ -58,6 +58,7 @@
<w>adisp</w>
<w>advertizing</w>
<w>aidl</w>
<w>aint</w>
<w>aioloop</w>
<w>aiomain</w>
<w>alarmsound</w>
@ -513,6 +514,7 @@
<w>createtime</w>
<w>creationflags</w>
<w>creditslist</w>
<w>credstr</w>
<w>cresult</w>
<w>cryptmodule</w>
<w>cryptosimple</w>
@ -2364,6 +2366,7 @@
<w>strs</w>
<w>strt</w>
<w>strval</w>
<w>stuttery</w>
<w>subargs</w>
<w>subc</w>
<w>subclassof</w>
@ -2753,6 +2756,8 @@
<w>woooo</w>
<w>workdir</w>
<w>workflows</w>
<w>workspaceid</w>
<w>workspacename</w>
<w>woutdir</w>
<w>wpath</w>
<w>wprjp</w>

View File

@ -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!)
- 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!)
- 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)
- V2 account logic fixes

View File

@ -60,6 +60,7 @@
"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__/_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__/internal.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/_tournament.py",
"ba_data/python/ba/_ui.py",
"ba_data/python/ba/_workspace.py",
"ba_data/python/ba/deprecated.py",
"ba_data/python/ba/internal.py",
"ba_data/python/ba/macmusicapp.py",

View File

@ -195,6 +195,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
build/ba_data/python/ba/_tips.py \
build/ba_data/python/ba/_tournament.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/internal.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__/_tournament.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__/internal.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/macmusicapp.cpython-310.opt-1.pyc \

View File

@ -1 +1 @@
217115660358712006436605949517410465969
109029534501022699603510994431677623146

View File

@ -1924,6 +1924,16 @@ def get_v2_fleet() -> 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:
@overload
def getactivity(doraise: Literal[True] = True) -> ba.Activity:

View File

@ -6,8 +6,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
from typing import Optional
pass
class AccountV2Subsystem:
@ -18,10 +20,20 @@ class AccountV2Subsystem:
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:
"""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."""
raise RuntimeError('This should be overridden.')
@ -35,17 +47,74 @@ class AccountV2Subsystem:
raise RuntimeError('This should be overridden.')
@property
def primary(self) -> Optional[AccountV2Handle]:
def primary(self) -> AccountV2Handle | None:
"""The primary account for the app, or None if not logged in."""
return None
def get_primary(self) -> Optional[AccountV2Handle]:
def do_get_primary(self) -> AccountV2Handle | None:
"""Internal - should be overridden by subclass."""
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:
"""Handle for interacting with a v2 account."""
def __init__(self) -> None:
self.tag = '?'
self.workspacename: str | None = None
self.workspaceid: str | None = None

View File

@ -19,10 +19,11 @@ from ba._accountv1 import AccountV1Subsystem
from ba._meta import MetadataSubsystem
from ba._ads import AdsSubsystem
from ba._net import NetworkSubsystem
from ba._workspace import WorkspaceSubsystem
if TYPE_CHECKING:
import asyncio
from typing import Optional, Any, Callable
from typing import Any, Callable
import ba
from ba._cloud import CloudSubsystem
@ -49,10 +50,21 @@ class App:
class State(Enum):
"""High level state the app can be in."""
# Python-level systems being inited but should not interact.
LAUNCHING = 0
RUNNING = 1
PAUSED = 2
SHUTTING_DOWN = 3
# Initial account logins, workspace & asset downloads, etc.
LOADING = 1
# Normal running state.
RUNNING = 2
# App is backgrounded or otherwise suspended.
PAUSED = 3
# App is shutting down.
SHUTTING_DOWN = 4
@property
def aioloop(self) -> asyncio.AbstractEventLoop:
@ -208,7 +220,9 @@ class App:
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
# Config.
@ -219,7 +233,7 @@ class App:
# refreshed/etc.
self.fg_state = 0
self._aioloop: Optional[asyncio.AbstractEventLoop] = None
self._aioloop: asyncio.AbstractEventLoop | None = None
self._env = _ba.env()
self.protocol_version: int = self._env['protocol_version']
@ -243,12 +257,12 @@ class App:
# Misc.
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.log_have_new = 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
# 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
# key values for the same controller type so we keep their mappings
# distinct.
self.input_map_hash: Optional[str] = None
self.input_map_hash: str | None = None
# Co-op Campaigns.
self.campaigns: dict[str, ba.Campaign] = {}
# Server Mode.
self.server: Optional[ba.ServerController] = None
self.server: ba.ServerController | None = None
self.meta = MetadataSubsystem()
self.accounts_v1 = AccountV1Subsystem()
@ -273,15 +287,16 @@ class App:
self.ui = UISubsystem()
self.ads = AdsSubsystem()
self.net = NetworkSubsystem()
self.workspaces = WorkspaceSubsystem()
# Lobby.
self.lobby_random_profile_index: int = 1
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.
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.
self.spaz_appearances: dict[str, spazappearance.Appearance] = {}
@ -300,19 +315,19 @@ class App:
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_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.invite_confirm_windows: list[Any] = [] # FIXME: Don't use Any.
self.store_layout: Optional[dict[str, list[dict[str, Any]]]] = None
self.store_items: Optional[dict[str, dict]] = None
self.pro_sale_start_time: Optional[int] = None
self.pro_sale_start_val: Optional[int] = None
self.store_layout: dict[str, list[dict[str, Any]]] | None = None
self.store_items: dict[str, dict] | None = None
self.pro_sale_start_time: int | None = None
self.pro_sale_start_val: int | None = None
self.delegate: Optional[ba.AppDelegate] = None
self._asyncio_timer: Optional[ba.Timer] = None
self.delegate: ba.AppDelegate | None = None
self._asyncio_timer: ba.Timer | None = None
def on_app_launch(self) -> None:
"""Runs after the app finishes bootstrapping.
"""Runs after the app finishes low level bootstrapping.
(internal)"""
# pylint: disable=cyclic-import
@ -398,19 +413,23 @@ class App:
if not self.headless_mode:
_ba.timer(3.0, check_special_offer, timetype=TimeType.REAL)
self.meta.on_app_launch()
self.accounts_v2.on_app_launch()
self.accounts_v1.on_app_launch()
self.plugins.on_app_launch()
# See note below in on_app_pause.
if self.state != self.State.LAUNCHING:
logging.error('on_app_launch found state %s; expected LAUNCHING.',
self.state)
self._app_launched = True
self._launch_completed = True
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
# test_depset()
if bool(False):
@ -420,8 +439,13 @@ class App:
if self._app_paused:
self.state = self.State.PAUSED
else:
if self._app_launched:
if self._initial_login_completed:
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:
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
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
and not _ba.have_connected_clients()):
from ba._language import Lstr
@ -523,7 +547,7 @@ class App:
# If we're in a host-session, tell them to end.
# 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:
# 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.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:
"""Testing https support.

View File

@ -11,7 +11,7 @@ import _ba
if TYPE_CHECKING:
from typing import Callable, Any
from efro.message import Message
from efro.message import Message, Response
import bacommon.cloud
# TODO: Should make it possible to define a protocol in bacommon.cloud and
@ -20,7 +20,7 @@ if TYPE_CHECKING:
class CloudSubsystem:
"""Used for communicating with the cloud."""
"""Manages communication with cloud components."""
def is_connected(self) -> bool:
"""Return whether a connection to the cloud is present.
@ -31,7 +31,7 @@ class CloudSubsystem:
return False # Needs to be overridden
@overload
def send_message(
def send_message_cb(
self,
msg: bacommon.cloud.LoginProxyRequestMessage,
on_response: Callable[
@ -40,7 +40,7 @@ class CloudSubsystem:
...
@overload
def send_message(
def send_message_cb(
self,
msg: bacommon.cloud.LoginProxyStateQueryMessage,
on_response: Callable[
@ -49,36 +49,19 @@ class CloudSubsystem:
...
@overload
def send_message(
def send_message_cb(
self,
msg: bacommon.cloud.LoginProxyCompleteMessage,
on_response: Callable[[None | Exception], None],
) -> None:
...
@overload
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(
def send_message_cb(
self,
msg: Message,
on_response: Callable[[Any], 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
and passed either the response or the error that occurred.
@ -89,3 +72,22 @@ class CloudSubsystem:
_ba.pushcall(
Call(on_response,
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.')

View File

@ -6,22 +6,21 @@ from __future__ import annotations
import os
import time
import pathlib
import threading
from pathlib import Path
from typing import TYPE_CHECKING
from dataclasses import dataclass, field
import _ba
if TYPE_CHECKING:
from typing import Union, Optional
import ba
# The meta api version of this build of the game.
# Only packages and modules requiring this exact api version
# will be considered when scanning directories.
# See: https://ballistica.net/wiki/Meta-Tags
CURRENT_API_VERSION = 6
CURRENT_API_VERSION = 7
@dataclass
@ -43,10 +42,11 @@ class MetadataSubsystem:
"""
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:
"""Should be called when the app is done bootstrapping."""
def on_app_running(self) -> None:
"""Should be called when the app enters the running state."""
# Start scanning for things exposed via ba_meta.
self.start_scan()
@ -58,7 +58,8 @@ class MetadataSubsystem:
app = _ba.app
if self.metascan is not None:
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.start()
@ -99,16 +100,10 @@ class MetadataSubsystem:
class_path=class_path,
available=True))
if class_path not in plugstates:
if _ba.app.headless_mode:
# If we running in headless mode, enable plugin by default
# to allow server admins to get their modified build
# working 'out-of-the-box', without manually updating the
# 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}
# Go ahead and enable new plugins by default, but we'll
# inform the user that they need to restart to pick them up.
# they can also disable them in settings so they never load.
plugstates[class_path] = {'enabled': True}
config_changed = True
found_new = True
@ -223,18 +218,17 @@ class DirectoryScan:
"""
# 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()
def _get_path_module_entries(
self, path: pathlib.Path, subpath: Union[str, pathlib.Path],
modules: list[tuple[pathlib.Path, pathlib.Path]]) -> None:
def _get_path_module_entries(self, path: Path, subpath: str | Path,
modules: list[tuple[Path, Path]]) -> None:
"""Scan provided path and add module entries to provided list."""
try:
# Special case: let's save some time and skip the whole 'ba'
# package since we know it doesn't contain any meta tags.
fullpath = pathlib.Path(path, subpath)
entries = [(path, pathlib.Path(subpath, name))
fullpath = Path(path, subpath)
entries = [(path, Path(subpath, name))
for name in os.listdir(fullpath) if name != 'ba']
except PermissionError:
# Expected sometimes.
@ -248,13 +242,13 @@ class DirectoryScan:
for entry in entries:
if entry[1].name.endswith('.py'):
modules.append(entry)
elif (pathlib.Path(entry[0], entry[1]).is_dir() and pathlib.Path(
entry[0], entry[1], '__init__.py').is_file()):
elif (Path(entry[0], entry[1]).is_dir()
and Path(entry[0], entry[1], '__init__.py').is_file()):
modules.append(entry)
def scan(self) -> None:
"""Scan provided paths."""
modules: list[tuple[pathlib.Path, pathlib.Path]] = []
modules: list[tuple[Path, Path]] = []
for path in self.paths:
self._get_path_module_entries(path, '', modules)
for moduledir, subpath in modules:
@ -269,14 +263,13 @@ class DirectoryScan:
self.results.games.sort()
self.results.plugins.sort()
def scan_module(self, moduledir: pathlib.Path,
subpath: pathlib.Path) -> None:
def scan_module(self, moduledir: Path, subpath: Path) -> None:
"""Scan an individual module and add the findings to results."""
if subpath.name.endswith('.py'):
fpath = pathlib.Path(moduledir, subpath)
fpath = Path(moduledir, subpath)
ispackage = False
else:
fpath = pathlib.Path(moduledir, subpath, '__init__.py')
fpath = Path(moduledir, subpath, '__init__.py')
ispackage = True
with fpath.open(encoding='utf-8') as infile:
flines = infile.readlines()
@ -305,7 +298,7 @@ class DirectoryScan:
# If its a package, recurse into its subpackages.
if ispackage:
try:
submodules: list[tuple[pathlib.Path, pathlib.Path]] = []
submodules: list[tuple[Path, Path]] = []
self._get_path_module_entries(moduledir, subpath, submodules)
for submodule in submodules:
if submodule[1].name != '__init__.py':
@ -315,8 +308,7 @@ class DirectoryScan:
self.results.warnings += (
f"Error scanning '{subpath}': {traceback.format_exc()}\n")
def _process_module_meta_tags(self, subpath: pathlib.Path,
flines: list[str],
def _process_module_meta_tags(self, subpath: Path, flines: list[str],
meta_lines: dict[int, list[str]]) -> None:
"""Pull data from a module based on its ba_meta tags."""
for lindex, mline in meta_lines.items():
@ -360,8 +352,8 @@ class DirectoryScan:
': unrecognized export type "' + exporttype +
'" on line ' + str(lindex + 1) + '.\n')
def _get_export_class_name(self, subpath: pathlib.Path, lines: list[str],
lindex: int) -> Optional[str]:
def _get_export_class_name(self, subpath: Path, lines: list[str],
lindex: int) -> str | None:
"""Given line num of an export tag, returns its operand class name."""
lindexorig = lindex
classname = None
@ -386,9 +378,12 @@ class DirectoryScan:
str(lindexorig + 1) + '.\n')
return classname
def get_api_requirement(self, subpath: pathlib.Path,
meta_lines: dict[int, list[str]],
toplevel: bool) -> Optional[int]:
def get_api_requirement(
self,
subpath: Path,
meta_lines: dict[int, list[str]],
toplevel: bool,
) -> int | None:
"""Return an API requirement integer or None if none present.
Malformed api requirement strings will be logged as warnings.

View File

@ -25,16 +25,16 @@ class PluginSubsystem:
self.potential_plugins: list[ba.PotentialPlugin] = []
self.active_plugins: dict[str, ba.Plugin] = {}
def on_app_launch(self) -> None:
"""Should be called at app launch time."""
# Load up our plugins and go ahead and call their on_app_launch calls.
def on_app_running(self) -> None:
"""Should be called when the app reaches the running state."""
# Load up our plugins and go ahead and call their on_app_running calls.
self.load_plugins()
for plugin in self.active_plugins.values():
try:
plugin.on_app_launch()
plugin.on_app_running()
except Exception:
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:
"""Called when the app goes to a suspended state."""
@ -80,16 +80,23 @@ class PluginSubsystem:
try:
cls = getclass(plugkey, Plugin)
except Exception as exc:
_ba.log(f"Error loading plugin class '{plugkey}': {exc}",
to_server=False)
_ba.playsound(_ba.getsound('error'))
# TODO: Lstr.
errstr = f"Error loading plugin class '{plugkey}': {exc}"
_ba.screenmessage(errstr, color=(1, 0, 0))
_ba.log(errstr, to_server=False)
continue
try:
plugin = cls()
assert plugkey not in self.active_plugins
self.active_plugins[plugkey] = plugin
except Exception:
except Exception as exc:
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
@ -119,8 +126,8 @@ class Plugin:
app is running in order to modify its behavior in some way.
"""
def on_app_launch(self) -> None:
"""Called when the app is being launched."""
def on_app_running(self) -> None:
"""Called when the app reaches the running state."""
def on_app_pause(self) -> None:
"""Called after pausing game activity."""

View 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)

View File

@ -2,4 +2,4 @@
#
"""Ballistica standard library: games, UI, etc."""
# ba_meta require api 6
# ba_meta require api 7

View File

@ -2,7 +2,7 @@
#
"""Defines assault minigame."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Defines a capture-the-flag game."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Provides the chosen-one mini-game."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Provides the Conquest game."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""DeathMatch game and support classes."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Provides an easter egg hunt game."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Elimination mini-game."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""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)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Hockey game and support classes."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Defines a keep-away game type."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""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)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Defines a bomb-dodging mini-game."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Provides Ninja Fight mini-game."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Defines Race mini-game."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Implements Target Practice game."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -2,7 +2,7 @@
#
"""Defines a default keyboards."""
# ba_meta require api 6
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations

View File

@ -63,9 +63,9 @@ class V2SignInWindow(ba.Window):
self._update_timer: Optional[ba.Timer] = None
# Ask the cloud for a proxy login id.
ba.app.cloud.send_message(bacommon.cloud.LoginProxyRequestMessage(),
on_response=ba.WeakCall(
self._on_proxy_request_response))
ba.app.cloud.send_message_cb(bacommon.cloud.LoginProxyRequestMessage(),
on_response=ba.WeakCall(
self._on_proxy_request_response))
def _on_proxy_request_response(
self, response: Union[bacommon.cloud.LoginProxyRequestResponse,
@ -135,9 +135,10 @@ class V2SignInWindow(ba.Window):
def _ask_for_status(self) -> None:
assert self._proxyid is not None
assert self._proxykey is not None
ba.app.cloud.send_message(bacommon.cloud.LoginProxyStateQueryMessage(
proxyid=self._proxyid, proxykey=self._proxykey),
on_response=ba.WeakCall(self._got_status))
ba.app.cloud.send_message_cb(
bacommon.cloud.LoginProxyStateQueryMessage(
proxyid=self._proxyid, proxykey=self._proxykey),
on_response=ba.WeakCall(self._got_status))
def _got_status(
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)
assert self._proxyid is not None
try:
ba.app.cloud.send_message(
ba.app.cloud.send_message_cb(
bacommon.cloud.LoginProxyCompleteMessage(
proxyid=self._proxyid),
on_response=ba.WeakCall(self._proxy_complete_response))

View File

@ -258,6 +258,7 @@
<w>crashlytics</w>
<w>createbuilddirectory</w>
<w>createtime</w>
<w>credstr</w>
<w>cresult</w>
<w>crom</w>
<w>crosswire</w>
@ -1209,6 +1210,7 @@
<w>strlen</w>
<w>strs</w>
<w>strtof</w>
<w>stuttery</w>
<w>subargs</w>
<w>subc</w>
<w>subclsssing</w>
@ -1385,6 +1387,8 @@
<w>wofocj</w>
<w>wonkiness</w>
<w>woohoo</w>
<w>workspaceid</w>
<w>workspacename</w>
<w>worldspace</w>
<w>woutdir</w>
<w>wprjp</w>

View File

@ -21,7 +21,7 @@
namespace ballistica {
// 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";
// Our standalone globals.

View File

@ -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,
ALenum* format, ALsizei* freq) {
std::string sound_cache_dir =
g_platform->GetConfigDirectory() + "/audiocache";
g_platform->GetVolatileDataDirectory() + BA_DIRSLASH + "audiocache";
static bool made_sound_cache_dir = false;
if (!made_sound_cache_dir) {
g_platform->MakeDir(sound_cache_dir);

View File

@ -19,7 +19,7 @@ class PlatformApple : public Platform {
auto GetDeviceV1AccountUUIDPrefix() -> std::string override;
auto GetRealLegacyDeviceUUID(std::string* uuid) -> bool override;
auto GenerateUUID() -> std::string override;
auto GetDefaultConfigDir() -> std::string override;
auto GetDefaultConfigDirectory() -> std::string override;
auto GetLocale() -> std::string override;
auto DoGetDeviceName() -> std::string override;
auto DoHasTouchScreen() -> bool override;

View File

@ -205,7 +205,7 @@ auto Platform::GetDeviceUUIDInputs() -> std::list<std::string> {
throw Exception("GetDeviceUUIDInputs unimplemented");
}
auto Platform::GetDefaultConfigDir() -> std::string {
auto Platform::GetDefaultConfigDirectory() -> std::string {
std::string config_dir;
// As a default, look for a HOME env var and use that if present
// this will cover linux and command-line macOS.
@ -213,7 +213,7 @@ auto Platform::GetDefaultConfigDir() -> std::string {
if (home) {
config_dir = std::string(home) + "/.ballisticacore";
} else {
printf("GetDefaultConfigDir: can't get env var \"HOME\"\n");
printf("GetDefaultConfigDirectory: can't get env var \"HOME\"\n");
fflush(stdout);
throw Exception();
}
@ -257,18 +257,25 @@ void Platform::SetLowLevelConfigValue(const char* key, int value) {
auto Platform::GetUserPythonDirectory() -> std::string {
// 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();
// Attempt to make it. (it's ok if this fails)
MakeDir(user_scripts_dir_, true);
attempted_to_make_user_scripts_dir = true;
attempted_to_make_user_scripts_dir_ = true;
}
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 {
static bool checked_dir = false;
if (!checked_dir) {
@ -408,7 +415,7 @@ auto Platform::GetConfigDirectory() -> std::string {
if (!g_app_globals->user_config_dir.empty()) {
config_dir_ = g_app_globals->user_config_dir;
} else {
config_dir_ = GetDefaultConfigDir();
config_dir_ = GetDefaultConfigDirectory();
}
// Try to make sure the config dir exists.
@ -424,7 +431,7 @@ void Platform::MakeDir(const std::string& dir, bool quiet) {
if (!exists) {
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)
assert(quiet || FilePathExists(dir));
}
@ -1077,7 +1084,7 @@ void Platform::SignInV1(const std::string& account_type) {
Log("SignInV1() unimplemented");
}
void Platform::LoginDidChange() {
void Platform::V1LoginDidChange() {
// Default is no-op.
}

View File

@ -76,7 +76,7 @@ class Platform {
#pragma mark FILES -------------------------------------------------------------
/// remove() support UTF8 strings.
/// remove() supporting UTF8 strings.
virtual auto Remove(const char* path) -> int;
/// stat() supporting UTF8 strings.
@ -93,8 +93,8 @@ class Platform {
/// Simple cross-platform check for existence of a file.
auto FilePathExists(const std::string& name) -> bool;
/// Attempt to make a directory; raise an Exception if unable,
/// unless quiet is true.
/// Attempt to make a directory. Raise an Exception if unable,
/// unless quiet is true. Succeeds if the directory already exists.
auto MakeDir(const std::string& dir, bool quiet = false) -> void;
/// Return the current working directory.
@ -145,22 +145,49 @@ class Platform {
// With it off, we loop in Main() ourself.
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;
/// Get the root config directory. This dir contains the app config file
/// and other data considered essential to the app install. This directory
/// should be included in OS backups.
auto GetConfigDirectory() -> std::string;
/// Get the path of the app config file.
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;
/// Return the directory where the app expects to find its bundled Python
/// files.
auto GetAppPythonDirectory() -> std::string;
/// Return the directory where bundled 3rd party Python files live.
auto GetSitePythonDirectory() -> std::string;
/// Return the directory where game replay files live.
auto GetReplaysDir() -> std::string;
// Return en_US or whatnot.
/// Return en_US or whatnot.
virtual auto GetLocale() -> std::string;
virtual void SetupDataDirectory();
virtual auto GetUserAgentString() -> 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.
/// Raises an exception on errors.
virtual void SetEnv(const std::string& name, const std::string& value);
@ -308,7 +335,7 @@ class Platform {
virtual auto SignOutV1() -> void;
virtual auto GameCenterLogin() -> void;
virtual auto LoginDidChange() -> void;
virtual auto V1LoginDidChange() -> void;
/// Returns the ID to use for the device account.
auto GetDeviceV1AccountID() -> std::string;
@ -513,8 +540,7 @@ class Platform {
virtual auto DoGetDeviceName() -> std::string;
/// Attempt to actually create a directory.
/// Should not raise Exceptions if it already exists or
/// if quiet is true.
/// Should *not* raise Exceptions if it already exists or if quiet is true.
virtual auto DoMakeDir(const std::string& dir, bool quiet) -> void;
/// Attempt to actually get an abs path. This will only be called if
@ -526,7 +552,7 @@ class Platform {
virtual auto DoGetUserPythonDirectory() -> std::string;
/// Return the default config directory for this platform.
virtual auto GetDefaultConfigDir() -> std::string;
virtual auto GetDefaultConfigDirectory() -> std::string;
/// Generate a random UUID string.
virtual auto GenerateUUID() -> std::string;
@ -545,11 +571,14 @@ class Platform {
bool is_tegra_k1_{};
bool have_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_{};
std::string legacy_device_uuid_;
bool have_device_uuid_{};
std::string config_dir_;
std::string user_scripts_dir_;
std::string volatile_data_dir_;
std::string app_python_dir_;
std::string site_python_dir_;
std::string replays_dir_;

View File

@ -166,7 +166,7 @@ std::string PlatformWindows::GenerateUUID() {
return val;
}
std::string PlatformWindows::GetDefaultConfigDir() {
std::string PlatformWindows::GetDefaultConfigDirectory() {
std::string config_dir;
wchar_t* path;
auto result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path);

View File

@ -18,7 +18,7 @@ class PlatformWindows : public Platform {
auto GetDeviceV1AccountUUIDPrefix() -> std::string override { return "w"; }
auto GetDeviceUUIDInputs() -> std::list<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 Stat(const char* path, struct BA_STAT* buffer) -> int;
auto Rename(const char* oldname, const char* newname) -> int;

View File

@ -549,6 +549,13 @@ auto PyGetLogFilePath(PyObject* self, PyObject* args) -> PyObject* {
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* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("is_log_full");
@ -966,6 +973,15 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
"\n"
"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,
METH_VARARGS | METH_KEYWORDS,
"set_platform_misc_read_vals(mode: str) -> None\n"

View File

@ -787,8 +787,10 @@ def test_full_pipeline() -> None:
outdict['_sidecar_data'] = getattr(msg, '_sidecar_data')
@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."""
del message # Unused.
if self.test_sidecar:
setattr(response, '_sidecar_data', indata['_sidecar_data'])
@ -830,8 +832,10 @@ def test_full_pipeline() -> None:
setattr(message, '_sidecar_data', indata['_sidecar_data'])
@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."""
del message # Unused.
if self.test_sidecar:
outdict['_sidecar_data'] = getattr(response, '_sidecar_data')

View File

@ -3,12 +3,13 @@
"""Functionality related to cloud functionality."""
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Annotated, Optional
from enum import Enum
from efro.message import Message, Response
from efro.dataclassio import ioprepped, IOAttrs
from bacommon.transfer import DirectoryManifest
if TYPE_CHECKING:
pass
@ -77,27 +78,57 @@ class LoginProxyCompleteMessage(Message):
@ioprepped
@dataclass
class AccountSessionReleaseMessage(Message):
"""We're done using this particular session."""
token: Annotated[str, IOAttrs('tk')]
@ioprepped
@dataclass
class CredentialsCheckMessage(Message):
"""Are our current credentials valid?"""
class TestMessage(Message):
"""Can I get some of that workspace action?"""
testfoo: Annotated[int, IOAttrs('f')]
@classmethod
def get_response_types(cls) -> list[type[Response]]:
return [CredentialsCheckResponse]
return [TestResponse]
@ioprepped
@dataclass
class CredentialsCheckResponse(Response):
"""Info returned when checking credentials."""
class TestResponse(Response):
"""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

View File

@ -34,7 +34,6 @@ class DirectoryManifest:
def create_from_disk(cls, path: Path) -> DirectoryManifest:
"""Create a manifest from a directory on disk."""
import hashlib
from multiprocessing import cpu_count
from concurrent.futures import ThreadPoolExecutor
pathstr = str(path)
@ -65,7 +64,10 @@ class DirectoryManifest:
filesize=filesize))
# 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)))
@classmethod

View File

@ -118,8 +118,9 @@ class _Outputter:
if isinstance(extra_attrs, dict):
if not _is_valid_for_codec(extra_attrs, self._codec):
raise TypeError(
f'Extra attrs on {fieldpath} contains data type(s)'
f' not supported by json.')
f'Extra attrs on \'{fieldpath}\' contains data type(s)'
f' not supported by \'{self._codec.value}\' codec:'
f' {extra_attrs}.')
if self._create:
assert out is not None
out.update(extra_attrs)

View File

@ -42,12 +42,6 @@ class Response:
# Some standard response types:
class ErrorType(Enum):
"""Type of error that occurred in remote message handling."""
OTHER = 0
CLEAN = 1
@ioprepped
@dataclass
class ErrorResponse(Response):
@ -56,6 +50,13 @@ class ErrorResponse(Response):
This type is unique in that it is not returned to the user; it
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_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.OTHER

View File

@ -15,8 +15,7 @@ from efro.error import CleanError
from efro.dataclassio import (is_ioprepped_dataclass, dataclass_to_dict,
dataclass_from_dict)
from efro.message._message import (Message, Response, ErrorResponse,
EmptyResponse, ErrorType,
UnregisteredMessageIDError)
EmptyResponse, UnregisteredMessageIDError)
if TYPE_CHECKING:
from typing import Any, Literal
@ -141,11 +140,11 @@ class MessageProtocol:
# If anything goes wrong, return a ErrorResponse instead.
if isinstance(exc, CleanError) and self.preserve_clean_errors:
return ErrorResponse(error_message=str(exc),
error_type=ErrorType.CLEAN)
error_type=ErrorResponse.ErrorType.CLEAN)
return ErrorResponse(
error_message=(traceback.format_exc() if self.trusted_sender else
'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],
opname: str) -> dict:

View File

@ -15,7 +15,7 @@ from efro.message._message import (Message, Response, EmptyResponse,
ErrorResponse, UnregisteredMessageIDError)
if TYPE_CHECKING:
from typing import Any, Callable, Optional, Union, Awaitable
from typing import Any, Callable, Awaitable
from efro.message._protocol import MessageProtocol
@ -50,19 +50,19 @@ class MessageReceiver:
def __init__(self, protocol: MessageProtocol) -> None:
self.protocol = protocol
self._handlers: dict[type[Message], Callable] = {}
self._decode_filter_call: Optional[Callable[[Any, dict, Message],
None]] = None
self._encode_filter_call: Optional[Callable[[Any, Response, dict],
None]] = None
self._decode_filter_call: Callable[[Any, dict, Message],
None] | None = None
self._encode_filter_call: Callable[
[Any, Message | None, Response, dict], None] | None = None
# TODO: don't currently have async encode equivalent
# or either for sender; can add as needed.
self._decode_filter_async_call: Optional[Callable[
[Any, dict, Message], Awaitable[None]]] = None
self._decode_filter_async_call: Callable[[Any, dict, Message],
Awaitable[None]] | None = None
# noinspection PyProtectedMember
def register_handler(
self, call: Callable[[Any, Message], Optional[Response]]) -> None:
self, call: Callable[[Any, Message], Response | None]) -> None:
"""Register a handler call.
The message type handled by the call is determined by its
@ -106,7 +106,7 @@ class MessageReceiver:
assert issubclass(msgtype, Message)
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.
if isinstance(ret, (_GenericAlias, types.UnionType)):
@ -178,8 +178,8 @@ class MessageReceiver:
return call
def encode_filter_method(
self, call: Callable[[Any, Response, dict], None]
) -> Callable[[Any, Response, dict], None]:
self, call: Callable[[Any, Message | None, Response, dict], None]
) -> Callable[[Any, Message | None, Response, dict], None]:
"""Function decorator for defining an encode filter.
Encode filters can be used to add extra data to the message
@ -202,42 +202,38 @@ class MessageReceiver:
else:
raise TypeError(msg)
def _decode_incoming_message_base(
self, bound_obj: Any,
msg: str) -> tuple[Any, dict, Message, type[Message]]:
def _decode_incoming_message_base(self, bound_obj: Any,
msg: str) -> tuple[Any, dict, Message]:
# Decode the incoming message.
msg_dict = self.protocol.decode_dict(msg)
msg_decoded = self.protocol.message_from_dict(msg_dict)
msgtype = type(msg_decoded)
assert issubclass(msgtype, Message)
assert isinstance(msg_decoded, Message)
if self._decode_filter_call is not None:
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,
msg: str) -> tuple[Message, type[Message]]:
bound_obj, _msg_dict, msg_decoded, msgtype = (
def _decode_incoming_message(self, bound_obj: Any, msg: str) -> Message:
bound_obj, _msg_dict, msg_decoded = (
self._decode_incoming_message_base(bound_obj=bound_obj, msg=msg))
# If they've set an async filter but are calling sync
# handle_raw_message() its likely a bug.
assert self._decode_filter_async_call is None
return msg_decoded, msgtype
return msg_decoded
async def _decode_incoming_message_async(
self, bound_obj: Any, msg: str) -> tuple[Message, type[Message]]:
bound_obj, msg_dict, msg_decoded, msgtype = (
self._decode_incoming_message_base(bound_obj=bound_obj, msg=msg))
async def _decode_incoming_message_async(self, bound_obj: Any,
msg: str) -> Message:
bound_obj, msg_dict, msg_decoded = (self._decode_incoming_message_base(
bound_obj=bound_obj, msg=msg))
if self._decode_filter_async_call is not None:
await self._decode_filter_async_call(bound_obj, msg_dict,
msg_decoded)
return msg_decoded, msgtype
return msg_decoded
def encode_user_response(self, bound_obj: Any,
response: Optional[Response],
msgtype: type[Message]) -> str:
def encode_user_response(self, bound_obj: Any, message: Message,
response: Response | None) -> str:
"""Encode a response provided by the user for sending."""
# A return value of None equals EmptyResponse.
@ -247,18 +243,21 @@ class MessageReceiver:
assert isinstance(response, Response)
# (user should never explicitly return error-responses)
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)
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)
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."""
response = self.protocol.error_to_response(exc)
response_dict = self.protocol.response_to_dict(response)
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)
def handle_raw_message(self,
@ -273,21 +272,22 @@ class MessageReceiver:
error responses returned to the sender.
"""
assert not self.is_async, "can't call sync handler on async receiver"
msg_decoded: Message | None = None
try:
msg_decoded, msgtype = self._decode_incoming_message(
bound_obj, msg)
msg_decoded = self._decode_incoming_message(bound_obj, msg)
msgtype = type(msg_decoded)
handler = self._handlers.get(msgtype)
if handler is None:
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
response = handler(bound_obj, msg_decoded)
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:
if (raise_unregistered
and isinstance(exc, UnregisteredMessageIDError)):
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(
self,
@ -299,21 +299,23 @@ class MessageReceiver:
The return value is the raw response to the message.
"""
assert self.is_async, "can't call async handler on sync receiver"
msg_decoded: Message | None = None
try:
msg_decoded, msgtype = await self._decode_incoming_message_async(
msg_decoded = await self._decode_incoming_message_async(
bound_obj, msg)
msgtype = type(msg_decoded)
handler = self._handlers.get(msgtype)
if handler is None:
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
response = await handler(bound_obj, msg_decoded)
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:
if (raise_unregistered
and isinstance(exc, UnregisteredMessageIDError)):
raise
return self.encode_error_response(bound_obj, exc)
return self.encode_error_response(bound_obj, msg_decoded, exc)
class BoundMessageReceiver:
@ -334,5 +336,12 @@ class BoundMessageReceiver:
return self._receiver.protocol
def encode_error_response(self, exc: Exception) -> str:
"""Given an error, return a response ready to send."""
return self._receiver.encode_error_response(self._obj, exc)
"""Given an error, return a response ready to send.
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)

View File

@ -6,13 +6,14 @@ Supports static typing for message types and possible return types.
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, TypeVar
from efro.error import CleanError, RemoteError
from efro.message._message import (EmptyResponse, ErrorResponse, ErrorType)
from efro.message._message import EmptyResponse, ErrorResponse
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._protocol import MessageProtocol
@ -42,13 +43,13 @@ class MessageSender:
def __init__(self, protocol: MessageProtocol) -> None:
self.protocol = protocol
self._send_raw_message_call: Optional[Callable[[Any, str], str]] = None
self._send_async_raw_message_call: Optional[Callable[
[Any, str], Awaitable[str]]] = None
self._encode_filter_call: Optional[Callable[[Any, Message, dict],
None]] = None
self._decode_filter_call: Optional[Callable[[Any, dict, Response],
None]] = None
self._send_raw_message_call: Callable[[Any, str], str] | None = None
self._send_async_raw_message_call: Callable[
[Any, str], Awaitable[str]] | None = None
self._encode_filter_call: Callable[[Any, Message, dict],
None] | None = None
self._decode_filter_call: Callable[[Any, Message, dict, Response],
None] | None = None
def send_method(
self, call: Callable[[Any, str],
@ -79,8 +80,8 @@ class MessageSender:
return call
def decode_filter_method(
self, call: Callable[[Any, dict, Response], None]
) -> Callable[[Any, dict, Response], None]:
self, call: Callable[[Any, Message, dict, Response], None]
) -> Callable[[Any, Message, dict, Response], None]:
"""Function decorator for defining a decode filter.
Decode filters can be used to extract extra data from incoming
@ -90,71 +91,137 @@ class MessageSender:
self._decode_filter_call = call
return call
def send(self, bound_obj: Any, message: Message) -> Optional[Response]:
"""Send a message and receive a response.
def send(self, bound_obj: Any, message: Message) -> Response | None:
"""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:
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)
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
or type(response) in type(message).get_response_types())
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."""
msg_dict = self.protocol.message_to_dict(message)
if self._encode_filter_call is not None:
self._encode_filter_call(bound_obj, message, msg_dict)
return self.protocol.encode_dict(msg_dict)
def decode_response(self, bound_obj: Any,
response_encoded: str) -> Optional[Response]:
"""Decode, filter, and possibly act on raw response 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)
def _decode_raw_response(self, bound_obj: Any, message: Message,
response_encoded: str) -> Response:
"""Create a Response from returned data.
# Special case: if we get EmptyResponse, we simply return None.
if isinstance(response, EmptyResponse):
These Responses may encapsulate things like remote errors and
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
# Special case: a remote error occurred. Raise a local Exception
# instead of returning the message.
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)
# Some error occurred. Raise a local Exception for it.
if isinstance(raw_response, ErrorResponse):
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,
message: Message) -> Optional[Response]:
"""Send a message asynchronously using asyncio.
# If they want to support clean errors, do those.
if (self.protocol.preserve_clean_errors and
raw_response.error_type is ErrorResponse.ErrorType.CLEAN):
raise CleanError(raw_response.error_message)
The message will be encoded for transport and passed to
dispatch_raw_message_async.
"""
if self._send_async_raw_message_call is None:
raise RuntimeError('send_async() is unimplemented for this type.')
# In all other cases, just say something went wrong 'out there'.
raise RemoteError(raw_response.error_message)
msg_encoded = self.encode_message(bound_obj, message)
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
return raw_response
class BoundMessageSender:
@ -171,20 +238,34 @@ class BoundMessageSender:
"""Protocol associated with this sender."""
return self._sender.protocol
def send_untyped(self, message: Message) -> Optional[Response]:
def send_untyped(self, message: Message) -> Response | None:
"""Send a message synchronously.
Whenever possible, use the send() call provided by generated
subclasses instead of this; it will provide better type safety.
"""
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.
Whenever possible, use the send_async() call provided by generated
subclasses instead of this; it will provide better type safety.
"""
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)