mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-27 01:13:13 +08:00
Merge branch 'efroemling:master' into master
This commit is contained in:
commit
6074709e30
@ -420,7 +420,7 @@
|
||||
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/60/ad/38269b7f1c7dc20cb9a506cd0681",
|
||||
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/72/85/d6fc4d16b7081d91fba2850b5b10",
|
||||
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/e9/ae/1d674d0c086eaa0bd1c3b1db0505",
|
||||
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/a3/38/05424b4cfb6e23e902b6dc20b209",
|
||||
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/68/22/ee4cff9f9fa011db6a2ed7092d1f",
|
||||
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/5b/cf/4501b151257c3d8d6ee8d0497d14",
|
||||
"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/17/21/7b6371bde52392eb4a38e7c6d55a",
|
||||
@ -431,12 +431,12 @@
|
||||
"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/70/7d/6cbdaf130eaa5c58cffb1f321e3d",
|
||||
"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/c1/3c/2d45627563fbfbbbda2b7e6799e1",
|
||||
"assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/8f/73/093120ae2241d8f4b899ccda2d75",
|
||||
"assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/25/65/1cb03566e73811fc6e1b841d9072",
|
||||
"assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/ef/e6/d4909f571d7473fd04055728490e",
|
||||
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/a5/28/6bf6b15f8359a145cd2e599849f1",
|
||||
"assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/82/eb/37ff44af76812097f9c98f05c730",
|
||||
"assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/08/3b/68cea4d16f7020d932829af85323",
|
||||
"assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/50/e8/837be1324c8128507b3df89b689f",
|
||||
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/d8/f2/aa16bc336bd7660cc86c3264bfc4",
|
||||
"assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/09/6c/942dd354447772a69ea5cae1d486",
|
||||
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/91/70/05ad4a6fdbdaa0f471225f7ad317",
|
||||
@ -3995,26 +3995,26 @@
|
||||
"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/0a/56/252de9190ee6367ccbf37174783d",
|
||||
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/f5/30/29f5a9d9cc5c6f5c76e3058d3621",
|
||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3e/e5/037d736cacd93a4b005cc93e72ad",
|
||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/22/04/430aa3457c427f0814058c2b4483",
|
||||
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/93/68/307719e44199480a5ee051d993f5",
|
||||
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/9f/50/6f8a60e5375bf651bdebda617249",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cf/03/a5a5748fda33c876fbf3e8261b02",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/82/3e/5725b87a8cc1e90f69bec58c65d5",
|
||||
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/64/16/1589abfd35715bd2aa2915766148",
|
||||
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a8/36/584d685f3bea03753acf7344dfce",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3d/09/cbb451c2e8f856de61c0eafc5fdc",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b3/c9/9b3e221426dae6a047a893a4eb39",
|
||||
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/61/2e/af3b07614ea2fb60f70b3d3b442a",
|
||||
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/6e/fb/4b6e3e14ae9e329ae2a5c2eaab24",
|
||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/86/3b/f8fc04eefa313d673ee98d20e360",
|
||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d1/83/544e088664612666bbaa6c1ff422",
|
||||
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/79/13/29d322c6e8f7717ec87d5027bb20",
|
||||
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/94/01/19f43fe2ee530d48f31665d22ff0",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/17/e9/d6369d897f3595fbe03202887447",
|
||||
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/07/ff/cd46cba42a67cf31d6454b9eba7d",
|
||||
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/eb/f6/0fa02f0dd61fe86f030e235cb65d",
|
||||
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e8/5a/c49738579f58cff159f78330685e",
|
||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/77/79/d970690e3fa5d24e0cdfb5aff646",
|
||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/83/56/8203d51e88d563f373bd73304219",
|
||||
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/2b/7a/3eb09023c93e907472043fbccfff",
|
||||
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d0/4f/d139bb2f0a1e4e400dade616f5d3",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/5d/5d/20bd4d3a607a8b3a5d9d9d925146",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a0/f2/0646e8a3ed1c1ae091bee9628b8e",
|
||||
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/08/e8/064033db53071b97422d21386e8f",
|
||||
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e7/e6/3cc5634143190749753a806a4792",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d1/94/513926fe5432ba722b0e4f119f0c",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ff/7e/884381d6bc009c804496a512beaf",
|
||||
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/be/06/c6f50e95926031616daf349acc64",
|
||||
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/16/95/4a354d9d8faa18f26be3a7c57f63",
|
||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f1/39/6da99f25127e5ec62b5586e30378",
|
||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d6/17/65c7d490b78d8ae0120e1d254b83",
|
||||
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/de/28/bea57ab25706df395792f06fd08b",
|
||||
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/4c/29/17e37dd645bb9d5fb6f85db2b1bd",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/26/22/bbd9535c97eedfc2c18c45e65f9b",
|
||||
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/a6/40/e9517fe39850dca141f0cc086503",
|
||||
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3b/0c/2f4061ab877d415a1c30e0e736db",
|
||||
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3c/5a/2b0714af254c64954ccfe51c70b3",
|
||||
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1f/ae/c8a885b1a1868b6846b606cdb456",
|
||||
@ -4031,14 +4031,14 @@
|
||||
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/df/efb51d1c226eac613d48e2cbf0b8",
|
||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1c/f6/357fe951c86c9fc5b1b737cd91ae",
|
||||
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/04/17/e2de0ab5df6b938d828e8662ce6d",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/69/dc/6fc1614b2548c6ac76c9e891c2e2",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/c2/1b/263c5e001c6891d774d941f0bdfd",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/7b/3a/f77ffca8d7c45b859d1e48c1b468",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/88/15/1aa07f986d0bf7dac9a1f39635f2",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/4b/e1/646d3095ab442e0b18d4c0de9689",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/0f/fe/034c116781ddfe6cc89ada030056",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/91/64/10fcd883cf0d15895d72a638e2ad",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/1e/69/bf40bc8defe923cfa6d48cb5dd04",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/34/77/1986ffe869aca7b8aee6b24ea64b",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/0f/af/5430940c906f3d0c6e0983b9a2b9",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/22/84/02b9109e1449f2acca49f4f9b934",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/2b/22/b32d8e18c6a258929f091a14419d",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/a2/08/ecf905f1c6ede831e66e8d84c6f6",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/83/d7/01a034b1d9e2f028cb5f964396d3",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/81/a5/e780126b52d530cce18a64ae65e4",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/d8/39/51a851a77b6ce36073e9d190b9bf",
|
||||
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/7d/3e/229a581cb2454ed856f1d8b564a7",
|
||||
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/aa/a5/3ddc86d1789b2bf1d376b7671a3d"
|
||||
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/98/12/571b2160d69d42580e8f31fa6a8d"
|
||||
}
|
||||
4
.idea/dictionaries/ericf.xml
generated
4
.idea/dictionaries/ericf.xml
generated
@ -337,6 +337,7 @@
|
||||
<w>capturetheflag</w>
|
||||
<w>carentity</w>
|
||||
<w>cashregistersound</w>
|
||||
<w>cbegin</w>
|
||||
<w>cbgn</w>
|
||||
<w>cbits</w>
|
||||
<w>cbot</w>
|
||||
@ -699,6 +700,7 @@
|
||||
<w>eachother</w>
|
||||
<w>eaddrnotavail</w>
|
||||
<w>easteregghunt</w>
|
||||
<w>echofile</w>
|
||||
<w>edcc</w>
|
||||
<w>editcontroller</w>
|
||||
<w>editgame</w>
|
||||
@ -891,6 +893,7 @@
|
||||
<w>floofcls</w>
|
||||
<w>floooff</w>
|
||||
<w>floop</w>
|
||||
<w>flushhhhh</w>
|
||||
<w>flycheck</w>
|
||||
<w>fmod</w>
|
||||
<w>fname</w>
|
||||
@ -1391,6 +1394,7 @@
|
||||
<w>listvalidconfigs</w>
|
||||
<w>lival</w>
|
||||
<w>llzma</w>
|
||||
<w>lmap</w>
|
||||
<w>lmerged</w>
|
||||
<w>lmod</w>
|
||||
<w>lmodfile</w>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
### 1.7.7 (build 20723, api 7, 2022-08-26)
|
||||
### 1.7.7 (build 20730, api 7, 2022-09-02)
|
||||
- Added `ba.app.meta.load_exported_classes()` for loading classes discovered by the meta subsystem cleanly in a background thread.
|
||||
- Improved logging of missing playlist game types.
|
||||
- Some ba.Lstr functionality can now be used in background threads.
|
||||
@ -8,6 +8,7 @@
|
||||
- Added support for the console tool in the new devices section on ballistica.net.
|
||||
- Increased timeouts in net-testing gui and a few other places to be able to better diagnose/handle places with very poor connectivity.
|
||||
- Removed `Platform::SetLastPyCall()` which was just for debugging and which has not been useful in a while.
|
||||
- Moved some app bootstrapping from the C++ layer to the ba._bootstrap module.
|
||||
|
||||
### 1.7.6 (build 20687, api 7, 2022-08-11)
|
||||
- Cleaned up da MetaSubsystem code.
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"ba_data/python/ba/__pycache__/_assetmanager.cpython-310.opt-1.pyc",
|
||||
"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__/_bootstrap.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",
|
||||
@ -82,6 +83,7 @@
|
||||
"ba_data/python/ba/_assetmanager.py",
|
||||
"ba_data/python/ba/_asyncio.py",
|
||||
"ba_data/python/ba/_benchmark.py",
|
||||
"ba_data/python/ba/_bootstrap.py",
|
||||
"ba_data/python/ba/_campaign.py",
|
||||
"ba_data/python/ba/_cloud.py",
|
||||
"ba_data/python/ba/_collision.py",
|
||||
@ -511,6 +513,7 @@
|
||||
"ba_data/python/efro/__pycache__/__init__.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/call.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/error.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/log.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/rpc.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/terminal.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/util.cpython-310.opt-1.pyc",
|
||||
@ -532,6 +535,7 @@
|
||||
"ba_data/python/efro/dataclassio/_prep.py",
|
||||
"ba_data/python/efro/dataclassio/extras.py",
|
||||
"ba_data/python/efro/error.py",
|
||||
"ba_data/python/efro/log.py",
|
||||
"ba_data/python/efro/message/__init__.py",
|
||||
"ba_data/python/efro/message/__pycache__/__init__.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/efro/message/__pycache__/_message.cpython-310.opt-1.pyc",
|
||||
|
||||
@ -150,6 +150,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
|
||||
build/ba_data/python/ba/_assetmanager.py \
|
||||
build/ba_data/python/ba/_asyncio.py \
|
||||
build/ba_data/python/ba/_benchmark.py \
|
||||
build/ba_data/python/ba/_bootstrap.py \
|
||||
build/ba_data/python/ba/_campaign.py \
|
||||
build/ba_data/python/ba/_cloud.py \
|
||||
build/ba_data/python/ba/_collision.py \
|
||||
@ -399,6 +400,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
|
||||
build/ba_data/python/ba/__pycache__/_assetmanager.cpython-310.opt-1.pyc \
|
||||
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__/_bootstrap.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 \
|
||||
@ -664,6 +666,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
|
||||
build/ba_data/python/efro/dataclassio/_prep.py \
|
||||
build/ba_data/python/efro/dataclassio/extras.py \
|
||||
build/ba_data/python/efro/error.py \
|
||||
build/ba_data/python/efro/log.py \
|
||||
build/ba_data/python/efro/message/__init__.py \
|
||||
build/ba_data/python/efro/message/_message.py \
|
||||
build/ba_data/python/efro/message/_module.py \
|
||||
@ -694,6 +697,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
|
||||
build/ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-310.opt-1.pyc \
|
||||
build/ba_data/python/efro/dataclassio/__pycache__/extras.cpython-310.opt-1.pyc \
|
||||
build/ba_data/python/efro/__pycache__/error.cpython-310.opt-1.pyc \
|
||||
build/ba_data/python/efro/__pycache__/log.cpython-310.opt-1.pyc \
|
||||
build/ba_data/python/efro/message/__pycache__/__init__.cpython-310.opt-1.pyc \
|
||||
build/ba_data/python/efro/message/__pycache__/_message.cpython-310.opt-1.pyc \
|
||||
build/ba_data/python/efro/message/__pycache__/_module.cpython-310.opt-1.pyc \
|
||||
|
||||
185
assets/src/ba_data/python/ba/_bootstrap.py
Normal file
185
assets/src/ba_data/python/ba/_bootstrap.py
Normal file
@ -0,0 +1,185 @@
|
||||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Bootstrapping."""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, TextIO, Callable
|
||||
|
||||
|
||||
def bootstrap() -> None:
|
||||
"""Run bootstrapping logic.
|
||||
|
||||
This is the very first userland code that runs.
|
||||
It sets up low level environment bits and creates the app instance.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
|
||||
# The first thing we set up is capturing/redirecting Python
|
||||
# stdout/stderr so we can at least debug problems on systems where
|
||||
# native stdout/stderr is not easily accessible (looking at you, Android).
|
||||
sys.stdout = _Redirect(sys.stdout, _ba.print_stdout) # type: ignore
|
||||
sys.stderr = _Redirect(sys.stderr, _ba.print_stderr) # type: ignore
|
||||
|
||||
env = _ba.env()
|
||||
|
||||
# Give a soft warning if we're being used with a different binary
|
||||
# version than we expect.
|
||||
expected_build = 20730
|
||||
running_build: int = env['build_number']
|
||||
if running_build != expected_build:
|
||||
print(
|
||||
f'WARNING: These script files are meant to be used with'
|
||||
f' Ballistica build {expected_build}.\n'
|
||||
f' You are running build {running_build}.'
|
||||
f' This might cause the app to error or misbehave.',
|
||||
file=sys.stderr)
|
||||
|
||||
# Tell Python to not handle SIGINT itself (it normally generates
|
||||
# KeyboardInterrupts which make a mess; we want to intercept them
|
||||
# for simple clean exit). We capture interrupts per-platform in
|
||||
# the C++ layer.
|
||||
# Note: I tried creating a handler in Python but it seemed to often have
|
||||
# a delay of up to a second before getting called. (not a huge deal
|
||||
# but I'm picky).
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling.
|
||||
|
||||
# ..though it turns out we need to set up our C signal handling AFTER
|
||||
# we've told Python to disable its own; otherwise (on Mac at least) it
|
||||
# wipes out our existing C handler.
|
||||
_ba.setup_sigint()
|
||||
|
||||
# Sanity check: we should always be run in UTF-8 mode.
|
||||
if sys.flags.utf8_mode != 1:
|
||||
print(
|
||||
'ERROR: Python\'s UTF-8 mode is not set.'
|
||||
' This will likely result in errors.',
|
||||
file=sys.stderr)
|
||||
|
||||
debug_build = env['debug_build']
|
||||
|
||||
# We expect dev_mode on in debug builds and off otherwise.
|
||||
if debug_build != sys.flags.dev_mode:
|
||||
print(
|
||||
f'WARNING: Mismatch in debug_build {debug_build}'
|
||||
f' and sys.flags.dev_mode {sys.flags.dev_mode}',
|
||||
file=sys.stderr)
|
||||
|
||||
# In embedded situations (when we're providing our own Python) let's
|
||||
# also provide our own root certs so ssl works. We can consider overriding
|
||||
# this in particular embedded cases if we can verify that system certs
|
||||
# are working.
|
||||
# (We also allow forcing this via an env var if the user desires)
|
||||
if (_ba.contains_python_dist()
|
||||
or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'):
|
||||
import certifi
|
||||
|
||||
# Let both OpenSSL and requests (if present) know to use this.
|
||||
os.environ['SSL_CERT_FILE'] = os.environ['REQUESTS_CA_BUNDLE'] = (
|
||||
certifi.where())
|
||||
|
||||
# FIXME: I think we should init Python in the main thread, which should
|
||||
# also avoid these issues. (and also might help us play better with
|
||||
# Python debuggers?)
|
||||
|
||||
# Gloriously hacky workaround here:
|
||||
# Our 'main' Python thread is the game thread (not the app's main
|
||||
# thread) which means it has a small stack compared to the main
|
||||
# thread (at least on apple). Sadly it turns out this causes the
|
||||
# debug build of Python to blow its stack immediately when doing
|
||||
# some big imports.
|
||||
# Normally we'd just give the game thread the same stack size as
|
||||
# the main thread and that'd be the end of it. However
|
||||
# we're using std::threads which it turns out have no way to set
|
||||
# the stack size (as of fall '19). Grumble.
|
||||
#
|
||||
# However python threads *can* take custom stack sizes.
|
||||
# (and it appears they might use the main thread's by default?..)
|
||||
# ...so as a workaround in the debug version, we can run problematic
|
||||
# heavy imports here in another thread and all is well.
|
||||
# If we ever see stack overflows in our release build we'll have
|
||||
# to take more drastic measures like switching from std::threads
|
||||
# to pthreads.
|
||||
|
||||
if debug_build:
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def _thread_func() -> None:
|
||||
# pylint: disable=unused-import
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
testthread = threading.Thread(target=_thread_func)
|
||||
testthread.start()
|
||||
testthread.join()
|
||||
del testthread
|
||||
|
||||
# Clear out the standard quit/exit messages since they don't work for us.
|
||||
# pylint: disable=c-extension-no-member
|
||||
if not TYPE_CHECKING:
|
||||
import __main__
|
||||
del __main__.__builtins__.quit
|
||||
del __main__.__builtins__.exit
|
||||
|
||||
# Now spin up our App instance and store it on both _ba and ba.
|
||||
from ba._app import App
|
||||
import ba
|
||||
_ba.app = ba.app = App()
|
||||
|
||||
|
||||
class _Redirect:
|
||||
|
||||
def __init__(self, original: TextIO, call: Callable[[str], None]) -> None:
|
||||
self._lock = threading.Lock()
|
||||
self._linebits: list[str] = []
|
||||
self._original = original
|
||||
self._call = call
|
||||
self._pending_ship = False
|
||||
|
||||
def write(self, sval: Any) -> None:
|
||||
"""Override standard write call."""
|
||||
|
||||
self._call(sval)
|
||||
|
||||
# Now do logging:
|
||||
# Add it to our accumulated line.
|
||||
# If the message ends in a newline, we can ship it
|
||||
# immediately as a log entry. Otherwise, schedule a ship
|
||||
# next cycle (if it hasn't yet at that point) so that we
|
||||
# can accumulate subsequent prints.
|
||||
# (so stuff like print('foo', 123, 'bar') will ship as one entry)
|
||||
with self._lock:
|
||||
self._linebits.append(sval)
|
||||
if sval.endswith('\n'):
|
||||
self._shiplog()
|
||||
else:
|
||||
_ba.pushcall(self._shiplog,
|
||||
from_other_thread=True,
|
||||
suppress_other_thread_warning=True)
|
||||
|
||||
def _shiplog(self) -> None:
|
||||
with self._lock:
|
||||
line = ''.join(self._linebits)
|
||||
if not line:
|
||||
return
|
||||
self._linebits = []
|
||||
|
||||
# Log messages aren't expected to have trailing newlines.
|
||||
if line.endswith('\n'):
|
||||
line = line[:-1]
|
||||
_ba.log(line, to_stdout=False)
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush the file."""
|
||||
self._original.flush()
|
||||
|
||||
def isatty(self) -> bool:
|
||||
"""Are we a terminal?"""
|
||||
return self._original.isatty()
|
||||
@ -106,8 +106,8 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]):
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
ba.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
default_music = ba.MusicType.FOOTBALL
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
|
||||
@ -143,6 +143,10 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]):
|
||||
self._flag_respawn_light: ba.NodeActor | None = None
|
||||
self._score_to_win = int(settings['Score to Win'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (ba.MusicType.EPIC
|
||||
if self._epic_mode else ba.MusicType.FOOTBALL)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
touchdowns = self._score_to_win / 7
|
||||
@ -330,7 +334,6 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
|
||||
tips = ['Use the pick-up button to grab the flag < ${PICKUP} >']
|
||||
scoreconfig = ba.ScoreConfig(scoretype=ba.ScoreType.MILLISECONDS,
|
||||
version='B')
|
||||
default_music = ba.MusicType.FOOTBALL
|
||||
|
||||
# FIXME: Need to update co-op games to use getscoreconfig.
|
||||
def get_score_type(self) -> str:
|
||||
|
||||
@ -137,8 +137,8 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]):
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
ba.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
default_music = ba.MusicType.HOCKEY
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
|
||||
@ -203,6 +203,10 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]):
|
||||
self._puck: Puck | None = None
|
||||
self._score_to_win = int(settings['Score to Win'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (ba.MusicType.EPIC
|
||||
if self._epic_mode else ba.MusicType.HOCKEY)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
if self._score_to_win == 1:
|
||||
|
||||
@ -76,9 +76,9 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]):
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
ba.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
scoreconfig = ba.ScoreConfig(label='Time Held')
|
||||
default_music = ba.MusicType.KEEP_AWAY
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
|
||||
@ -115,6 +115,10 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]):
|
||||
self._flag: Flag | None = None
|
||||
self._hold_time = int(settings['Hold Time'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (ba.MusicType.EPIC
|
||||
if self._epic_mode else ba.MusicType.KEEP_AWAY)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
return 'Carry the flag for ${ARG1} seconds.', self._hold_time
|
||||
|
||||
@ -79,6 +79,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
ba.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
scoreconfig = ba.ScoreConfig(label='Time Held')
|
||||
|
||||
@ -115,6 +116,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
|
||||
self._scoring_team: weakref.ref[Team] | None = None
|
||||
self._hold_time = int(settings['Hold Time'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self._flag_region_material = ba.Material()
|
||||
self._flag_region_material.add_actions(
|
||||
conditions=('they_have_material', shared.player_material),
|
||||
@ -128,7 +130,9 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
|
||||
))
|
||||
|
||||
# Base class overrides.
|
||||
self.default_music = ba.MusicType.SCARY
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (ba.MusicType.EPIC
|
||||
if self._epic_mode else ba.MusicType.SCARY)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
return 'Secure the flag for ${ARG1} seconds.', self._hold_time
|
||||
|
||||
@ -188,6 +188,7 @@
|
||||
<w>cancelbtn</w>
|
||||
<w>capitan</w>
|
||||
<w>cargs</w>
|
||||
<w>cbegin</w>
|
||||
<w>cbgn</w>
|
||||
<w>cbresults</w>
|
||||
<w>cbtnoffs</w>
|
||||
@ -362,6 +363,7 @@
|
||||
<w>dxgi</w>
|
||||
<w>dynamicdata</w>
|
||||
<w>echidna</w>
|
||||
<w>echofile</w>
|
||||
<w>edef</w>
|
||||
<w>effmult</w>
|
||||
<w>efro</w>
|
||||
@ -460,6 +462,7 @@
|
||||
<w>floooff</w>
|
||||
<w>floop</w>
|
||||
<w>flopsy</w>
|
||||
<w>flushhhhh</w>
|
||||
<w>fname</w>
|
||||
<w>fnode</w>
|
||||
<w>fnumc</w>
|
||||
@ -703,6 +706,7 @@
|
||||
<w>linkstoryboards</w>
|
||||
<w>listobj</w>
|
||||
<w>llock</w>
|
||||
<w>lmap</w>
|
||||
<w>localmodlibs</w>
|
||||
<w>localns</w>
|
||||
<w>lockpath</w>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
namespace ballistica {
|
||||
|
||||
// These are set automatically via script; don't modify them here.
|
||||
const int kAppBuildNumber = 20723;
|
||||
const int kAppBuildNumber = 20730;
|
||||
const char* kAppVersion = "1.7.7";
|
||||
|
||||
// Our standalone globals.
|
||||
|
||||
@ -1,78 +1,20 @@
|
||||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Initial ballistica bootstrapping."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, TextIO, Callable
|
||||
pass
|
||||
|
||||
# All we do here is make our script files accessible and then hand it off
|
||||
# to them.
|
||||
|
||||
class _BAConsoleRedirect:
|
||||
|
||||
def __init__(self, original: TextIO, call: Callable[[str], None]) -> None:
|
||||
self._lock = threading.Lock()
|
||||
self._linebits: list[str] = []
|
||||
self._original = original
|
||||
self._call = call
|
||||
self._pending_ship = False
|
||||
|
||||
def write(self, sval: Any) -> None:
|
||||
"""Override standard write call."""
|
||||
|
||||
self._call(sval)
|
||||
|
||||
# Now do logging:
|
||||
# Add it to our accumulated line.
|
||||
# If the message ends in a newline, we can ship it
|
||||
# immediately as a log entry. Otherwise, schedule a ship
|
||||
# next cycle (if it hasn't yet at that point) so that we
|
||||
# can accumulate subsequent prints.
|
||||
# (so stuff like print('foo', 123, 'bar') will ship as one entry)
|
||||
with self._lock:
|
||||
self._linebits.append(sval)
|
||||
if sval.endswith('\n'):
|
||||
self._shiplog()
|
||||
else:
|
||||
_ba.pushcall(self._shiplog,
|
||||
from_other_thread=True,
|
||||
suppress_other_thread_warning=True)
|
||||
|
||||
def _shiplog(self) -> None:
|
||||
with self._lock:
|
||||
line = ''.join(self._linebits)
|
||||
if not line:
|
||||
return
|
||||
self._linebits = []
|
||||
|
||||
# Log messages aren't expected to have trailing newlines.
|
||||
if line.endswith('\n'):
|
||||
line = line[:-1]
|
||||
_ba.log(line, to_stdout=False)
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush the file."""
|
||||
self._original.flush()
|
||||
|
||||
def isatty(self) -> bool:
|
||||
"""Are we a terminal?"""
|
||||
return self._original.isatty()
|
||||
|
||||
|
||||
# The very first thing we set up is redirecting Python stdout/stderr so
|
||||
# we can at least debug problems on systems where native stdout/stderr
|
||||
# is not easily accessible (looking at you, Android).
|
||||
sys.stdout = _BAConsoleRedirect(sys.stdout, _ba.print_stdout) # type: ignore
|
||||
sys.stderr = _BAConsoleRedirect(sys.stderr, _ba.print_stderr) # type: ignore
|
||||
|
||||
# Now get access to our various script files.
|
||||
# Let's lookup mods first (so users can do whatever they want).
|
||||
# and then our bundled scripts last (don't want bundled site-package
|
||||
# stuff overwriting system versions)
|
||||
@ -80,93 +22,9 @@ sys.path.insert(0, _ba.env()['python_directory_user'])
|
||||
sys.path.append(_ba.env()['python_directory_app'])
|
||||
sys.path.append(_ba.env()['python_directory_app_site'])
|
||||
|
||||
# Tell Python to not handle SIGINT itself (it normally generates
|
||||
# KeyboardInterrupts which make a mess; we want to intercept them
|
||||
# for simple clean exit). We capture interrupts per-platform in
|
||||
# the C++ layer.
|
||||
# Note: I tried creating a handler in Python but it seemed to often have
|
||||
# a delay of up to a second before getting called. (not a huge deal
|
||||
# but I'm picky).
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling.
|
||||
|
||||
# ..though it turns out we need to set up our C signal handling AFTER
|
||||
# we've told Python to disable its own; otherwise (on Mac at least) it
|
||||
# wipes out our existing C handler.
|
||||
_ba.setup_sigint()
|
||||
|
||||
# Sanity check: we should always be run in UTF-8 mode.
|
||||
if sys.flags.utf8_mode != 1:
|
||||
print('ERROR: Python\'s UTF-8 mode is not set.'
|
||||
' This will likely result in errors.')
|
||||
|
||||
debug_build = _ba.env()['debug_build']
|
||||
|
||||
# We expect dev_mode on in debug builds and off otherwise.
|
||||
if debug_build != sys.flags.dev_mode:
|
||||
print(f'WARNING: Mismatch in debug_build {debug_build}'
|
||||
f' and sys.flags.dev_mode {sys.flags.dev_mode}')
|
||||
|
||||
# In embedded situations (when we're providing our own Python) let's
|
||||
# also provide our own root certs so ssl works. We can consider overriding
|
||||
# this in particular embedded cases if we can verify that system certs
|
||||
# are working.
|
||||
# (We also allow forcing this via an env var if the user desires)
|
||||
# pylint: disable=wrong-import-position
|
||||
if (_ba.contains_python_dist()
|
||||
or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'):
|
||||
import certifi
|
||||
|
||||
# Let both OpenSSL and requests (if present) know to use this.
|
||||
os.environ['SSL_CERT_FILE'] = os.environ['REQUESTS_CA_BUNDLE'] = (
|
||||
certifi.where())
|
||||
|
||||
# FIXME: I think we should init Python in the main thread, which should
|
||||
# also avoid these issues. (and also might help us play better with
|
||||
# Python debuggers?)
|
||||
|
||||
# Gloriously hacky workaround here:
|
||||
# Our 'main' Python thread is the game thread (not the app's main
|
||||
# thread) which means it has a small stack compared to the main
|
||||
# thread (at least on apple). Sadly it turns out this causes the
|
||||
# debug build of Python to blow its stack immediately when doing
|
||||
# some big imports.
|
||||
# Normally we'd just give the game thread the same stack size as
|
||||
# the main thread and that'd be the end of it. However
|
||||
# we're using std::threads which it turns out have no way to set
|
||||
# the stack size (as of fall '19). Grumble.
|
||||
#
|
||||
# However python threads *can* take custom stack sizes.
|
||||
# (and it appears they might use the main thread's by default?..)
|
||||
# ...so as a workaround in the debug version, we can run problematic
|
||||
# heavy imports here in another thread and all is well.
|
||||
# If we ever see stack overflows in our release build we'll have
|
||||
# to take more drastic measures like switching from std::threads
|
||||
# to pthreads.
|
||||
|
||||
if debug_build:
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def _thread_func() -> None:
|
||||
# pylint: disable=unused-import
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
testthread = threading.Thread(target=_thread_func)
|
||||
testthread.start()
|
||||
testthread.join()
|
||||
del testthread
|
||||
|
||||
# Clear out the standard quit/exit messages since they don't work for us.
|
||||
# pylint: disable=c-extension-no-member
|
||||
if not TYPE_CHECKING:
|
||||
import __main__
|
||||
del __main__.__builtins__.quit
|
||||
del __main__.__builtins__.exit
|
||||
|
||||
# Now spin up our App instance, store it on both _ba and ba,
|
||||
# and return it to the C++ layer.
|
||||
# The import is down here since it won't work until we muck with paths.
|
||||
# noinspection PyProtectedMember
|
||||
from ba._app import App
|
||||
import ba
|
||||
# pylint: disable=wrong-import-position
|
||||
from ba._bootstrap import bootstrap
|
||||
|
||||
_ba.app = ba.app = App()
|
||||
bootstrap()
|
||||
|
||||
247
tools/efro/log.py
Normal file
247
tools/efro/log.py
Normal file
@ -0,0 +1,247 @@
|
||||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Logging functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import datetime
|
||||
import threading
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
from efro.util import utc_now
|
||||
from efro.terminal import TerminalColor
|
||||
from efro.dataclassio import ioprepped, IOAttrs, dataclass_to_json
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class LogLevel(Enum):
|
||||
"""Severity level for a log entry.
|
||||
|
||||
These enums have numeric values so they can be compared in severity.
|
||||
Note that these values are not currently interchangeable with the
|
||||
logging.ERROR, logging.DEBUG, etc. values.
|
||||
"""
|
||||
DEBUG = 0
|
||||
INFO = 1
|
||||
WARNING = 2
|
||||
ERROR = 3
|
||||
CRITICAL = 4
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class LogEntry:
|
||||
"""Single logged message."""
|
||||
name: Annotated[str,
|
||||
IOAttrs('n', soft_default='root', store_default=False)]
|
||||
message: Annotated[str, IOAttrs('m')]
|
||||
level: Annotated[LogLevel, IOAttrs('l')]
|
||||
time: Annotated[datetime.datetime, IOAttrs('t')]
|
||||
|
||||
|
||||
class LogHandler(logging.Handler):
|
||||
"""Fancy-pants handler for logging output.
|
||||
|
||||
Writes logs to disk in structured json format and echoes them
|
||||
to stdout/stderr with pretty colors.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
path: str | Path | None,
|
||||
echofile: Any,
|
||||
suppress_non_root_debug: bool = False):
|
||||
super().__init__()
|
||||
# pylint: disable=consider-using-with
|
||||
self._file = (None
|
||||
if path is None else open(path, 'w', encoding='utf-8'))
|
||||
self._echofile = echofile
|
||||
self._callbacks: list[Callable[[LogEntry], None]] = []
|
||||
self._suppress_non_root_debug = suppress_non_root_debug
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
|
||||
# Special case - filter out this common extra-chatty category.
|
||||
# TODO - should use a standard logging.Filter for this.
|
||||
if (self._suppress_non_root_debug and record.name != 'root'
|
||||
and record.levelname == 'DEBUG'):
|
||||
return
|
||||
|
||||
# Bake down all log formatting into a simple string.
|
||||
msg = self.format(record)
|
||||
|
||||
# Translate Python log levels to our own.
|
||||
level = {
|
||||
'DEBUG': LogLevel.DEBUG,
|
||||
'INFO': LogLevel.INFO,
|
||||
'WARNING': LogLevel.WARNING,
|
||||
'ERROR': LogLevel.ERROR,
|
||||
'CRITICAL': LogLevel.CRITICAL
|
||||
}[record.levelname]
|
||||
|
||||
entry = LogEntry(message=msg,
|
||||
name=record.name,
|
||||
level=level,
|
||||
time=datetime.datetime.fromtimestamp(
|
||||
record.created, datetime.timezone.utc))
|
||||
|
||||
for call in self._callbacks:
|
||||
call(entry)
|
||||
|
||||
# Also route log entries to the echo file (generally stdout/stderr)
|
||||
# with pretty colors.
|
||||
if self._echofile is not None:
|
||||
cbegin: str
|
||||
cend: str
|
||||
cbegin, cend = {
|
||||
LogLevel.DEBUG:
|
||||
(TerminalColor.CYAN.value, TerminalColor.RESET.value),
|
||||
LogLevel.INFO: ('', ''),
|
||||
LogLevel.WARNING:
|
||||
(TerminalColor.YELLOW.value, TerminalColor.RESET.value),
|
||||
LogLevel.ERROR:
|
||||
(TerminalColor.RED.value, TerminalColor.RESET.value),
|
||||
LogLevel.CRITICAL:
|
||||
(TerminalColor.STRONG_MAGENTA.value +
|
||||
TerminalColor.BOLD.value + TerminalColor.BG_BLACK.value,
|
||||
TerminalColor.RESET.value),
|
||||
}[level]
|
||||
|
||||
self._echofile.write(f'{cbegin}{msg}{cend}\n')
|
||||
|
||||
# Note to self: it sounds like logging wraps calls to us
|
||||
# in a lock so we shouldn't have to worry about garbled
|
||||
# json output due to multiple threads writing at once,
|
||||
# but may be good to find out for sure?
|
||||
if self._file is not None:
|
||||
entry_s = dataclass_to_json(entry)
|
||||
assert '\n' not in entry_s # make sure its a single line
|
||||
print(entry_s, file=self._file, flush=True)
|
||||
|
||||
def emit_custom(self, name: str, message: str, level: LogLevel) -> None:
|
||||
"""Custom emit call for our stdout/stderr redirection."""
|
||||
entry = LogEntry(name=name,
|
||||
message=message,
|
||||
level=level,
|
||||
time=utc_now())
|
||||
|
||||
for call in self._callbacks:
|
||||
call(entry)
|
||||
|
||||
if self._file is not None:
|
||||
entry_s = dataclass_to_json(entry)
|
||||
assert '\n' not in entry_s # Make sure its a single line.
|
||||
print(entry_s, file=self._file, flush=True)
|
||||
|
||||
def add_callback(self, call: Callable[[LogEntry], None]) -> None:
|
||||
"""Add a callback to be run for each added entry."""
|
||||
self._callbacks.append(call)
|
||||
|
||||
|
||||
class LogRedirect:
|
||||
"""A file-like object for redirecting stdout/stderr to our log."""
|
||||
|
||||
def __init__(self, name: str, orig_out: Any, log_handler: LogHandler,
|
||||
log_level: LogLevel):
|
||||
self._name = name
|
||||
self._orig_out = orig_out
|
||||
self._log_handler = log_handler
|
||||
self._log_level = log_level
|
||||
self._chunk = ''
|
||||
self._chunk_start_time = 0.0
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def write(self, s: str) -> None:
|
||||
"""Write something to output."""
|
||||
|
||||
assert isinstance(s, str)
|
||||
|
||||
# First, ship it off to the original destination.
|
||||
self._orig_out.write(s)
|
||||
|
||||
# Now add this to our chunk and ship completed chunks
|
||||
# off to the logger.
|
||||
# Let's consider a chunk completed when we're passed
|
||||
# a single '\n' by itself. (print() statement will do
|
||||
# this at the end by default).
|
||||
# We may get some false positives/negatives this way
|
||||
# but it should result in *most* big multi-line print
|
||||
# statements being wrapped into a single log entry.
|
||||
# Also, flush with only_old=True can be called periodically
|
||||
# to dump any pending chunks that don't happen to fit
|
||||
# this pattern.
|
||||
with self._lock:
|
||||
if s == '\n':
|
||||
self._log_handler.emit_custom(name=self._name,
|
||||
message=self._chunk,
|
||||
level=self._log_level)
|
||||
self._chunk = ''
|
||||
else:
|
||||
if self._chunk == '':
|
||||
self._chunk_start_time = time.time()
|
||||
self._chunk += s
|
||||
|
||||
def flush(self, only_old: bool = False) -> None:
|
||||
"""Flushhhhh!"""
|
||||
self._orig_out.flush()
|
||||
if only_old and time.time() - self._chunk_start_time < 0.5:
|
||||
return
|
||||
with self._lock:
|
||||
if self._chunk != '':
|
||||
chunk = self._chunk
|
||||
if chunk.endswith('\n'):
|
||||
chunk = chunk[:-1]
|
||||
self._log_handler.emit_custom(name=self._name,
|
||||
message=chunk,
|
||||
level=self._log_level)
|
||||
self._chunk = ''
|
||||
|
||||
|
||||
def setup_logging(log_path: str | Path | None,
|
||||
level: LogLevel,
|
||||
suppress_non_root_debug: bool = False) -> LogHandler:
|
||||
"""Set up our logging environment.
|
||||
|
||||
Returns the custom handler which can be used to fetch information
|
||||
about logs that have passed through it. (worst log-levels, etc.).
|
||||
"""
|
||||
|
||||
lmap = {
|
||||
LogLevel.DEBUG: logging.DEBUG,
|
||||
LogLevel.INFO: logging.INFO,
|
||||
LogLevel.WARNING: logging.WARNING,
|
||||
LogLevel.ERROR: logging.ERROR,
|
||||
LogLevel.CRITICAL: logging.CRITICAL,
|
||||
}
|
||||
|
||||
# Wire logger output to go to a structured log file.
|
||||
# Also echo it to stderr IF we're running in a terminal.
|
||||
loghandler = LogHandler(
|
||||
log_path,
|
||||
echofile=sys.stderr if sys.stderr.isatty() else None,
|
||||
suppress_non_root_debug=suppress_non_root_debug)
|
||||
|
||||
logging.basicConfig(level=lmap[level],
|
||||
format='%(message)s',
|
||||
handlers=[loghandler])
|
||||
|
||||
# DISABLING THIS BIT FOR NOW - want to keep things as pure as possible.
|
||||
if bool(False):
|
||||
# Now wire Python stdout/stderr output to generate log entries
|
||||
# in addition to its regular routing. Make sure to do this *after* we
|
||||
# tell the log-handler to write to stderr, otherwise we get an infinite
|
||||
# loop.
|
||||
# NOTE: remember that this won't capture subcommands or other
|
||||
# non-python stdout/stderr output.
|
||||
sys.stdout = LogRedirect( # type: ignore
|
||||
'stdout', sys.stdout, loghandler, LogLevel.INFO)
|
||||
sys.stderr = LogRedirect( # type: ignore
|
||||
'stderr', sys.stderr, loghandler, LogLevel.INFO)
|
||||
|
||||
return loghandler
|
||||
Loading…
x
Reference in New Issue
Block a user