diff --git a/.efrocachemap b/.efrocachemap index 0fbba15c..f2ce5e08 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -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/bf/de/08804e3ad0f319521b1831dedb8b", - "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d6/8c/13132c50cc5613d8a1b63ae2b668", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/eb/9e/f7b96cb44899e45c8887e6b0a541", - "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/33/b5/94df2669c0972cb67f6d5a0deda2", - "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e5/50/55227dbd30219b2d85b91b3cb12e", - "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d6/0e/fe82f0f783da43ad915b8907f84d", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/33/42/e0081d7e62f3d414330d48773064", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/71/39/33283a49ffd8ea1a1df1630e6960", - "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/3a/48/2a620297639e4cd77e7c47a31e6b", - "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/37/57/07794ce711c7c156bce78a51c485", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1d/bb/72d4eb6505c35d86371e3492963f", - "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2c/3e/c85c3d7d2834fc5d1e0abdc49fb9", - "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/4f/e7/da0f7ae7fb279bc8862b78c14e45", - "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ad/49/17db1cebd712f82af2639658e3f3", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2b/83/7ff2c4de06eb943e83ffdc8f17c6", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b4/d7/4d794ad4a56a1e46359d13c44123", - "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ce/5c/df22430468506b5a2ed6172f326a", - "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/92/71/5301a16ba18e72504d10d0f72ce0", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/6e/e0/6bba7b32ead0a56ddace4c0f3515", - "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/b8/f9/282a87b6d4ed616c2e2d174f6ffb", - "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/72/9d/7041244c1532e8739f4144a0dbdb", - "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f9/d3/43568f3cfbf7b9e1f1c0a80d3cbb", - "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1d/d7/9508dda5f67d8120159fc27e5aa6", - "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/32/19/422f06dd741d5fecd501192345db", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a3/e0/532750ec3447a0d2507297ef7e06", - "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/07/f4/07c76f58e65817f064a6f8eb947a", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/20/fb/ceab54e3526c6754978d599feea3", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7e/87/5534e303798640c45eebb3ff2a78", - "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ec/71/2b6c9e9920857db9d4f08e13a147", - "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d4/cd/475d49a92f27e1023c4c9929f229", - "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/23/02/efb19178a0c9f32e7dd1a74c6e0f", - "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6c/8b/239dd8045dbdd3f674557dcea6f6", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/eb/28/f681d90ec4fc218081d27a54b524", - "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b3/5e/11e126f42e97c2ad8fbbdedbddeb", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/32/80/f5b20ad75c2822c99472832588f0", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ae/bc/1dfe7e5f4991b790eb28b4655b97", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/22/90/e3e1510d320434dce98c2f2f4bd7", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/41/83/cc82f715fd3b766b8e99cd3e47f3", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/3b/01/b0131177be78b774f7645a25d9fa", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/74/53/922f030cdb5f6631f30d9918cdac", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/71/20/4a9cd20befe66687c3409577dbfe", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/fd/d4/b0984f40c5c005e1ba9c9b5c0b6b", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/3a/2c/2ae55b591d38a78d82a8d929ec45", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/dc/fa/b8cd1ec4ec21748c8cdecd1d857e", + "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", "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" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 28df2c75..d4b8c937 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -600,6 +600,7 @@ dincrease dingsound dingsoundhigh + dinl dirfilter dirmanifest dirname @@ -872,6 +873,7 @@ floinkdingle floof floofcls + floooff floop flycheck fmod @@ -1391,6 +1393,7 @@ locs logcat logincode + loginid logintoken logitech logput @@ -2325,6 +2328,7 @@ startscan startsplits starttime + statestr statictest statictestfiles statictype @@ -2481,6 +2485,7 @@ testpatch testpath testpt + testresponse testsealable testsentinel testsoundtrack @@ -2520,6 +2525,7 @@ timetype tipstext titletext + tkinval tlog tmpdir tmpf @@ -2592,6 +2598,7 @@ typestr tzdiff tzinfos + tzoffset tzpath uadfc uber @@ -2605,6 +2612,7 @@ uicleanupchecks uicontroller uilocation + uinl uiscale uiupkeeptimer unallowed diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c29df8a..0b340f72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.2 (20601, 2022-06-04) +### 1.7.2 (20604, 2022-06-15) - 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!) diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json index 2445265b..f27e5ab1 100644 --- a/assets/.asset_manifest_public.json +++ b/assets/.asset_manifest_public.json @@ -18,6 +18,7 @@ "ba_data/python/ba/__pycache__/_asyncio.cpython-310.opt-1.pyc", "ba_data/python/ba/__pycache__/_benchmark.cpython-310.opt-1.pyc", "ba_data/python/ba/__pycache__/_campaign.cpython-310.opt-1.pyc", + "ba_data/python/ba/__pycache__/_cloud.cpython-310.opt-1.pyc", "ba_data/python/ba/__pycache__/_collision.cpython-310.opt-1.pyc", "ba_data/python/ba/__pycache__/_coopgame.cpython-310.opt-1.pyc", "ba_data/python/ba/__pycache__/_coopsession.cpython-310.opt-1.pyc", @@ -59,7 +60,6 @@ "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__/cloud.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", @@ -82,6 +82,7 @@ "ba_data/python/ba/_asyncio.py", "ba_data/python/ba/_benchmark.py", "ba_data/python/ba/_campaign.py", + "ba_data/python/ba/_cloud.py", "ba_data/python/ba/_collision.py", "ba_data/python/ba/_coopgame.py", "ba_data/python/ba/_coopsession.py", @@ -127,7 +128,6 @@ "ba_data/python/ba/_tips.py", "ba_data/python/ba/_tournament.py", "ba_data/python/ba/_ui.py", - "ba_data/python/ba/cloud.py", "ba_data/python/ba/deprecated.py", "ba_data/python/ba/internal.py", "ba_data/python/ba/macmusicapp.py", @@ -143,12 +143,14 @@ "ba_data/python/bacommon/__pycache__/cloud.cpython-310.opt-1.pyc", "ba_data/python/bacommon/__pycache__/net.cpython-310.opt-1.pyc", "ba_data/python/bacommon/__pycache__/servermanager.cpython-310.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/transfer.cpython-310.opt-1.pyc", "ba_data/python/bacommon/assets.py", "ba_data/python/bacommon/bacloud.py", "ba_data/python/bacommon/build.py", "ba_data/python/bacommon/cloud.py", "ba_data/python/bacommon/net.py", "ba_data/python/bacommon/servermanager.py", + "ba_data/python/bacommon/transfer.py", "ba_data/python/bastd/__init__.py", "ba_data/python/bastd/__pycache__/__init__.cpython-310.opt-1.pyc", "ba_data/python/bastd/__pycache__/appdelegate.cpython-310.opt-1.pyc", diff --git a/assets/Makefile b/assets/Makefile index 5119290d..a32126aa 100644 --- a/assets/Makefile +++ b/assets/Makefile @@ -151,6 +151,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \ build/ba_data/python/ba/_asyncio.py \ build/ba_data/python/ba/_benchmark.py \ build/ba_data/python/ba/_campaign.py \ + build/ba_data/python/ba/_cloud.py \ build/ba_data/python/ba/_collision.py \ build/ba_data/python/ba/_coopgame.py \ build/ba_data/python/ba/_coopsession.py \ @@ -194,7 +195,6 @@ 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/cloud.py \ build/ba_data/python/ba/deprecated.py \ build/ba_data/python/ba/internal.py \ build/ba_data/python/ba/macmusicapp.py \ @@ -398,6 +398,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ build/ba_data/python/ba/__pycache__/_asyncio.cpython-310.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_benchmark.cpython-310.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_campaign.cpython-310.opt-1.pyc \ + build/ba_data/python/ba/__pycache__/_cloud.cpython-310.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_collision.cpython-310.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_coopgame.cpython-310.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_coopsession.cpython-310.opt-1.pyc \ @@ -441,7 +442,6 @@ 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__/cloud.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 \ @@ -648,6 +648,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \ build/ba_data/python/bacommon/cloud.py \ build/ba_data/python/bacommon/net.py \ build/ba_data/python/bacommon/servermanager.py \ + build/ba_data/python/bacommon/transfer.py \ build/ba_data/python/efro/__init__.py \ build/ba_data/python/efro/call.py \ build/ba_data/python/efro/dataclassio/__init__.py \ @@ -677,6 +678,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \ build/ba_data/python/bacommon/__pycache__/cloud.cpython-310.opt-1.pyc \ build/ba_data/python/bacommon/__pycache__/net.cpython-310.opt-1.pyc \ build/ba_data/python/bacommon/__pycache__/servermanager.cpython-310.opt-1.pyc \ + build/ba_data/python/bacommon/__pycache__/transfer.cpython-310.opt-1.pyc \ build/ba_data/python/efro/__pycache__/__init__.cpython-310.opt-1.pyc \ build/ba_data/python/efro/__pycache__/call.cpython-310.opt-1.pyc \ build/ba_data/python/efro/dataclassio/__pycache__/__init__.cpython-310.opt-1.pyc \ diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index 89f8abbb..82df9420 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -24,6 +24,7 @@ from ba._actor import Actor from ba._player import PlayerInfo, Player, EmptyPlayer, StandLocation from ba._nodeactor import NodeActor from ba._app import App +from ba._cloud import CloudSubsystem from ba._coopgame import CoopGameActivity from ba._coopsession import CoopSession from ba._dependency import (Dependency, DependencyComponent, DependencySet, @@ -89,7 +90,7 @@ __all__ = [ 'clipboard_get_text', 'clipboard_has_text', 'clipboard_is_supported', 'clipboard_set_text', 'CollideModel', 'Collision', 'columnwidget', 'containerwidget', 'Context', 'ContextCall', 'ContextError', - 'CoopGameActivity', 'CoopSession', 'Data', 'DeathType', + 'CloudSubsystem', 'CoopGameActivity', 'CoopSession', 'Data', 'DeathType', 'DelegateNotFoundError', 'Dependency', 'DependencyComponent', 'DependencyError', 'DependencySet', 'DieMessage', 'do_once', 'DropMessage', 'DroppedMessage', 'DualTeamSession', 'emitfx', 'EmptyPlayer', 'EmptyTeam', diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py index edec32f5..0da45bfa 100644 --- a/assets/src/ba_data/python/ba/_app.py +++ b/assets/src/ba_data/python/ba/_app.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from typing import Optional, Any, Callable import ba - from ba.cloud import CloudSubsystem + from ba._cloud import CloudSubsystem from bastd.actor import spazappearance from ba._accountv2 import AccountV2Subsystem diff --git a/assets/src/ba_data/python/ba/cloud.py b/assets/src/ba_data/python/ba/_cloud.py similarity index 82% rename from assets/src/ba_data/python/ba/cloud.py rename to assets/src/ba_data/python/ba/_cloud.py index da0057a5..588cf70b 100644 --- a/assets/src/ba_data/python/ba/cloud.py +++ b/assets/src/ba_data/python/ba/_cloud.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, overload import _ba if TYPE_CHECKING: - from typing import Union, Callable, Any + from typing import Callable, Any from efro.message import Message import bacommon.cloud @@ -35,8 +35,7 @@ class CloudSubsystem: self, msg: bacommon.cloud.LoginProxyRequestMessage, on_response: Callable[ - [Union[bacommon.cloud.LoginProxyRequestResponse, - Exception]], None], + [bacommon.cloud.LoginProxyRequestResponse | Exception], None], ) -> None: ... @@ -45,8 +44,7 @@ class CloudSubsystem: self, msg: bacommon.cloud.LoginProxyStateQueryMessage, on_response: Callable[ - [Union[bacommon.cloud.LoginProxyStateQueryResponse, - Exception]], None], + [bacommon.cloud.LoginProxyStateQueryResponse | Exception], None], ) -> None: ... @@ -54,7 +52,7 @@ class CloudSubsystem: def send_message( self, msg: bacommon.cloud.LoginProxyCompleteMessage, - on_response: Callable[[Union[None, Exception]], None], + on_response: Callable[[None | Exception], None], ) -> None: ... @@ -63,7 +61,7 @@ class CloudSubsystem: self, msg: bacommon.cloud.CredentialsCheckMessage, on_response: Callable[ - [Union[bacommon.cloud.CredentialsCheckResponse, Exception]], None], + [bacommon.cloud.CredentialsCheckResponse | Exception], None], ) -> None: ... @@ -71,7 +69,7 @@ class CloudSubsystem: def send_message( self, msg: bacommon.cloud.AccountSessionReleaseMessage, - on_response: Callable[[Union[None, Exception]], None], + on_response: Callable[[None | Exception], None], ) -> None: ... diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index 4da7fa62..ba0922b1 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -317,6 +317,7 @@ dfmt dictval diffbit + dinl dirfilter dirslash dlfcn @@ -441,6 +442,7 @@ fjcoiwef flipbit floinkdingle + floooff floop flopsy fname @@ -683,6 +685,7 @@ lockstr locktype logincode + loginid logmsg logpath logprefix @@ -1186,6 +1189,7 @@ starttime startx starty + statestr staticdata statictest stdint @@ -1236,6 +1240,7 @@ testint testinternalcapi testnode + testresponse texel texqualstr textcolor @@ -1258,6 +1263,7 @@ timesteps timetype timetypes + tkinval tlog tmpmat tomer @@ -1293,12 +1299,14 @@ twst typeobj typestr + tzoffset tzpath uber udbz udif uibounds uiid + uinl unbased unblessed uncas diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index 5df2752e..4c50d445 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -21,7 +21,7 @@ namespace ballistica { // These are set automatically via script; don't modify them here. -const int kAppBuildNumber = 20601; +const int kAppBuildNumber = 20604; const char* kAppVersion = "1.7.2"; // Our standalone globals. diff --git a/tools/bacloud b/tools/bacloud index 4d1a46dc..726aace0 100755 --- a/tools/bacloud +++ b/tools/bacloud @@ -7,42 +7,42 @@ This facilitates workflows such as creating asset-packages, etc. from __future__ import annotations -import sys import os +import sys +import zlib import time +import base64 import datetime +import tempfile +import subprocess from pathlib import Path from typing import TYPE_CHECKING from dataclasses import dataclass -import json -import subprocess -import tempfile +from concurrent.futures import ThreadPoolExecutor import requests -from efro.error import CleanError from efro.terminal import Clr -from efro.dataclassio import dataclass_from_json -from bacommon.bacloud import Response +from efro.error import CleanError +from efro.dataclassio import (dataclass_from_json, dataclass_to_dict, + dataclass_to_json, ioprepped) +from bacommon.bacloud import RequestData, ResponseData, BACLOUD_VERSION +from bacommon.transfer import DirectoryManifest if TYPE_CHECKING: - from typing import Optional, BinaryIO, IO - -# Version is sent to the master-server with all commands. Can be incremented -# if we need to change behavior server-side to go along with client changes. -VERSION = 2 + from typing import BinaryIO, IO TOOL_NAME = 'bacloud' # Server we talk to (can override via env var). -MASTER_SERVER_URL = os.getenv('BACLOUD_SERVER_URL', - 'https://bamaster.appspot.com') +BACLOUD_SERVER_URL = os.getenv('BACLOUD_SERVER_URL', 'https://ballistica.net') +@ioprepped @dataclass class StateData: """Persistent state data stored to disk.""" - login_token: Optional[str] = None + login_token: str | None = None def get_tz_offset_seconds() -> float: @@ -53,70 +53,12 @@ def get_tz_offset_seconds() -> float: return utc_offset -@dataclass -class DirManifestFile: - """Represents a single file within a DirManifest.""" - filehash: str - filesize: int - - -class DirManifest: - """Represents a directory of files with some common purpose.""" - - def __init__(self) -> None: - self.path = Path('') - self.files: dict[str, DirManifestFile] = {} - - @classmethod - def load_from_disk(cls, path: Path) -> DirManifest: - """Create a package populated from a directory on disk.""" - package = DirManifest() - - package.path = path - packagepathstr = str(path) - paths: list[str] = [] - - # Simply return empty manifests if the given path isn't a dir. - # (the server may intend to create it and is just asking what's - # there already) - if path.is_dir(): - # Build the full list of package-relative paths. - for basename, _dirnames, filenames in os.walk(path): - for filename in filenames: - fullname = os.path.join(basename, filename) - assert fullname.startswith(packagepathstr) - paths.append(fullname[len(packagepathstr) + 1:]) - - import hashlib - from concurrent.futures import ThreadPoolExecutor - from multiprocessing import cpu_count - - def _get_file_info(filepath: str) -> tuple[str, DirManifestFile]: - sha = hashlib.sha256() - fullfilepath = os.path.join(packagepathstr, filepath) - if not os.path.isfile(fullfilepath): - raise Exception(f'File not found: "{fullfilepath}"') - with open(fullfilepath, 'rb') as infile: - filebytes = infile.read() - filesize = len(filebytes) - sha.update(filebytes) - return (filepath, - DirManifestFile(filehash=sha.hexdigest(), - filesize=filesize)) - - # Now use all procs to hash the files efficiently. - with ThreadPoolExecutor(max_workers=cpu_count()) as executor: - package.files = dict(executor.map(_get_file_info, paths)) - - return package - - class App: """Context for a run of the tool.""" def __init__(self) -> None: self._state = StateData() - self._project_root: Optional[Path] = None + self._project_root: Path | None = None self._end_command_args: dict = {} def run(self) -> None: @@ -163,7 +105,7 @@ class App: return try: with open(self._state_data_path, 'r', encoding='utf-8') as infile: - self._state = StateData(**json.loads(infile.read())) + self._state = dataclass_from_json(StateData, infile.read()) except Exception: print(f'{Clr.RED}Error loading {TOOL_NAME} data;' f' resetting to defaults.{Clr.RST}') @@ -172,31 +114,35 @@ class App: if not self._state_dir.exists(): self._state_dir.mkdir(parents=True, exist_ok=True) with open(self._state_data_path, 'w', encoding='utf-8') as outfile: - outfile.write(json.dumps(self._state.__dict__)) + outfile.write(dataclass_to_json(self._state)) def _servercmd(self, cmd: str, - data: dict, - files: dict[str, IO] = None) -> Response: + payload: dict, + files: dict[str, IO] = None) -> ResponseData: """Issue a command to the server and get a response.""" - response_raw_2 = requests.post( - (MASTER_SERVER_URL + '/bacloudcmd'), - headers={'User-Agent': f'bacloud/{VERSION}'}, + response_raw = requests.post( + f'{BACLOUD_SERVER_URL}/bacloudcmd', + headers={'User-Agent': f'bacloud/{BACLOUD_VERSION}'}, data={ - 'c': cmd, - 'v': VERSION, - 't': json.dumps(self._state.login_token), - 'd': json.dumps(data), - 'z': get_tz_offset_seconds(), - 'y': int(sys.stdout.isatty()), + 'v': + BACLOUD_VERSION, + 'r': + dataclass_to_json( + RequestData(command=cmd, + token=self._state.login_token, + payload=payload, + tzoffset=get_tz_offset_seconds(), + isatty=sys.stdout.isatty())), }, - files=files) - response_raw_2.raise_for_status() # Except if anything went wrong. - assert isinstance(response_raw_2.content, bytes) + files=files, + ) + response_raw.raise_for_status() # Error if anything went wrong. + assert isinstance(response_raw.content, bytes) - response = dataclass_from_json(Response, - response_raw_2.content.decode()) + response = dataclass_from_json(ResponseData, + response_raw.content.decode()) # Handle a few things inline. # (so this functionality is available even to recursive commands, etc.) @@ -212,7 +158,7 @@ class App: return response def _upload_file(self, filename: str, call: str, args: dict) -> None: - print(f'{Clr.BLU}Uploading {filename}{Clr.RST}', flush=True) + print(f'Uploading {Clr.BLU}{filename}{Clr.RST}', flush=True) with tempfile.TemporaryDirectory() as tempdir: srcpath = Path(filename) gzpath = Path(tempdir, 'file.gz') @@ -228,17 +174,10 @@ class App: ) def _handle_dir_manifest_response(self, dirmanifest: str) -> None: - from dataclasses import asdict - manifest = DirManifest.load_from_disk(Path(dirmanifest)) - - # Store the manifest to be included with our next called command. - self._end_command_args['manifest'] = { - 'files': {key: asdict(val) - for key, val in manifest.files.items()} - } + self._end_command_args['manifest'] = dataclass_to_dict( + DirectoryManifest.create_from_disk(Path(dirmanifest))) def _handle_uploads(self, uploads: tuple[list[str], str, dict]) -> None: - from concurrent.futures import ThreadPoolExecutor assert len(uploads) == 3 filenames, uploadcmd, uploadargs = uploads assert isinstance(filenames, list) @@ -256,12 +195,29 @@ class App: # exceptions that occurred. list(executor.map(_do_filename, filenames)) - def _handle_downloads_inline(self, downloads_inline: dict[str, - str]) -> None: + def _handle_deletes(self, deletes: list[str]) -> None: + """Handle file deletes.""" + for fname in deletes: + # Server shouldn't be sending us dir paths here. + assert not os.path.isdir(fname) + os.unlink(fname) + + def _handle_downloads_inline( + self, + downloads_inline: dict[str, str], + ) -> None: """Handle inline file data to be saved to the client.""" - import base64 - import zlib for fname, fdata in downloads_inline.items(): + + # 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) @@ -270,26 +226,6 @@ class App: with open(fname, 'wb') as outfile: outfile.write(data) - def _handle_deletes(self, deletes: list[str]) -> None: - """Handle file deletes.""" - for fname in deletes: - os.unlink(fname) - - def _handle_uploads_inline(self, uploads_inline: list[str]) -> None: - """Handle uploading files inline.""" - import base64 - import zlib - files: dict[str, str] = {} - for filepath in uploads_inline: - if not os.path.exists(filepath): - raise CleanError(f'File not found: {filepath}') - with open(filepath, 'rb') as infile: - data = infile.read() - data_zipped = zlib.compress(data) - data_base64 = base64.b64encode(data_zipped).decode() - files[filepath] = data_base64 - self._end_command_args['uploads_inline'] = files - 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. @@ -304,6 +240,19 @@ class App: if not dirnames and not filenames and basename != prunedir: os.rmdir(basename) + def _handle_uploads_inline(self, uploads_inline: list[str]) -> None: + """Handle uploading files inline.""" + files: dict[str, str] = {} + for filepath in uploads_inline: + if not os.path.exists(filepath): + raise CleanError(f'File not found: {filepath}') + with open(filepath, 'rb') as infile: + data = infile.read() + data_zipped = zlib.compress(data) + data_base64 = base64.b64encode(data_zipped).decode() + files[filepath] = data_base64 + self._end_command_args['uploads_inline'] = files + def _handle_open_url(self, url: str) -> None: import webbrowser webbrowser.open(url) @@ -321,13 +270,14 @@ class App: """Run a single user command to completion.""" # pylint: disable=too-many-branches - nextcall: Optional[tuple[str, dict]] = ('user', {'a': args}) + nextcall: tuple[str, dict] | None = ('user', {'a': args}) # Now talk to the server in a loop until there's nothing left to do. while nextcall is not None: self._end_command_args = {} response = self._servercmd(*nextcall) nextcall = None + if response.login is not None: self._state.login_token = response.login if response.logout: @@ -338,12 +288,18 @@ class App: self._handle_uploads_inline(response.uploads_inline) if response.uploads is not None: self._handle_uploads(response.uploads) - if response.downloads_inline: - self._handle_downloads_inline(response.downloads_inline) + + # Note: we handle file deletes *before* downloads. This + # way our file-download code only has to worry about creating or + # removing directories; not files, and corner cases such as + # a file getting replaced with a directory should just work. if response.deletes: self._handle_deletes(response.deletes) + if response.downloads_inline: + self._handle_downloads_inline(response.downloads_inline) if response.dir_prune_empty: self._handle_dir_prune_empty(response.dir_prune_empty) + if response.open_url is not None: self._handle_open_url(response.open_url) if response.input_prompt is not None: @@ -364,7 +320,7 @@ if __name__ == '__main__': App().run() except KeyboardInterrupt: # Let's do a clean fail on keyboard interrupt. - # Can make this optional if a backtrace is ever useful.. + # Can make this optional if a backtrace is ever useful. sys.exit(1) except CleanError as clean_exc: clean_exc.pretty_print() diff --git a/tools/bacommon/assets.py b/tools/bacommon/assets.py index 097b52f6..2c4441ab 100644 --- a/tools/bacommon/assets.py +++ b/tools/bacommon/assets.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Optional, Annotated +from typing import TYPE_CHECKING, Annotated from enum import Enum from efro.dataclassio import ioprepped, IOAttrs @@ -57,4 +57,4 @@ class AssetPackageBuildState: # Build error string. If this is present, it should be presented # to the user and they should required to explicitly restart the build # in some way if desired. - error: Annotated[Optional[str], IOAttrs('e')] = None + error: Annotated[str | None, IOAttrs('e')] = None diff --git a/tools/bacommon/bacloud.py b/tools/bacommon/bacloud.py index f22f7fe4..99c9be3b 100644 --- a/tools/bacommon/bacloud.py +++ b/tools/bacommon/bacloud.py @@ -3,18 +3,34 @@ """Functionality related to the bacloud tool.""" from __future__ import annotations -from dataclasses import dataclass -from typing import TYPE_CHECKING, Optional -from efro.dataclassio import ioprepped +from dataclasses import dataclass +from typing import TYPE_CHECKING, Annotated + +from efro.dataclassio import ioprepped, IOAttrs if TYPE_CHECKING: pass +# Version is sent to the master-server with all commands. Can be incremented +# if we need to change behavior server-side to go along with client changes. +BACLOUD_VERSION = 6 + @ioprepped @dataclass -class Response: +class RequestData: + """Request sent to bacloud server.""" + command: Annotated[str, IOAttrs('c')] + token: Annotated[str | None, IOAttrs('t')] + payload: Annotated[dict, IOAttrs('p')] + tzoffset: Annotated[float, IOAttrs('z')] + isatty: Annotated[bool, IOAttrs('y')] + + +@ioprepped +@dataclass +class ResponseData: # noinspection PyUnresolvedReferences """Response sent from the bacloud server to the client. @@ -35,10 +51,10 @@ class Response: uploads_inline: If present, a list of pathnames that should be base64 gzipped and uploaded to an 'uploads_inline' dict in end_command args. This should be limited to relatively small files. + deletes: If present, file paths that should be deleted on the client. downloads_inline: If present, pathnames mapped to base64 gzipped data to be written to the client. This should only be used for relatively small files as they are all included inline as part of the response. - deletes: If present, file paths that should be deleted on the client. dir_prune_empty: If present, all empty dirs under this one should be removed. open_url: If present, url to display to the user. @@ -52,20 +68,29 @@ class Response: end_command: If present, this command is run with these args at the end of response processing. """ - message: Optional[str] = None - message_end: str = '\n' - error: Optional[str] = None - delay_seconds: float = 0.0 - login: Optional[str] = None - logout: bool = False - dir_manifest: Optional[str] = None - uploads: Optional[tuple[list[str], str, dict]] = None - uploads_inline: Optional[list[str]] = None - downloads_inline: Optional[dict[str, str]] = None - deletes: Optional[list[str]] = None - dir_prune_empty: Optional[str] = None - open_url: Optional[str] = None - input_prompt: Optional[tuple[str, bool]] = None - end_message: Optional[str] = None - end_message_end: str = '\n' - end_command: Optional[tuple[str, dict]] = None + message: Annotated[str | None, IOAttrs('m', store_default=False)] = None + message_end: Annotated[str, IOAttrs('m_end', store_default=False)] = '\n' + error: Annotated[str | None, IOAttrs('e', store_default=False)] = None + delay_seconds: Annotated[float, IOAttrs('d', store_default=False)] = 0.0 + login: Annotated[str | None, IOAttrs('l', store_default=False)] = None + logout: Annotated[bool, IOAttrs('lo', store_default=False)] = False + dir_manifest: Annotated[str | None, + IOAttrs('man', store_default=False)] = None + uploads: Annotated[tuple[list[str], str, dict] | None, + IOAttrs('u', store_default=False)] = None + uploads_inline: Annotated[list[str] | None, + IOAttrs('uinl', store_default=False)] = None + deletes: Annotated[list[str] | None, + IOAttrs('dlt', store_default=False)] = None + downloads_inline: Annotated[dict[str, str] | None, + IOAttrs('dinl', store_default=False)] = None + dir_prune_empty: Annotated[str | None, + IOAttrs('dpe', store_default=False)] = None + open_url: Annotated[str | None, IOAttrs('url', store_default=False)] = None + input_prompt: Annotated[tuple[str, bool] | None, + IOAttrs('inp', store_default=False)] = None + end_message: Annotated[str | None, + IOAttrs('em', store_default=False)] = None + end_message_end: Annotated[str, IOAttrs('eme', store_default=False)] = '\n' + end_command: Annotated[tuple[str, dict] | None, + IOAttrs('ec', store_default=False)] = None diff --git a/tools/bacommon/net.py b/tools/bacommon/net.py index 5c48443d..3409ba61 100644 --- a/tools/bacommon/net.py +++ b/tools/bacommon/net.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Any, Annotated +from typing import TYPE_CHECKING, Any, Annotated from dataclasses import dataclass, field from efro.dataclassio import ioprepped, IOAttrs @@ -28,7 +28,7 @@ class ServerNodeQueryResponse: """A response to a query about server-nodes.""" # If present, something went wrong, and this describes it. - error: Annotated[Optional[str], IOAttrs('e', store_default=False)] = None + error: Annotated[str | None, IOAttrs('e', store_default=False)] = None # The set of servernodes. servers: Annotated[list[ServerNodeEntry], @@ -40,11 +40,11 @@ class ServerNodeQueryResponse: @dataclass class PrivateHostingState: """Combined state of whether we're hosting, whether we can, etc.""" - unavailable_error: Optional[str] = None - party_code: Optional[str] = None + unavailable_error: str | None = None + party_code: str | None = None tickets_to_host_now: int = 0 - minutes_until_free_host: Optional[float] = None - free_host_minutes_remaining: Optional[float] = None + minutes_until_free_host: float | None = None + free_host_minutes_remaining: float | None = None @ioprepped @@ -55,10 +55,10 @@ class PrivateHostingConfig: playlist_name: str = 'Unknown' randomize: bool = False tutorial: bool = False - custom_team_names: Optional[tuple[str, str]] = None - custom_team_colors: Optional[tuple[tuple[float, float, float], - tuple[float, float, float]]] = None - playlist: Optional[list[dict[str, Any]]] = None + custom_team_names: tuple[str, str] | None = None + custom_team_colors: tuple[tuple[float, float, float], + tuple[float, float, float]] | None = None + playlist: list[dict[str, Any]] | None = None exit_minutes: float = 120.0 exit_minutes_unclean: float = 180.0 exit_minutes_idle: float = 10.0 @@ -68,7 +68,7 @@ class PrivateHostingConfig: @dataclass class PrivatePartyConnectResult: """Info about a server we get back when connecting.""" - error: Optional[str] = None - addr: Optional[str] = None - port: Optional[int] = None - password: Optional[str] = None + error: str | None = None + addr: str | None = None + port: int | None = None + password: str | None = None diff --git a/tools/bacommon/transfer.py b/tools/bacommon/transfer.py new file mode 100644 index 00000000..25c49f7c --- /dev/null +++ b/tools/bacommon/transfer.py @@ -0,0 +1,78 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to transferring files/data.""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import TYPE_CHECKING, Annotated + +from efro.dataclassio import ioprepped, IOAttrs + +if TYPE_CHECKING: + from pathlib import Path + + +@ioprepped +@dataclass +class DirectoryManifestFile: + """Describes metadata and hashes for a file in a manifest.""" + filehash: Annotated[str, IOAttrs('h')] + filesize: Annotated[int, IOAttrs('s')] + + +@ioprepped +@dataclass +class DirectoryManifest: + """Contains a summary of files in a directory.""" + files: Annotated[dict[str, DirectoryManifestFile], IOAttrs('f')] + + _empty_hash: str | None = None + + @classmethod + 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) + paths: list[str] = [] + + if path.is_dir(): + # Build the full list of package-relative paths. + for basename, _dirnames, filenames in os.walk(path): + for filename in filenames: + fullname = os.path.join(basename, filename) + assert fullname.startswith(pathstr) + paths.append(fullname[len(pathstr) + 1:]) + elif path.exists(): + # Just return a single file entry if path is not a dir. + paths.append(pathstr) + + def _get_file_info(filepath: str) -> tuple[str, DirectoryManifestFile]: + sha = hashlib.sha256() + fullfilepath = os.path.join(pathstr, filepath) + if not os.path.isfile(fullfilepath): + raise Exception(f'File not found: "{fullfilepath}"') + with open(fullfilepath, 'rb') as infile: + filebytes = infile.read() + filesize = len(filebytes) + sha.update(filebytes) + return (filepath, + DirectoryManifestFile(filehash=sha.hexdigest(), + filesize=filesize)) + + # Now use all procs to hash the files efficiently. + with ThreadPoolExecutor(max_workers=cpu_count()) as executor: + return cls(files=dict(executor.map(_get_file_info, paths))) + + @classmethod + def get_empty_hash(cls) -> str: + """Return the hash for an empty file.""" + if cls._empty_hash is None: + import hashlib + sha = hashlib.sha256() + cls._empty_hash = sha.hexdigest() + return cls._empty_hash diff --git a/tools/batools/build.py b/tools/batools/build.py index 5575e79f..bc6df6e1 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -38,8 +38,8 @@ class PipRequirement: # installing it. And as far as manually-installed bits, pip itself must # have some way to allow for that, right?... PIP_REQUIREMENTS = [ - PipRequirement(modulename='pylint', minversion=[2, 13, 9]), - PipRequirement(modulename='mypy', minversion=[0, 960]), + PipRequirement(modulename='pylint', minversion=[2, 14, 2]), + PipRequirement(modulename='mypy', minversion=[0, 961]), PipRequirement(modulename='yapf', minversion=[0, 32, 0]), PipRequirement(modulename='cpplint', minversion=[1, 6, 0]), PipRequirement(modulename='pytest', minversion=[7, 1, 2]), @@ -53,8 +53,8 @@ PIP_REQUIREMENTS = [ PipRequirement(pipname='types-requests', minversion=[2, 27, 29]), PipRequirement(pipname='types-pytz', minversion=[2021, 3, 8]), PipRequirement(pipname='types-PyYAML', minversion=[6, 0, 7]), - PipRequirement(pipname='certifi', minversion=[2022, 5, 18, 1]), - PipRequirement(pipname='types-certifi', minversion=[2021, 10, 8, 2]), + PipRequirement(pipname='certifi', minversion=[2022, 6, 15]), + PipRequirement(pipname='types-certifi', minversion=[2021, 10, 8, 3]), ] # Parts of full-tests suite we only run on particular days. diff --git a/tools/efro/message/_receiver.py b/tools/efro/message/_receiver.py index 36cada02..26f32ce7 100644 --- a/tools/efro/message/_receiver.py +++ b/tools/efro/message/_receiver.py @@ -15,7 +15,7 @@ from efro.message._message import (Message, Response, EmptyResponse, ErrorResponse, UnregisteredMessageIDError) if TYPE_CHECKING: - from typing import Any, Callable, Optional, Union + from typing import Any, Callable, Optional, Union, Awaitable from efro.message._protocol import MessageProtocol @@ -55,6 +55,11 @@ class MessageReceiver: self._encode_filter_call: Optional[Callable[[Any, Response, dict], 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 + # noinspection PyProtectedMember def register_handler( self, call: Callable[[Any, Message], Optional[Response]]) -> None: @@ -152,12 +157,26 @@ class MessageReceiver: """Function decorator for defining a decode filter. Decode filters can be used to extract extra data from incoming - message dicts. + message dicts. This version will work for both handle_raw_message() + and handle_raw_message_async() """ assert self._decode_filter_call is None self._decode_filter_call = call return call + def decode_filter_async_method( + self, call: Callable[[Any, dict, Message], Awaitable[None]] + ) -> Callable[[Any, dict, Message], Awaitable[None]]: + """Function decorator for defining a decode filter. + + Decode filters can be used to extract extra data from incoming + message dicts. Note that this version will only work with + handle_raw_message_async(). + """ + assert self._decode_filter_async_call is None + self._decode_filter_async_call = call + return call + def encode_filter_method( self, call: Callable[[Any, Response, dict], None] ) -> Callable[[Any, Response, dict], None]: @@ -183,8 +202,9 @@ class MessageReceiver: else: raise TypeError(msg) - def _decode_incoming_message(self, bound_obj: Any, - msg: str) -> tuple[Message, type[Message]]: + def _decode_incoming_message_base( + self, bound_obj: Any, + msg: str) -> tuple[Any, dict, Message, type[Message]]: # Decode the incoming message. msg_dict = self.protocol.decode_dict(msg) msg_decoded = self.protocol.message_from_dict(msg_dict) @@ -192,7 +212,27 @@ class MessageReceiver: assert issubclass(msgtype, 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 + def _decode_incoming_message(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)) + + # 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 + + 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)) + + 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 def encode_user_response(self, bound_obj: Any, @@ -260,7 +300,7 @@ class MessageReceiver: """ assert self.is_async, "can't call async handler on sync receiver" try: - msg_decoded, msgtype = self._decode_incoming_message( + msg_decoded, msgtype = await self._decode_incoming_message_async( bound_obj, msg) handler = self._handlers.get(msgtype) if handler is None: diff --git a/tools/efrotools/pcommand.py b/tools/efrotools/pcommand.py index 93f75f96..b9f3d609 100644 --- a/tools/efrotools/pcommand.py +++ b/tools/efrotools/pcommand.py @@ -481,10 +481,9 @@ def _filter_tool_config(cfg: str) -> str: pypaths = getconfig(PROJROOT).get('python_paths') if pypaths is None: raise RuntimeError('python_paths not set in project config') - cstr = "init-hook='import sys;" + cstr = 'init-hook=import sys;' for path in pypaths: cstr += f" sys.path.append('{PROJROOT}/{path}');" - cstr += "'" cfg = cfg.replace(pylint_init_tag, cstr) return cfg