Initial 1.6 builds with private party front-end in place

This commit is contained in:
Eric Froemling 2021-01-26 05:00:34 -08:00
parent ebaf83c502
commit 2a66b5b93f
61 changed files with 1531 additions and 527 deletions

View File

@ -420,7 +420,7 @@
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/a9/71/9286d55c45c37877f3267850f90b",
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/2f/09/36e691de67eb8f155449a7170861",
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/fd/a8/ad50785ce206e8dc3dcc7358b173",
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/34/93/4989950b5dd035da056a0291dbb2",
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/c2/36/59ab3af6b45307c79ba8c296df5b",
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/0d/25/26de912f111de8189f40aeeddee6",
"assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/44/ed/5b972fa848cffb73723533c2ccb7",
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/6a/20/57d91c24e0aeb1fe6b7d6dbbcac9",
@ -429,27 +429,27 @@
"assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/ce/de/b0f462205cdf687a875abc6f39bd",
"assets/build/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/3f/46/e4da3c1d2b0ebf916df55c608b28",
"assets/build/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/d1/07/37b7adc3dbec7328d26c5325f212",
"assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/3f/ee/72767c1e922d3f2cf668147ef143",
"assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/66/d1/8adfe1479fe6b4c30cd0d0e694d6",
"assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/6e/fd/685a4e1da031474d47a1d9eb2731",
"assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/18/b2/9c1f6e3ca6e18d6a6fdbdb14e224",
"assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/19/ba/b12493cfaa28d27f9bfee0459e20",
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/82/9f/bdcff3ac022c125a7232a412568e",
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/43/f3/9e88a199337b7913cb5e7961b1c6",
"assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/51/31/64479524c0ee990b3e97ffdca068",
"assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/ed/98/37d9457755f7e86e2f2875e3b055",
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/87/2d/027aa239eb66ea8f496562f4fd83",
"assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/a4/01/1fcc28b303858b3d028d26516907",
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/20/ca/d675783cd094030a625e7ce023cf",
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/6b/5a/6e8e3692347d9ba01aff607af5a8",
"assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/0a/84/bbb6ed2abf66509406f534cbbb52",
"assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/f8/e6/773d3da1cbdb2215956f9d99c348",
"assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/29/72/bcf75316f71373a47739a72ad6da",
"assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/3d/ba/7f4c8846babc5ef59187e87efa3d",
"assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/29/7d/be03fa23b8d80404b1b56c305edc",
"assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/44/3c/7cc06ca8d5475e1687d0ed05bdbf",
"assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/07/42/382831beefb1f134eed95d18b20b",
"assets/build/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/05/5b/910b8963f48cd494dc01d5a18a5c",
"assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/b7/0a/fab820b96e7aa587ee56427ecdc2",
"assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/58/3b/2bdbad7748ab0c76b9cf3d65c70e",
"assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/e0/83/32c5bc6544b647dbc599c11123df",
"assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/50/9f/be006ba19be6a69a57837eb6dca0",
"assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/19/fe/c97df315575d999ad2bb63478b5f",
"assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/2d/a3/114ca23a3fb0f2885bfbbabb9727",
"assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/66/b0/e1d71e57673a6fc78e0ea181b76b",
"assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/e4/3e/243eaa0237361b984fc6c56042be",
"assets/build/ba_data/data/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/04/52/683a27aaf9aa7c63e7e595f80d08",
@ -3932,40 +3932,40 @@
"assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450",
"assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e",
"assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f",
"build/prefab/full/linux_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/1c/5f/2e5338ab577aa7dac9942dda3d43",
"build/prefab/full/linux_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/50/bb/7a9daf22e3a09c6ab40f6b6909f2",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e5/6f/10a4fc55ee08a4f6e1d8f3b87422",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f3/fa/ba9a4fd942854e6d330079a5d5b2",
"build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/68/5d/09e80048115eaaa434ffe85a7f8f",
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/60/0e/804d130b536483882ee3f4299375",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/73/15/f45a3b15110a0cc0493ef17fae4c",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/55/e7/618d3ecd8072a67d07b1c1098369",
"build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/54/31/bce32b4e1aef2b4bd27dc9336619",
"build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/bf/5a/45640c9cdda02fdffd1c9b3beebe",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ed/ab/1d70d046b60626e74ab6a6d8d733",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cb/fb/135c187787329c51b6770a5f5135",
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/1a/1d/fb869851bf6e54e215d1447d1b68",
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/64/99/58dfc239e6fdac005d583287507f",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/00/a6/2727ba6b98c46e4ab1414be214a7",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a9/47/954ea39895cad0354cd1b39e4436",
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ac/62/9c8551a51d8f458e59e856cd4a3a",
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/75/e9/9674fac783d2827e5daf33b91afc",
"build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/f2/46/c17af441ceb12cd41895c8482475",
"build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/c8/16/000156a1d2728de92b7bcdaa1f22",
"build/prefab/lib/linux_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/65/e2/82162e0ab11caf2634692d2d7a4e",
"build/prefab/lib/linux_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6f/4d/baa3f14360c0d379ccb5ae330295",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c9/4a/167038e8603b915bc6bdebc886a5",
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fa/a3/3f9a8d8856f9db3340adc5488d9b",
"build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ef/d0/49b99ce32e5e2b01b056fbac5c67",
"build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/83/25/980050d75bbea49a84652209050c",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/47/aa/e82233695a50974e7e22db4e7146",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2b/45/7f9fbae208890455fce2fbc172d3",
"build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fc/75/8ea8b8eeb9a1c47c534bfb9e5d3f",
"build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/69/a9/01af1b4a126cf517e9bbbfade412",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d5/f5/7c740da86b84653a3d7b23cb11d7",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/47/64/f4a9fb9a0c338dd0daa0dc0e52a6",
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/74/66/c94da5b860d0581b20c9679d183f",
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0f/c1/90db918c2dce94bd94725b25b2b4",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0f/49/4269f4e88a55e6712841b7b56f5a",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f7/6c/fe17cf1a3f98cacbe946549631c5"
"build/prefab/full/linux_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/3c/61/69d59f46d61b3aeee054aa5daab1",
"build/prefab/full/linux_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d2/41/e8362cfe9f6fd6dc882858021874",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/83/ff/29cf07587f212d5278bb5ed92e75",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/90/e7/c4834a3b41d8f9d837071dc0800a",
"build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ce/3d/0febcd4db42fe5dedb701c60bddf",
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c5/d8/3d7ca6668af72200a6eea00c6d84",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/79/f2/3381ea14e11854a9e8804953bb06",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/80/00/a02fdfe67796bc04a78dbd6ed353",
"build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/19/36/ebf5f0f1c7a2923f7e3c4ee292ad",
"build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/fb/4f/fbddddae2d5963a1ec6f97234db3",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0d/32/8755d570c2e74316497bdd290f0d",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/24/30/863f0c9948669219f66b117502bc",
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/3c/dc/317d57caa8b27d5b685193cf3de9",
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/cd/8a/3fc0cca1383b6af2a4f8a5ae2cfe",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/20/16/c0cf342b1971dcbc757abd29bdc5",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1b/06/f81485b10ac37b58a012bc158952",
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/85/44/92172591b72302dc4909b0e91108",
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d4/e6/8d5a9ffcf32588f8b770a88a80d5",
"build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/c7/9c/e55d58caf88ad6bb88f1a5ebfdbf",
"build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/f2/6e/981553869590e8367854b5df2802",
"build/prefab/lib/linux_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/34/21/016123d9ec8293ce92ba910dd00a",
"build/prefab/lib/linux_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6f/f8/cf46e7c33a0a237e2c6f19ef4f92",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/10/67/5a3d6131d1b1fdf9f629301b68db",
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6b/bc/72d32c36d51acb255b2667c4d386",
"build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7c/9b/af5e799cb4d296074598f11a063f",
"build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/35/7c/b5f26ca01907df6ea80ff3ae5861",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8c/76/9ea770a68773fd4a08fb78855fbb",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1d/b3/329956f15f5cb76a63bd4fd3e0cc",
"build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/30/04/7b7c4f847fd992b23719f755981a",
"build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/bc/20/e13686c62052e3e388431d4859aa",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a3/a1/1bb8a1926628e34054aeca5a0178",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5a/33/c232d2c633bf70915e1a8eecdf95",
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/84/8c/2930553d642210c81b6342bffb9f",
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/68/22/cc580cef75b9e452de66095ba62b",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/93/2d/b6482a7ce6957156fe050f4cc9dc",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d1/a2/1d592f5d21dd39d7b16da8f1c8c4"
}

View File

@ -45,6 +45,7 @@
<w>adbcfaca</w>
<w>adbpath</w>
<w>addcall</w>
<w>addchars</w>
<w>addgame</w>
<w>addlevel</w>
<w>addr</w>
@ -98,6 +99,7 @@
<w>archs</w>
<w>argh</w>
<w>argparse</w>
<w>argsjoined</w>
<w>argtypes</w>
<w>argval</w>
<w>armeabi</w>
@ -146,6 +148,7 @@
<w>autoretain</w>
<w>autoselect</w>
<w>autotools</w>
<w>availmins</w>
<w>availplug</w>
<w>aval</w>
<w>axismotion</w>
@ -244,6 +247,7 @@
<w>bsuffix</w>
<w>bsui</w>
<w>btnh</w>
<w>btnlabel</w>
<w>btnv</w>
<w>btnx</w>
<w>btype</w>
@ -279,6 +283,7 @@
<w>cameraflash</w>
<w>camerashake</w>
<w>campaignname</w>
<w>cancelbtn</w>
<w>capb</w>
<w>capturetheflag</w>
<w>carentity</w>
@ -286,6 +291,7 @@
<w>cbits</w>
<w>cbot</w>
<w>cbtn</w>
<w>cbtnoffs</w>
<w>ccfgs</w>
<w>ccode</w>
<w>ccompiler</w>
@ -433,6 +439,7 @@
<w>crashlytics</w>
<w>creationflags</w>
<w>creditslist</w>
<w>cresult</w>
<w>cryptmodule</w>
<w>cspbd</w>
<w>cspnf</w>
@ -782,6 +789,7 @@
<w>freeforallendscreen</w>
<w>freeforallsession</w>
<w>freeforallvictory</w>
<w>freemins</w>
<w>freepik</w>
<w>freesound</w>
<w>froemling</w>
@ -966,6 +974,7 @@
<w>homebrew</w>
<w>hometest</w>
<w>hostconfig</w>
<w>hostingstate</w>
<w>hostuser</w>
<w>hout</w>
<w>howtoplay</w>
@ -1201,6 +1210,7 @@
<w>locationlist</w>
<w>locationsingles</w>
<w>locationval</w>
<w>lockpath</w>
<w>lockstr</w>
<w>locktype</w>
<w>locs</w>
@ -1417,6 +1427,7 @@
<w>nosynctool</w>
<w>nosynctools</w>
<w>notdir</w>
<w>nowtickets</w>
<w>npos</w>
<w>nprocessors</w>
<w>ntpath</w>
@ -1439,6 +1450,8 @@
<w>oculus</w>
<w>oenval</w>
<w>offsanchor</w>
<w>offsx</w>
<w>offsy</w>
<w>ofval</w>
<w>oggenc</w>
<w>oghash</w>
@ -1591,6 +1604,7 @@
<w>powervr</w>
<w>ppos</w>
<w>pproxy</w>
<w>pptabcom</w>
<w>pragmas</w>
<w>prch</w>
<w>prec</w>
@ -1619,6 +1633,7 @@
<w>printobjects</w>
<w>printpaths</w>
<w>priv</w>
<w>privatetab</w>
<w>proactor</w>
<w>proc</w>
<w>procs</w>
@ -1628,6 +1643,7 @@
<w>profilenames</w>
<w>proj</w>
<w>projconfig</w>
<w>projdir</w>
<w>projectpath</w>
<w>projectroot</w>
<w>projroot</w>
@ -1751,6 +1767,7 @@
<w>reqtype</w>
<w>reqtypes</w>
<w>resample</w>
<w>resetbtn</w>
<w>resetinput</w>
<w>resourcetypeinfo</w>
<w>respawn</w>
@ -1802,6 +1819,7 @@
<w>samsung</w>
<w>sandboxing</w>
<w>sandyrb</w>
<w>savebtn</w>
<w>savebutton</w>
<w>saxutils</w>
<w>sbblk</w>
@ -1844,7 +1862,11 @@
<w>sdkcheck</w>
<w>sdkutils</w>
<w>sdtk</w>
<w>selchild</w>
<w>selectmodule</w>
<w>selindex</w>
<w>selwidget</w>
<w>selwidgets</w>
<w>senze</w>
<w>seqtype</w>
<w>seqtypestr</w>
@ -2145,6 +2167,7 @@
<w>this'll</w>
<w>threadtype</w>
<w>throwiness</w>
<w>ticon</w>
<w>timedisplay</w>
<w>timeformat</w>
<w>timemax</w>

View File

@ -1,7 +1,14 @@
### 1.5.30 (20263)
### 1.6.0 (20268)
- Added private parties functionality (cloud hosted parties with associated codes making it easier to play with friends)
- The meta subsystem now enables new plugins by default in headless builds.
- Added option to save party in Manual tab
- Slight tidying on the tourney entry popup
- Env var to override UI scale is now BA_UI_SCALE instead of BA_FORCE_UI_SCALE.
- Fixed an issue where ba.storagename() could prevent objects on the stack from getting released cleanly
- Improvements to documentation generation such as link to some external base types.
- Added ba.clipboard_* functions for copying and pasting text on supported platforms.
- Implemented clipboard functionality on SDL based builds (such as prefab)
- Fixed an issue where click locations on scaled text fields could be incorrectly calculated.
### 1.5.29 (20246)
- Exposed ba method/class initing in public C++ layer.

View File

@ -364,16 +364,14 @@
"ba_data/python/bastd/ui/gather/__init__.py",
"ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/bases.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/googleplaytab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/privatetab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/abouttab.py",
"ba_data/python/bastd/ui/gather/bases.py",
"ba_data/python/bastd/ui/gather/googleplaytab.py",
"ba_data/python/bastd/ui/gather/manualtab.py",
"ba_data/python/bastd/ui/gather/nearbytab.py",
"ba_data/python/bastd/ui/gather/privatetab.py",
"ba_data/python/bastd/ui/gather/publictab.py",
"ba_data/python/bastd/ui/getcurrency.py",
"ba_data/python/bastd/ui/getremote.py",

View File

@ -296,10 +296,9 @@ SCRIPT_TARGETS_PY_PUBLIC = \
build/ba_data/python/bastd/ui/fileselector.py \
build/ba_data/python/bastd/ui/gather/__init__.py \
build/ba_data/python/bastd/ui/gather/abouttab.py \
build/ba_data/python/bastd/ui/gather/bases.py \
build/ba_data/python/bastd/ui/gather/googleplaytab.py \
build/ba_data/python/bastd/ui/gather/manualtab.py \
build/ba_data/python/bastd/ui/gather/nearbytab.py \
build/ba_data/python/bastd/ui/gather/privatetab.py \
build/ba_data/python/bastd/ui/gather/publictab.py \
build/ba_data/python/bastd/ui/getcurrency.py \
build/ba_data/python/bastd/ui/getremote.py \
@ -541,10 +540,9 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
build/ba_data/python/bastd/ui/__pycache__/fileselector.cpython-38.opt-1.pyc \
build/ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.opt-1.pyc \
build/ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-38.opt-1.pyc \
build/ba_data/python/bastd/ui/gather/__pycache__/bases.cpython-38.opt-1.pyc \
build/ba_data/python/bastd/ui/gather/__pycache__/googleplaytab.cpython-38.opt-1.pyc \
build/ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-38.opt-1.pyc \
build/ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-38.opt-1.pyc \
build/ba_data/python/bastd/ui/gather/__pycache__/privatetab.cpython-38.opt-1.pyc \
build/ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-38.opt-1.pyc \
build/ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-38.opt-1.pyc \
build/ba_data/python/bastd/ui/__pycache__/getremote.cpython-38.opt-1.pyc \

View File

@ -996,7 +996,7 @@ class Timer:
time: length of time (in seconds by default) that the timer will wait
before firing. Note that the actual delay experienced may vary
depending on the timetype. (see below)
depending on the timetype. (see below)
call: A callable Python object. Note that the timer will retain a
strong reference to the callable for as long as it exists, so you
@ -1006,28 +1006,11 @@ class Timer:
repeat: if True, the timer will fire repeatedly, with each successive
firing having the same delay as the first.
timetype can be either 'sim', 'base', or 'real'. It defaults to
'sim'. Types are explained below:
timetype: A ba.TimeType value determining which timeline the timer is
placed onto.
'sim' time maps to local simulation time in ba.Activity or ba.Session
Contexts. This means that it may progress slower in slow-motion play
modes, stop when the game is paused, etc. This time type is not
available in UI contexts.
'base' time is also linked to gameplay in ba.Activity or ba.Session
Contexts, but it progresses at a constant rate regardless of
slow-motion states or pausing. It can, however, slow down or stop
in certain cases such as network outages or game slowdowns due to
cpu load. Like 'sim' time, this is unavailable in UI contexts.
'real' time always maps to actual clock time with a bit of filtering
added, regardless of Context. (the filtering prevents it from going
backwards or jumping forward by large amounts due to the app being
backgrounded, system time changing, etc.)
Real time timers are currently only available in the UI context.
the 'timeformat' arg defaults to SECONDS but can also be MILLISECONDS
if you want to pass time as milliseconds.
timeformat: A ba.TimeFormat value determining how the passed time is
interpreted.
# Example: use a Timer object to print repeatedly for a few seconds:
def say_it():
@ -1035,9 +1018,9 @@ class Timer:
def stop_saying_it():
self.t = None
ba.screenmessage('MUSHROOM MUSHROOM!')
# create our timer; it will run as long as we hold self.t
# Create our timer; it will run as long as we have the self.t ref.
self.t = ba.Timer(0.3, say_it, repeat=True)
# now fire off a one-shot timer to kill it
# Now fire off a one-shot timer to kill it.
ba.timer(3.89, stop_saying_it)
"""
@ -1579,6 +1562,58 @@ def client_info_query_response(token: str, response: Any) -> None:
return None
def clipboard_get_text() -> str:
"""clipboard_get_text() -> str
Return text currently on the system clipboard.
Category: General Utility Functions
Ensure that ba.clipboard_has_text() returns True before calling
this function.
"""
return str()
def clipboard_has_text() -> bool:
"""clipboard_has_text() -> bool
Return whether there is currently text on the clipboard.
Category: General Utility Functions
This will return False if no system clipboard is available; no need
to call ba.clipboard_available() separately.
"""
return bool()
def clipboard_is_supported() -> bool:
"""clipboard_is_supported() -> bool
Return whether this platform supports clipboard operations at all.
Category: General Utility Functions
If this returns False, UIs should not show 'copy to clipboard'
buttons, etc.
"""
return bool()
def clipboard_set_text(value: str) -> None:
"""clipboard_set_text(value: str) -> None
Copy a string to the system clipboard.
Category: General Utility Functions
Ensure that ba.clipboard_available() returns True before adding
buttons/etc. that make use of this functionality.
"""
return None
def columnwidget(edit: ba.Widget = None,
parent: ba.Widget = None,
size: Sequence[float] = None,
@ -3293,24 +3328,24 @@ def restore_purchases() -> None:
return None
def rowwidget(edit: Widget = None,
parent: Widget = None,
def rowwidget(edit: ba.Widget = None,
parent: ba.Widget = None,
size: Sequence[float] = None,
position: Sequence[float] = None,
background: bool = None,
selected_child: Widget = None,
visible_child: Widget = None,
selected_child: ba.Widget = None,
visible_child: ba.Widget = None,
claims_left_right: bool = None,
claims_tab: bool = None,
selection_loops_to_parent: bool = None) -> Widget:
"""rowwidget(edit: Widget = None, parent: Widget = None,
selection_loops_to_parent: bool = None) -> ba.Widget:
"""rowwidget(edit: ba.Widget = None, parent: ba.Widget = None,
size: Sequence[float] = None,
position: Sequence[float] = None,
background: bool = None, selected_child: Widget = None,
visible_child: Widget = None,
background: bool = None, selected_child: ba.Widget = None,
visible_child: ba.Widget = None,
claims_left_right: bool = None,
claims_tab: bool = None,
selection_loops_to_parent: bool = None) -> Widget
selection_loops_to_parent: bool = None) -> ba.Widget
Create or edit a row widget.
@ -3320,7 +3355,8 @@ def rowwidget(edit: Widget = None,
a new one is created and returned. Arguments that are not set to None
are applied to the Widget.
"""
return Widget()
import ba # pylint: disable=cyclic-import
return ba.Widget()
def run_transactions() -> None:
@ -3775,8 +3811,8 @@ def submit_score(game: str,
return None
def textwidget(edit: Widget = None,
parent: Widget = None,
def textwidget(edit: ba.Widget = None,
parent: ba.Widget = None,
size: Sequence[float] = None,
position: Sequence[float] = None,
text: Union[str, ba.Lstr] = None,
@ -3787,13 +3823,13 @@ def textwidget(edit: Widget = None,
on_return_press_call: Callable[[], None] = None,
on_activate_call: Callable[[], None] = None,
selectable: bool = None,
query: Widget = None,
query: ba.Widget = None,
max_chars: int = None,
color: Sequence[float] = None,
click_activate: bool = None,
on_select_call: Callable[[], None] = None,
always_highlight: bool = None,
draw_controller: Widget = None,
draw_controller: ba.Widget = None,
scale: float = None,
corner_scale: float = None,
description: Union[str, ba.Lstr] = None,
@ -3810,16 +3846,16 @@ def textwidget(edit: Widget = None,
big: bool = None,
extra_touch_border_scale: float = None,
res_scale: float = None) -> Widget:
"""textwidget(edit: Widget = None, parent: Widget = None,
"""textwidget(edit: ba.Widget = None, parent: ba.Widget = None,
size: Sequence[float] = None, position: Sequence[float] = None,
text: Union[str, ba.Lstr] = None, v_align: str = None,
h_align: str = None, editable: bool = None, padding: float = None,
on_return_press_call: Callable[[], None] = None,
on_activate_call: Callable[[], None] = None,
selectable: bool = None, query: Widget = None, max_chars: int = None,
selectable: bool = None, query: ba.Widget = None, max_chars: int = None,
color: Sequence[float] = None, click_activate: bool = None,
on_select_call: Callable[[], None] = None,
always_highlight: bool = None, draw_controller: Widget = None,
always_highlight: bool = None, draw_controller: ba.Widget = None,
scale: float = None, corner_scale: float = None,
description: Union[str, ba.Lstr] = None,
transition_delay: float = None, maxwidth: float = None,

View File

@ -9,16 +9,16 @@ In some specific cases you may need to pull in individual submodules instead.
# pylint: disable=unused-import
# pylint: disable=redefined-builtin
from _ba import (CollideModel, Context, ContextCall, Data, InputDevice,
Material, Model, Node, SessionPlayer, Sound, Texture, Timer,
Vec3, Widget, buttonwidget, camerashake, checkboxwidget,
columnwidget, containerwidget, do_once, emitfx, getactivity,
getcollidemodel, getmodel, getnodes, getsession, getsound,
gettexture, hscrollwidget, imagewidget, log, newactivity,
newnode, playsound, printnodes, printobjects, pushcall, quit,
rowwidget, safecolor, screenmessage, scrollwidget,
set_analytics_screen, charstr, textwidget, time, timer,
open_url, widget)
from _ba import (
CollideModel, Context, ContextCall, Data, InputDevice, Material, Model,
Node, SessionPlayer, Sound, Texture, Timer, Vec3, Widget, buttonwidget,
camerashake, checkboxwidget, columnwidget, containerwidget, do_once,
emitfx, getactivity, getcollidemodel, getmodel, getnodes, getsession,
getsound, gettexture, hscrollwidget, imagewidget, log, newactivity,
newnode, playsound, printnodes, printobjects, pushcall, quit, rowwidget,
safecolor, screenmessage, scrollwidget, set_analytics_screen, charstr,
textwidget, time, timer, open_url, widget, clipboard_is_supported,
clipboard_has_text, clipboard_get_text, clipboard_set_text)
from ba._activity import Activity
from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem
from ba._actor import Actor

View File

@ -192,7 +192,7 @@ class Actor:
"""Return the ba.Activity this Actor is associated with.
If the Activity no longer exists, raises a ba.ActivityNotFoundError
or returns None depending on whether 'doraise' is set.
or returns None depending on whether 'doraise' is True.
"""
activity = self._activity()
if activity is None and doraise:

View File

@ -24,7 +24,7 @@ class AppConfig(dict):
AppConfig data is stored as json on disk on so make sure to only place
json-friendly values in it (dict, list, str, float, int, bool).
Be aware that tuples will be quietly converted to lists.
Be aware that tuples will be quietly converted to lists when stored.
"""
def resolve(self, key: str) -> Any:

View File

@ -38,16 +38,18 @@ T = TypeVar('T')
def existing(obj: Optional[ExistableType]) -> Optional[ExistableType]:
"""Convert invalid references to None for any ba.Existable type.
"""Convert invalid references to None for any ba.Existable object.
Category: Gameplay Functions
To best support type checking, it is important that invalid references
not be passed around and instead get converted to values of None.
That way the type checker can properly flag attempts to pass dead
objects into functions expecting only live ones, etc.
This call can be used on any 'existable' object (one with an exists()
method) and will convert it to a None value if it does not exist.
objects (Optional[FooType]) into functions expecting only live ones
(FooType), etc. This call can be used on any 'existable' object
(one with an exists() method) and will convert it to a None value
if it does not exist.
For more info, see notes on 'existables' here:
https://ballistica.net/wiki/Coding-Style-Guide
"""
@ -358,15 +360,17 @@ def _verify_object_death(wref: ReferenceType) -> None:
def storagename(suffix: str = None) -> str:
"""Generate a (hopefully) unique name for storing things in public places.
"""Generate a unique name for storing class data in shared places.
Category: General Utility Functions
This consists of a leading underscore, the module path at the
call site with dots replaced by underscores, the class name, and
the provided suffix. When storing data in public places such as
'customdata' dicts, this minimizes the chance of collisions if a
module or class is duplicated or renamed.
call site with dots replaced by underscores, the containing class's
qualified name, and the provided suffix. When storing data in public
places such as 'customdata' dicts, this minimizes the chance of
collisions with other similarly named classes.
Note that this will function even if called in the class definition.
# Example: generate a unique name for storage purposes:
class MyThingie:
@ -374,14 +378,21 @@ def storagename(suffix: str = None) -> str:
# This will give something like '_mymodule_submodule_mythingie_data'.
_STORENAME = ba.storagename('data')
# Use that name to store some data in the Activity we were passed.
def __init__(self, activity):
# Store some data in the Activity we were passed
activity.customdata[self._STORENAME] = {}
"""
frame = inspect.currentframe()
if frame is None:
raise RuntimeError('Cannot get current stack frame.')
fback = frame.f_back
# Note: We need to explicitly clear frame here to avoid a ref-loop
# that keeps all function-dicts in the stack alive until the next
# full GC cycle (the stack frame refers to this function's dict,
# which refers to the stack frame).
del frame
if fback is None:
raise RuntimeError('Cannot get parent stack frame.')
modulepath = fback.f_globals.get('__name__')

View File

@ -12,6 +12,7 @@ import _ba
if TYPE_CHECKING:
from typing import Callable, Any, Optional, Dict, Union, Type
import ba
class MusicType(Enum):
@ -469,8 +470,9 @@ class MusicPlayer:
self._actually_playing = False
def setmusic(musictype: Optional[MusicType], continuous: bool = False) -> None:
"""Tell the game to play (or stop playing) a certain type of music.
def setmusic(musictype: Optional[ba.MusicType],
continuous: bool = False) -> None:
"""Set the app to play (or stop playing) a certain type of music.
category: Gameplay Functions

View File

@ -284,8 +284,8 @@ class EmptyPlayer(Player['ba.EmptyTeam']):
Category: Gameplay Classes
ba.Player and ba.Team are 'Generic' types, and so passing them as
type arguments when defining a ba.Activity reduces type safety.
ba.Player and ba.Team are 'Generic' types, and so passing those top level
classes as type arguments when defining a ba.Activity reduces type safety.
For example, activity.teams[0].player will have type 'Any' in that case.
For that reason, it is better to pass EmptyPlayer and EmptyTeam when
defining a ba.Activity that does not need custom types of its own.

View File

@ -17,7 +17,7 @@ if TYPE_CHECKING:
class Session:
"""Defines a high level series of activities with a common purpose.
"""Defines a high level series of ba.Activities with a common purpose.
category: Gameplay Classes

View File

@ -200,8 +200,8 @@ class EmptyTeam(Team['ba.EmptyPlayer']):
Category: Gameplay Classes
ba.Player and ba.Team are 'Generic' types, and so passing them as
type arguments when defining a ba.Activity reduces type safety.
ba.Player and ba.Team are 'Generic' types, and so passing those top level
classes as type arguments when defining a ba.Activity reduces type safety.
For example, activity.teams[0].player will have type 'Any' in that case.
For that reason, it is better to pass EmptyPlayer and EmptyTeam when
defining a ba.Activity that does not need custom types of its own.

View File

@ -10,7 +10,7 @@ import _ba
from ba._enums import UIScale
if TYPE_CHECKING:
from typing import Optional, Dict, Any, Callable, List
from typing import Optional, Dict, Any, Callable, List, Type
from ba.ui import UICleanupCheck
import ba
@ -43,7 +43,7 @@ class UISubsystem:
else:
raise RuntimeError(f'Invalid UIScale value: {interfacetype}')
self.window_states: Dict = {} # FIXME: Kill this.
self.window_states: Dict[Type, Any] = {} # FIXME: Kill this.
self.main_menu_selection: Optional[str] = None # FIXME: Kill this.
self.have_party_queue_window = False
self.quit_window: Any = None
@ -76,7 +76,7 @@ class UISubsystem:
# this holds true at all aspect ratios.
# UPDATE: A better way to test this is now by setting the environment
# variable BA_FORCE_UI_SCALE to "small", "medium", or "large".
# variable BA_UI_SCALE to "small", "medium", or "large".
# This will affect system UIs not covered by the values below such
# as screen-messages. The below values remain functional, however,
# for cases such as Android where environment variables can't be set

View File

@ -118,7 +118,7 @@ class UIEntry:
class UIController:
"""Wrangles UILocations.
"""Wrangles ba.UILocations.
Category: User Interface Classes
"""

View File

@ -1088,13 +1088,13 @@ class AccountSettingsWindow(ba.Window):
sel_name = 'Scroll'
else:
raise ValueError('unrecognized selection')
ba.app.ui.window_states[self.__class__.__name__] = sel_name
ba.app.ui.window_states[type(self)] = sel_name
except Exception:
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__)
sel_name = ba.app.ui.window_states.get(type(self))
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'Scroll':

View File

@ -1542,7 +1542,7 @@ class CoopBrowserWindow(ba.Window):
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
sel_name = ba.app.ui.window_states.get(type(self),
{}).get('sel_name')
if sel_name == 'Back':
sel = self._back_button
@ -1572,9 +1572,7 @@ class CoopBrowserWindow(ba.Window):
sel_name = 'Scroll'
else:
raise ValueError('unrecognized selection')
ba.app.ui.window_states[self.__class__.__name__] = {
'sel_name': sel_name
}
ba.app.ui.window_states[type(self)] = {'sel_name': sel_name}
except Exception:
ba.print_exception(f'Error saving state for {self}.')

View File

@ -4,22 +4,56 @@
from __future__ import annotations
import weakref
from enum import Enum
from typing import TYPE_CHECKING
import _ba
import ba
from bastd.ui.gather.abouttab import AboutGatherTab
from bastd.ui.gather.manualtab import ManualGatherTab
from bastd.ui.gather.googleplaytab import GooglePlayGatherTab
from bastd.ui.gather.publictab import PublicGatherTab
from bastd.ui.gather.nearbytab import NearbyGatherTab
from bastd.ui.tabs import TabRow
if TYPE_CHECKING:
from typing import (Any, Optional, Tuple, Dict, List, Union, Callable,
Type)
from bastd.ui.gather.bases import GatherTab
class GatherTab:
"""Defines a tab for use in the gather UI."""
def __init__(self, window: GatherWindow) -> None:
self._window = weakref.ref(window)
@property
def window(self) -> GatherWindow:
"""The GatherWindow that this tab belongs to."""
window = self._window()
if window is None:
raise ba.NotFoundError("GatherTab's window no longer exists.")
return window
def on_activate(
self,
parent_widget: ba.Widget,
tab_button: ba.Widget,
region_width: float,
region_height: float,
region_left: float,
region_bottom: float,
) -> ba.Widget:
"""Called when the tab becomes the active one.
The tab should create and return a container widget covering the
specified region.
"""
def on_deactivate(self) -> None:
"""Called when the tab will no longer be the active one."""
def save_state(self) -> None:
"""Called when the parent window is saving state."""
def restore_state(self) -> None:
"""Called when the parent window is restoring state."""
class GatherWindow(ba.Window):
@ -29,8 +63,8 @@ class GatherWindow(ba.Window):
"""Our available tab types."""
ABOUT = 'about'
INTERNET = 'internet'
GOOGLE_PLAY = 'google_play'
LOCAL_NETWORK = 'local_network'
PRIVATE = 'private'
NEARBY = 'nearby'
MANUAL = 'manual'
def __init__(self,
@ -38,6 +72,13 @@ class GatherWindow(ba.Window):
origin_widget: ba.Widget = None):
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from bastd.ui.gather.abouttab import AboutGatherTab
from bastd.ui.gather.manualtab import ManualGatherTab
from bastd.ui.gather.privatetab import PrivateGatherTab
from bastd.ui.gather.publictab import PublicGatherTab
from bastd.ui.gather.nearbytab import NearbyGatherTab
ba.set_analytics_screen('Gather Window')
scale_origin: Optional[Tuple[float, float]]
if origin_widget is not None:
@ -104,9 +145,6 @@ class GatherWindow(ba.Window):
text=ba.Lstr(resource=self._r + '.titleText'),
maxwidth=550)
platform = ba.app.platform
subplatform = ba.app.subplatform
scroll_buffer_h = 130 + 2 * x_offs
tab_buffer_h = ((320 if condensed else 250) + 2 * x_offs)
@ -117,11 +155,10 @@ class GatherWindow(ba.Window):
if _ba.get_account_misc_read_val('enablePublicParties', True):
tabdefs.append((self.TabID.INTERNET,
ba.Lstr(resource=self._r + '.publicText')))
if platform == 'android' and subplatform == 'google':
tabdefs.append((self.TabID.GOOGLE_PLAY,
ba.Lstr(resource=self._r + '.googlePlayText')))
tabdefs.append((self.TabID.LOCAL_NETWORK,
ba.Lstr(resource=self._r + '.nearbyText')))
tabdefs.append(
(self.TabID.PRIVATE, ba.Lstr(resource=self._r + '.privateText')))
tabdefs.append(
(self.TabID.NEARBY, ba.Lstr(resource=self._r + '.nearbyText')))
tabdefs.append(
(self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText')))
@ -139,9 +176,9 @@ class GatherWindow(ba.Window):
tabtypes: Dict[GatherWindow.TabID, Type[GatherTab]] = {
self.TabID.ABOUT: AboutGatherTab,
self.TabID.MANUAL: ManualGatherTab,
self.TabID.GOOGLE_PLAY: GooglePlayGatherTab,
self.TabID.PRIVATE: PrivateGatherTab,
self.TabID.INTERNET: PublicGatherTab,
self.TabID.LOCAL_NETWORK: NearbyGatherTab
self.TabID.NEARBY: NearbyGatherTab
}
self._tabs: Dict[GatherWindow.TabID, GatherTab] = {}
for tab_id in self._tab_row.tabs:
@ -234,7 +271,7 @@ class GatherWindow(ba.Window):
sel_name = 'TabContainer'
else:
raise ValueError(f'unrecognized selection: \'{sel}\'')
ba.app.ui.window_states[self.__class__.__name__] = {
ba.app.ui.window_states[type(self)] = {
'sel_name': sel_name,
}
except Exception:
@ -247,7 +284,7 @@ class GatherWindow(ba.Window):
tab.restore_state()
sel: Optional[ba.Widget]
winstate = ba.app.ui.window_states.get(self.__class__.__name__, {})
winstate = ba.app.ui.window_states.get(type(self), {})
sel_name = winstate.get('sel_name', None)
assert isinstance(sel_name, (str, type(None)))
current_tab = self.TabID.ABOUT

View File

@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
import ba
import _ba
from bastd.ui.gather.bases import GatherTab
from bastd.ui.gather import GatherTab
if TYPE_CHECKING:
from typing import Optional

View File

@ -1,52 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for inviting/joining friends."""
from __future__ import annotations
import weakref
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from bastd.ui.gather import GatherWindow
class GatherTab:
"""Defines a tab for use in the gather UI."""
def __init__(self, window: GatherWindow) -> None:
self._window = weakref.ref(window)
@property
def window(self) -> GatherWindow:
"""The GatherWindow that this tab belongs to."""
window = self._window()
if window is None:
raise ba.NotFoundError("GatherTab's window no longer exists.")
return window
def on_activate(
self,
parent_widget: ba.Widget,
tab_button: ba.Widget,
region_width: float,
region_height: float,
region_left: float,
region_bottom: float,
) -> ba.Widget:
"""Called when the tab becomes the active one.
The tab should create and return a container widget covering the
specified region.
"""
def on_deactivate(self) -> None:
"""Called when the tab will no longer be the active one."""
def save_state(self) -> None:
"""Called when the parent window is saving state."""
def restore_state(self) -> None:
"""Called when the parent window is restoring state."""

View File

@ -1,86 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines the Google Play tab in the gather UI."""
from __future__ import annotations
from typing import TYPE_CHECKING
import _ba
import ba
from bastd.ui.gather.bases import GatherTab
if TYPE_CHECKING:
from typing import Optional
from bastd.ui.gather import GatherWindow
class GooglePlayGatherTab(GatherTab):
"""The public tab in the gather UI"""
def __init__(self, window: GatherWindow) -> None:
super().__init__(window)
self._container: Optional[ba.Widget] = None
def on_activate(
self,
parent_widget: ba.Widget,
tab_button: ba.Widget,
region_width: float,
region_height: float,
region_left: float,
region_bottom: float,
) -> ba.Widget:
c_width = region_width
c_height = 380.0
self._container = ba.containerwidget(
parent=parent_widget,
position=(region_left,
region_bottom + (region_height - c_height) * 0.5),
size=(c_width, c_height),
background=False,
selection_loops_to_parent=True)
v = c_height - 30.0
ba.textwidget(
parent=self._container,
position=(c_width * 0.5, v - 140.0),
color=(0.6, 1.0, 0.6),
scale=1.3,
size=(0.0, 0.0),
maxwidth=c_width * 0.9,
h_align='center',
v_align='center',
text=ba.Lstr(resource='googleMultiplayerDiscontinuedText'))
return self._container
def _on_google_play_show_invites_press(self) -> None:
from bastd.ui import account
if (_ba.get_account_state() != 'signed_in'
or _ba.get_account_type() != 'Google Play'):
account.show_sign_in_prompt('Google Play')
else:
_ba.show_invites_ui()
def _on_google_play_invite_press(self) -> None:
from bastd.ui.confirm import ConfirmWindow
from bastd.ui.account import show_sign_in_prompt
if (_ba.get_account_state() != 'signed_in'
or _ba.get_account_type() != 'Google Play'):
show_sign_in_prompt('Google Play')
else:
# If there's google play people connected to us, inform the user
# that they will get disconnected. Otherwise just go ahead.
google_player_count = (_ba.get_google_play_party_client_count())
if google_player_count > 0:
ConfirmWindow(
ba.Lstr(resource='gatherWindow.'
'googlePlayReInviteText',
subs=[('${COUNT}', str(google_player_count))]),
lambda: ba.timer(
0.2, _ba.invite_players, timetype=ba.TimeType.REAL),
width=500,
height=150,
ok_text=ba.Lstr(resource='gatherWindow.'
'googlePlayInviteText'))
else:
ba.timer(0.1, _ba.invite_players, timetype=ba.TimeType.REAL)

View File

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, cast
from enum import Enum
from dataclasses import dataclass
from bastd.ui.gather.bases import GatherTab
from bastd.ui.gather import GatherTab
import _ba
import ba
@ -161,11 +161,10 @@ class ManualGatherTab(GatherTab):
return self._container
def save_state(self) -> None:
ba.app.ui.window_states[self.__class__.__name__] = State(
sub_tab=self._sub_tab)
ba.app.ui.window_states[type(self)] = State(sub_tab=self._sub_tab)
def restore_state(self) -> None:
state = ba.app.ui.window_states.get(self.__class__.__name__)
state = ba.app.ui.window_states.get(type(self))
if state is None:
state = State()
assert isinstance(state, State)

View File

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING
import ba
import _ba
from bastd.ui.gather.bases import GatherTab
from bastd.ui.gather import GatherTab
if TYPE_CHECKING:
from typing import Optional, Dict, Any

View File

@ -0,0 +1,755 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines the Private tab in the gather UI."""
from __future__ import annotations
import os
import copy
import time
from enum import Enum
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
import ba
import _ba
from efro.dataclasses import dataclass_from_dict
from bastd.ui.gather import GatherTab
from bastd.ui import getcurrency
if TYPE_CHECKING:
from typing import Optional, Dict, Any, List
from bastd.ui.gather import GatherWindow
# Print a bit of info about queries, etc.
DEBUG_SERVER_COMMUNICATION = os.environ.get('BA_DEBUG_PPTABCOM') == '1'
class SubTabType(Enum):
"""Available sub-tabs."""
JOIN = 'join'
HOST = 'host'
@dataclass
class State:
"""Our core state that persists while the app is running."""
sub_tab: SubTabType = SubTabType.JOIN
@dataclass
class ConnectResult:
"""Info about a server we get back when connecting."""
error: Optional[str] = None
addr: Optional[str] = None
port: Optional[int] = None
@dataclass
class HostingState:
"""Our combined state of whether we're hosting, whether we can, etc."""
unavailable_error: Optional[str] = None
party_code: Optional[str] = None
able_to_host: bool = False
tickets_to_host_now: int = 0
minutes_until_free_host: Optional[float] = None
free_host_minutes_remaining: Optional[float] = None
class PrivateGatherTab(GatherTab):
"""The private tab in the gather UI"""
def __init__(self, window: GatherWindow) -> None:
super().__init__(window)
self._container: Optional[ba.Widget] = None
self._state: State = State()
self._hostingstate = HostingState()
self._join_sub_tab_text: Optional[ba.Widget] = None
self._host_sub_tab_text: Optional[ba.Widget] = None
self._update_timer: Optional[ba.Timer] = None
self._join_party_code_text: Optional[ba.Widget] = None
self._c_width: float = 0.0
self._c_height: float = 0.0
self._last_hosting_state_query_time: Optional[float] = None
self._initial_waiting_for_hosting_state = True
self._waiting_for_hosting_state = True
self._host_playlist_button: Optional[ba.Widget] = None
self._host_copy_button: Optional[ba.Widget] = None
self._host_connect_button: Optional[ba.Widget] = None
self._host_start_stop_button: Optional[ba.Widget] = None
self._get_tickets_button: Optional[ba.Widget] = None
self._ticket_count_text: Optional[ba.Widget] = None
self._showing_not_signed_in_screen = False
self._create_time = time.time()
self._last_action_send_time: Optional[float] = None
def on_activate(
self,
parent_widget: ba.Widget,
tab_button: ba.Widget,
region_width: float,
region_height: float,
region_left: float,
region_bottom: float,
) -> ba.Widget:
self._c_width = region_width
self._c_height = region_height - 20
self._container = ba.containerwidget(
parent=parent_widget,
position=(region_left,
region_bottom + (region_height - self._c_height) * 0.5),
size=(self._c_width, self._c_height),
background=False,
selection_loops_to_parent=True)
v = self._c_height - 30.0
self._join_sub_tab_text = ba.textwidget(
parent=self._container,
position=(self._c_width * 0.5 - 245, v - 13),
color=(0.6, 1.0, 0.6),
scale=1.3,
size=(200, 30),
maxwidth=250,
h_align='left',
v_align='center',
click_activate=True,
selectable=True,
autoselect=True,
on_activate_call=lambda: self._set_sub_tab(
SubTabType.JOIN,
playsound=True,
),
text=ba.Lstr(resource='gatherWindow.privatePartyJoinText'))
self._host_sub_tab_text = ba.textwidget(
parent=self._container,
position=(self._c_width * 0.5 + 45, v - 13),
color=(0.6, 1.0, 0.6),
scale=1.3,
size=(200, 30),
maxwidth=250,
h_align='left',
v_align='center',
click_activate=True,
selectable=True,
autoselect=True,
on_activate_call=lambda: self._set_sub_tab(
SubTabType.HOST,
playsound=True,
),
text=ba.Lstr(resource='gatherWindow.privatePartyHostText'))
ba.widget(edit=self._join_sub_tab_text, up_widget=tab_button)
ba.widget(edit=self._host_sub_tab_text,
left_widget=self._join_sub_tab_text,
up_widget=tab_button)
ba.widget(edit=self._join_sub_tab_text,
right_widget=self._host_sub_tab_text)
self._update_timer = ba.Timer(1.0,
ba.WeakCall(self._update),
repeat=True,
timetype=ba.TimeType.REAL)
# Get a new state query kicked off immediately and show nothing
# until it is back.
self._initial_waiting_for_hosting_state = True
self._last_hosting_state_query_time = None
self._last_action_send_time = None # So we don't ignore response.
self._update()
self._set_sub_tab(self._state.sub_tab)
return self._container
def on_deactivate(self) -> None:
self._update_timer = None
def _update_currency_ui(self) -> None:
# Keep currency count up to date if applicable.
try:
t_str = str(_ba.get_account_ticket_count())
except Exception:
t_str = '?'
if self._get_tickets_button:
ba.buttonwidget(edit=self._get_tickets_button,
label=ba.charstr(ba.SpecialChar.TICKET) + t_str)
if self._ticket_count_text:
ba.textwidget(edit=self._ticket_count_text,
text=ba.charstr(ba.SpecialChar.TICKET) + t_str)
def _update(self) -> None:
"""Periodic updating."""
now = ba.time(ba.TimeType.REAL)
self._update_currency_ui()
if self._state.sub_tab is SubTabType.HOST:
# If we're not signed in, just refresh to show that.
if (_ba.get_account_state() != 'signed_in'
and self._showing_not_signed_in_screen):
self._refresh_sub_tab()
else:
# Query an updated state periodically.
if (self._last_hosting_state_query_time is None
or now - self._last_hosting_state_query_time > 15.0):
self._debug_server_comm('querying private party state')
if _ba.get_account_state() == 'signed_in':
_ba.add_transaction(
{'type': 'PRIVATE_PARTY_QUERY'},
callback=ba.WeakCall(
self._hosting_state_idle_response),
)
_ba.run_transactions()
else:
self._hosting_state_idle_response(None)
self._last_hosting_state_query_time = now
def _hosting_state_idle_response(self,
result: Optional[Dict[str, Any]]) -> None:
# This simply passes through to our standard response handler.
# The one exception is if we've recently sent an action to the
# server (start/stop hosting/etc.) In that case we want to ignore
# idle background updates and wait for the response to our action.
# (this keeps the button showing 'one moment...' until the change
# takes effect, etc.)
if (self._last_action_send_time is not None
and time.time() - self._last_action_send_time < 5.0):
self._debug_server_comm('ignoring private party state response'
' due to recent action')
return
self._hosting_state_response(result)
def _hosting_state_response(self, result: Optional[Dict[str,
Any]]) -> None:
state: Optional[HostingState] = None
if result is not None:
self._debug_server_comm('got private party state response')
try:
state = dataclass_from_dict(HostingState, result)
except Exception:
pass
else:
self._debug_server_comm('private party state response errored')
# Hmm I guess let's just ignore failed responses?...
# Or should we show some sort of error state to the user?...
if result is None or state is None:
return
self._initial_waiting_for_hosting_state = False
self._waiting_for_hosting_state = False
self._hostingstate = state
self._refresh_sub_tab()
def _set_sub_tab(self, value: SubTabType, playsound: bool = False) -> None:
assert self._container
if playsound:
ba.playsound(ba.getsound('click01'))
# If switching from join to host, do a fresh state query.
if self._state.sub_tab is SubTabType.JOIN and value is SubTabType.HOST:
self._last_hosting_state_query_time = None
self._initial_waiting_for_hosting_state = True
self._last_action_send_time = None # So we don't ignore response.
self._update()
self._state.sub_tab = value
active_color = (0.6, 1.0, 0.6)
inactive_color = (0.5, 0.4, 0.5)
ba.textwidget(
edit=self._join_sub_tab_text,
color=active_color if value is SubTabType.JOIN else inactive_color)
ba.textwidget(
edit=self._host_sub_tab_text,
color=active_color if value is SubTabType.HOST else inactive_color)
self._refresh_sub_tab()
# Kick off an update to get any needed messages sent/etc.
ba.pushcall(self._update)
def _selwidgets(self) -> List[Optional[ba.Widget]]:
"""An indexed list of widgets we can use for saving/restoring sel."""
return [
self._host_playlist_button, self._host_copy_button,
self._host_connect_button, self._host_start_stop_button,
self._get_tickets_button
]
def _refresh_sub_tab(self) -> None:
assert self._container
# Store an index for our current selection so we can
# reselect the equivalent recreated widget if possible.
selindex: Optional[int] = None
selchild = self._container.get_selected_child()
if selchild is not None:
try:
selindex = self._selwidgets().index(selchild)
except ValueError:
pass
# Clear anything existing in the old sub-tab.
for widget in self._container.get_children():
if widget and widget not in {
self._host_sub_tab_text,
self._join_sub_tab_text,
}:
widget.delete()
if self._state.sub_tab is SubTabType.JOIN:
self._build_join_tab()
elif self._state.sub_tab is SubTabType.HOST:
self._build_host_tab()
else:
raise RuntimeError('Invalid state.')
# Select the new equivalent widget if there is one.
if selindex is not None:
selwidget = self._selwidgets()[selindex]
if selwidget:
ba.containerwidget(edit=self._container,
selected_child=selwidget)
def _build_join_tab(self) -> None:
ba.textwidget(parent=self._container,
position=(self._c_width * 0.5, self._c_height - 140),
color=(0.5, 0.46, 0.5),
scale=1.5,
size=(0, 0),
maxwidth=250,
h_align='center',
v_align='center',
text=ba.Lstr(resource='gatherWindow.partyCodeText'))
self._join_party_code_text = ba.textwidget(
parent=self._container,
position=(self._c_width * 0.5 - 150, self._c_height - 250),
flatness=1.0,
scale=1.5,
size=(300, 50),
editable=True,
description=ba.Lstr(resource='gatherWindow.partyCodeText'),
autoselect=True,
maxwidth=250,
h_align='left',
v_align='center',
text='')
btn = ba.buttonwidget(parent=self._container,
size=(300, 70),
label=ba.Lstr(resource='gatherWindow.'
'manualConnectText'),
position=(self._c_width * 0.5 - 150,
self._c_height - 350),
on_activate_call=self._connect_press,
autoselect=True)
ba.textwidget(edit=self._join_party_code_text,
on_return_press_call=btn.activate)
def _on_get_tickets_press(self) -> None:
if self._waiting_for_hosting_state:
return
# Bring up get-tickets window and then kill ourself (we're on the
# overlay layer so we'd show up above it).
getcurrency.GetCurrencyWindow(modal=True,
origin_widget=self._get_tickets_button)
# self._transition_out()
def _build_host_tab(self) -> None:
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
if _ba.get_account_state() != 'signed_in':
ba.textwidget(parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=200,
scale=0.8,
color=(0.6, 0.56, 0.6),
position=(self._c_width * 0.5, self._c_height * 0.5),
text=ba.Lstr(resource='notSignedInErrorText'))
self._showing_not_signed_in_screen = True
return
self._showing_not_signed_in_screen = False
# At first we don't want to show anything until we've gotten a state.
if self._initial_waiting_for_hosting_state:
ba.textwidget(
parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=200,
scale=0.8,
color=(0.6, 0.56, 0.6),
position=(self._c_width * 0.5, self._c_height * 0.5),
text=ba.Lstr(
value='${A}...',
subs=[('${A}', ba.Lstr(resource='store.loadingText'))],
),
)
return
# If we're not currently hosting and hosting requires tickets,
# Show our count (possibly with a link to purchase more).
if (self._hostingstate.party_code is None
and self._hostingstate.tickets_to_host_now != 0):
if not ba.app.ui.use_toolbars:
if ba.app.allow_ticket_purchases:
self._get_tickets_button = ba.buttonwidget(
parent=self._container,
position=(self._c_width - 210 + 125,
self._c_height - 44),
autoselect=True,
scale=0.6,
size=(120, 60),
textcolor=(0.2, 1, 0.2),
label=ba.charstr(ba.SpecialChar.TICKET),
color=(0.65, 0.5, 0.8),
on_activate_call=self._on_get_tickets_press)
else:
self._ticket_count_text = ba.textwidget(
parent=self._container,
scale=0.6,
position=(self._c_width - 210 + 125,
self._c_height - 44),
color=(0.2, 1, 0.2),
h_align='center',
v_align='center')
# Set initial ticket count.
self._update_currency_ui()
v = self._c_height - 90
if self._hostingstate.party_code is None:
ba.textwidget(
parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=self._c_width * 0.9,
scale=0.7,
flatness=1.0,
color=(0.5, 0.46, 0.5),
position=(self._c_width * 0.5, v),
text=ba.Lstr(
resource='gatherWindow.privatePartyCloudDescriptionText'))
v -= 100
if self._hostingstate.party_code is None:
# We've got no current party running; show options to set one up.
ba.textwidget(parent=self._container,
size=(0, 0),
h_align='right',
v_align='center',
maxwidth=200,
scale=0.8,
color=(0.6, 0.56, 0.6),
position=(self._c_width * 0.5 - 210, v),
text=ba.Lstr(resource='playlistText'))
self._host_playlist_button = ba.buttonwidget(
parent=self._container,
size=(400, 70),
color=(0.6, 0.5, 0.6),
textcolor=(0.8, 0.75, 0.8),
label='Default Free-For-All Playlist',
on_activate_call=lambda: ba.screenmessage(
'TODO: WIRE UP PLAYLIST SELECTION'),
position=(self._c_width * 0.5 - 200, v - 35),
up_widget=self._host_sub_tab_text,
autoselect=True)
else:
# We've got a current party; show its info.
ba.textwidget(
parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=600,
scale=0.9,
color=(0.7, 0.64, 0.7),
position=(self._c_width * 0.5, v + 90),
text=ba.Lstr(resource='gatherWindow.partyServerRunningText'))
ba.textwidget(parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=600,
scale=0.7,
color=(0.7, 0.64, 0.7),
position=(self._c_width * 0.5, v + 50),
text=ba.Lstr(resource='gatherWindow.partyCodeText'))
ba.textwidget(parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
scale=2.0,
color=(0.0, 1.0, 0.0),
position=(self._c_width * 0.5, v + 10),
text=self._hostingstate.party_code)
# Also action buttons to copy it and connect to it.
if ba.clipboard_is_supported():
cbtnoffs = 10
self._host_copy_button = ba.buttonwidget(
parent=self._container,
size=(140, 40),
color=(0.6, 0.5, 0.6),
textcolor=(0.8, 0.75, 0.8),
label=ba.Lstr(resource='gatherWindow.copyCodeText'),
on_activate_call=self._host_copy_press,
position=(self._c_width * 0.5 - 150, v - 70),
autoselect=True)
else:
cbtnoffs = -70
self._host_connect_button = ba.buttonwidget(
parent=self._container,
size=(140, 40),
color=(0.6, 0.5, 0.6),
textcolor=(0.8, 0.75, 0.8),
label=ba.Lstr(resource='gatherWindow.manualConnectText'),
on_activate_call=self._host_connect_press,
position=(self._c_width * 0.5 + cbtnoffs, v - 70),
autoselect=True)
v -= 120
# Line above the main action button:
# If hosting is unavailable, show the associated reason.
if self._hostingstate.unavailable_error is not None:
ba.textwidget(
parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=self._c_width * 0.9,
scale=0.7,
flatness=1.0,
color=(1.0, 0.0, 0.0),
position=(self._c_width * 0.5, v),
text=ba.Lstr(translate=('serverResponses',
self._hostingstate.unavailable_error)))
elif self._hostingstate.free_host_minutes_remaining is not None:
# If we've been pre-approved to start/stop for free, show that.
ba.textwidget(
parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=self._c_width * 0.9,
scale=0.7,
flatness=1.0,
color=((0.7, 0.64, 0.7) if self._hostingstate.party_code else
(0.0, 1.0, 0.0)),
position=(self._c_width * 0.5, v),
text=ba.Lstr(
resource='gatherWindow.startStopHostingMinutesText',
subs=[(
'${MINUTES}',
f'{self._hostingstate.free_host_minutes_remaining:.0f}'
)]))
else:
# Otherwise tell whether the free cloud server is available
# or will be at some point.
if self._hostingstate.party_code is None:
if self._hostingstate.tickets_to_host_now == 0:
ba.textwidget(
parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=self._c_width * 0.9,
scale=0.7,
flatness=1.0,
color=(0.0, 1.0, 0.0),
position=(self._c_width * 0.5, v),
text=ba.Lstr(
resource=
'gatherWindow.freeCloudServerAvailableNowText'))
else:
if self._hostingstate.minutes_until_free_host is None:
ba.textwidget(
parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=self._c_width * 0.9,
scale=0.7,
flatness=1.0,
color=(1.0, 0.6, 0.0),
position=(self._c_width * 0.5, v),
text=ba.Lstr(
resource=
'gatherWindow.freeCloudServerNotAvailableText')
)
else:
availmins = self._hostingstate.minutes_until_free_host
ba.textwidget(
parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=self._c_width * 0.9,
scale=0.7,
flatness=1.0,
color=(1.0, 0.6, 0.0),
position=(self._c_width * 0.5, v),
text=ba.Lstr(resource='gatherWindow.'
'freeCloudServerAvailableMinutesText',
subs=[('${MINUTES}',
f'{availmins:.0f}')]))
v -= 100
if self._waiting_for_hosting_state:
btnlabel = ba.Lstr(resource='oneMomentText')
else:
if self._hostingstate.unavailable_error is not None:
btnlabel = ba.Lstr(
resource='gatherWindow.hostingUnavailableText')
elif self._hostingstate.party_code is None:
ticon = _ba.charstr(ba.SpecialChar.TICKET)
nowtickets = self._hostingstate.tickets_to_host_now
if nowtickets > 0:
btnlabel = ba.Lstr(
resource='gatherWindow.startHostingPaidText',
subs=[('${COST}', f'{ticon}{nowtickets}')])
else:
btnlabel = ba.Lstr(
resource='gatherWindow.startHostingText')
else:
btnlabel = ba.Lstr(resource='gatherWindow.stopHostingText')
self._host_start_stop_button = ba.buttonwidget(
parent=self._container,
size=(400, 80),
color=((0.6, 0.6, 0.6)
if self._hostingstate.unavailable_error is not None else
(0.5, 1.0,
0.5) if self._waiting_for_hosting_state else None),
enable_sound=False,
label=btnlabel,
textcolor=((0.7, 0.7, 0.7)
if self._hostingstate.unavailable_error else None),
position=(self._c_width * 0.5 - 200, v),
on_activate_call=self._host_button_press,
autoselect=True)
def _host_copy_press(self) -> None:
assert self._hostingstate.party_code is not None
ba.clipboard_set_text(self._hostingstate.party_code)
ba.screenmessage(ba.Lstr(resource='gatherWindow.copyCodeConfirmText'))
def _host_connect_press(self) -> None:
assert self._hostingstate.party_code is not None
self._connect_to_party_code(self._hostingstate.party_code)
def _debug_server_comm(self, msg: str) -> None:
if DEBUG_SERVER_COMMUNICATION:
print(f'PPTABCOM: {msg} at time '
f'{time.time()-self._create_time:.2f}')
def _connect_to_party_code(self, code: str) -> None:
self._debug_server_comm('sending private party connect')
_ba.add_transaction(
{
'type': 'PRIVATE_PARTY_CONNECT',
'code': code
},
callback=ba.WeakCall(self._connect_response),
)
_ba.run_transactions()
def _host_button_press(self) -> None:
if self._waiting_for_hosting_state:
return
if _ba.get_account_state() != 'signed_in':
ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'))
ba.playsound(ba.getsound('error'))
self._refresh_sub_tab()
return
if self._hostingstate.unavailable_error is not None:
ba.playsound(ba.getsound('error'))
return
# If we're not hosting, start.
if self._hostingstate.party_code is None:
# If there's a ticket cost, make sure we have enough tickets.
if self._hostingstate.tickets_to_host_now > 0:
ticket_count: Optional[int]
try:
ticket_count = _ba.get_account_ticket_count()
except Exception:
# FIXME: should add a ba.NotSignedInError we can use here.
ticket_count = None
ticket_cost = self._hostingstate.tickets_to_host_now
if ticket_count is not None and ticket_count < ticket_cost:
getcurrency.show_get_tickets_prompt()
ba.playsound(ba.getsound('error'))
return
self._last_action_send_time = time.time()
_ba.add_transaction({'type': 'PRIVATE_PARTY_START'},
callback=ba.WeakCall(
self._hosting_state_response))
_ba.run_transactions()
else:
self._last_action_send_time = time.time()
_ba.add_transaction({'type': 'PRIVATE_PARTY_STOP'},
callback=ba.WeakCall(
self._hosting_state_response))
_ba.run_transactions()
ba.playsound(ba.getsound('click01'))
self._waiting_for_hosting_state = True
self._refresh_sub_tab()
def _connect_press(self) -> None:
code: Optional[str] = None
if self._join_party_code_text:
code = cast(str, ba.textwidget(query=self._join_party_code_text))
if not code:
ba.screenmessage(
ba.Lstr(resource='internal.invalidAddressErrorText'),
color=(1, 0, 0))
ba.playsound(ba.getsound('error'))
return
self._connect_to_party_code(code)
def _connect_response(self, result: Optional[Dict[str, Any]]) -> None:
try:
if result is None:
raise RuntimeError()
cresult = dataclass_from_dict(ConnectResult, result)
if cresult.error is not None:
self._debug_server_comm('got error connect response')
ba.screenmessage(
ba.Lstr(translate=('serverResponses', cresult.error)),
(1, 0, 0))
ba.playsound(ba.getsound('error'))
return
self._debug_server_comm('got valid connect response')
assert cresult.addr is not None and cresult.port is not None
_ba.connect_to_party(cresult.addr, port=cresult.port)
except Exception:
self._debug_server_comm('got connect response error')
ba.playsound(ba.getsound('error'))
def save_state(self) -> None:
ba.app.ui.window_states[type(self)] = copy.deepcopy(self._state)
def restore_state(self) -> None:
state = ba.app.ui.window_states.get(type(self))
if state is None:
state = State()
assert isinstance(state, State)
self._state = state

View File

@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, cast
import _ba
import ba
from bastd.ui.gather.bases import GatherTab
from bastd.ui.gather import GatherTab
if TYPE_CHECKING:
from typing import Callable, Any, Optional, Dict, Union, Tuple, List
@ -448,7 +448,7 @@ class PublicGatherTab(GatherTab):
# display these immediately when our UI comes back up which should
# be enough to make things feel nice and crisp while we do a full
# server re-query or whatnot.
ba.app.ui.window_states[self.__class__.__name__] = State(
ba.app.ui.window_states[type(self)] = State(
sub_tab=self._sub_tab,
parties=[(i, copy.copy(p)) for i, p in self._parties_sorted[:40]],
next_entry_index=self._next_entry_index,
@ -457,7 +457,7 @@ class PublicGatherTab(GatherTab):
have_valid_server_list=self._have_valid_server_list)
def restore_state(self) -> None:
state = ba.app.ui.window_states.get(self.__class__.__name__)
state = ba.app.ui.window_states.get(type(self))
if state is None:
state = State()
assert isinstance(state, State)
@ -544,9 +544,8 @@ class PublicGatherTab(GatherTab):
position=(270, v + 13),
maxwidth=150,
scale=0.8,
color=(0.5, 0.5, 0.5),
color=(0.5, 0.46, 0.5),
flatness=1.0,
shadow=0.0,
h_align='right',
v_align='center')
@ -556,9 +555,8 @@ class PublicGatherTab(GatherTab):
position=(90, v - 8),
maxwidth=60,
scale=0.6,
color=(0.5, 0.5, 0.5),
color=(0.5, 0.46, 0.5),
flatness=1.0,
shadow=0.0,
h_align='center',
v_align='center')
ba.textwidget(text=ba.Lstr(resource='gatherWindow.partySizeText'),
@ -567,9 +565,8 @@ class PublicGatherTab(GatherTab):
position=(755, v - 8),
maxwidth=60,
scale=0.6,
color=(0.5, 0.5, 0.5),
color=(0.5, 0.46, 0.5),
flatness=1.0,
shadow=0.0,
h_align='center',
v_align='center')
ba.textwidget(text=ba.Lstr(resource='gatherWindow.pingText'),
@ -578,9 +575,8 @@ class PublicGatherTab(GatherTab):
position=(825, v - 8),
maxwidth=60,
scale=0.6,
color=(0.5, 0.5, 0.5),
color=(0.5, 0.46, 0.5),
flatness=1.0,
shadow=0.0,
h_align='center',
v_align='center')
v -= sub_scroll_height + 23
@ -617,6 +613,20 @@ class PublicGatherTab(GatherTab):
v -= 25
is_public_enabled = _ba.get_public_party_enabled()
v -= 30
ba.textwidget(
parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=c_width * 0.9,
scale=0.7,
flatness=1.0,
color=(0.5, 0.46, 0.5),
position=(region_width * 0.5, v + 10),
text=ba.Lstr(resource='gatherWindow.publicHostRouterConfigText'))
v -= 30
party_name_text = ba.Lstr(
resource='gatherWindow.partyNameText',
fallback_resource='editGameListWindow.nameText')
@ -710,11 +720,10 @@ class PublicGatherTab(GatherTab):
size=(0, 0),
scale=0.7,
flatness=1.0,
shadow=0.0,
h_align='center',
v_align='top',
maxwidth=c_width,
color=(0.6, 0.6, 0.6),
maxwidth=c_width * 0.9,
color=(0.6, 0.56, 0.6),
position=(c_width * 0.5, v))
v -= 90
ba.textwidget(
@ -723,11 +732,10 @@ class PublicGatherTab(GatherTab):
size=(0, 0),
scale=0.7,
flatness=1.0,
shadow=0.0,
h_align='center',
v_align='center',
maxwidth=c_width * 0.9,
color=ba.app.ui.infotextcolor,
color=(0.5, 0.46, 0.5),
position=(c_width * 0.5, v))
# If public sharing is already on,
@ -750,8 +758,6 @@ class PublicGatherTab(GatherTab):
self._have_valid_server_list = True
parties_in = result['l']
# parties_in.reverse()
assert isinstance(parties_in, list)
self._pending_party_infos += parties_in
@ -1060,7 +1066,6 @@ class PublicGatherTab(GatherTab):
callback=ba.WeakCall(self._on_public_party_query_result))
_ba.run_transactions()
else:
# This will kick us over to a 'not signed in' message.
self._on_public_party_query_result(None)
def _ping_parties_periodically(self) -> None:

View File

@ -316,7 +316,7 @@ class KioskWindow(ba.Window):
repeat=True)
def _restore_state(self) -> None:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__)
sel_name = ba.app.ui.window_states.get(type(self))
sel: Optional[ba.Widget]
if sel_name == 'b1':
sel = self._b1
@ -355,7 +355,7 @@ class KioskWindow(ba.Window):
sel_name = 'b7'
else:
sel_name = 'b1'
ba.app.ui.window_states[self.__class__.__name__] = sel_name
ba.app.ui.window_states[type(self)] = sel_name
def _update(self) -> None:
# Kiosk-mode is designed to be used signed-out... try for force

View File

@ -540,13 +540,13 @@ class PlayWindow(ba.Window):
sel_name = 'Back'
else:
raise ValueError(f'unrecognized selection {sel}')
ba.app.ui.window_states[self.__class__.__name__] = sel_name
ba.app.ui.window_states[type(self)] = sel_name
except Exception:
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__)
sel_name = ba.app.ui.window_states.get(type(self))
if sel_name == 'Team Games':
sel = self._teams_button
elif sel_name == 'Co-op Games':

View File

@ -628,13 +628,13 @@ class PlaylistBrowserWindow(ba.Window):
sel_name = 'Scroll'
else:
raise Exception('unrecognized selected widget')
ba.app.ui.window_states[self.__class__.__name__] = sel_name
ba.app.ui.window_states[type(self)] = sel_name
except Exception:
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__)
sel_name = ba.app.ui.window_states.get(type(self))
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'Scroll':

View File

@ -347,13 +347,13 @@ class ProfileBrowserWindow(ba.Window):
sel_name = 'Scroll'
else:
sel_name = 'Back'
ba.app.ui.window_states[self.__class__.__name__] = sel_name
ba.app.ui.window_states[type(self)] = sel_name
except Exception:
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__)
sel_name = ba.app.ui.window_states.get(type(self))
if sel_name == 'Scroll':
sel = self._scrollwidget
elif sel_name == 'New':

View File

@ -626,16 +626,14 @@ class AdvancedSettingsWindow(ba.Window):
sel_name = 'Back'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
ba.app.ui.window_states[self.__class__.__name__] = {
'sel_name': sel_name
}
ba.app.ui.window_states[type(self)] = {'sel_name': sel_name}
except Exception:
ba.print_exception(f'Error saving state for {self.__class__}')
def _restore_state(self) -> None:
# pylint: disable=too-many-branches
try:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
sel_name = ba.app.ui.window_states.get(type(self),
{}).get('sel_name')
if sel_name == 'Back':
sel = self._back_button

View File

@ -256,15 +256,13 @@ class AllSettingsWindow(ba.Window):
sel_name = 'Back'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
ba.app.ui.window_states[self.__class__.__name__] = {
'sel_name': sel_name
}
ba.app.ui.window_states[type(self)] = {'sel_name': sel_name}
except Exception:
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
sel_name = ba.app.ui.window_states.get(type(self),
{}).get('sel_name')
sel: Optional[ba.Widget]
if sel_name == 'Controllers':

View File

@ -252,13 +252,13 @@ class AudioSettingsWindow(ba.Window):
sel_name = 'VRHeadRelative'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
ba.app.ui.window_states[self.__class__.__name__] = sel_name
ba.app.ui.window_states[type(self)] = sel_name
except Exception:
ba.print_exception(f'Error saving state for {self.__class__}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__)
sel_name = ba.app.ui.window_states.get(type(self))
sel: Optional[ba.Widget]
if sel_name == 'SoundMinus':
sel = self._sound_volume_numedit.minusbutton

View File

@ -457,10 +457,10 @@ class ControlsSettingsWindow(ba.Window):
sel_name = 'Wiimotes'
else:
sel_name = 'Back'
ba.app.ui.window_states[self.__class__.__name__] = sel_name
ba.app.ui.window_states[type(self)] = sel_name
def _restore_state(self) -> None:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__)
sel_name = ba.app.ui.window_states.get(type(self))
if sel_name == 'GamePads':
sel = self._gamepads_button
elif sel_name == 'Touch':

View File

@ -463,13 +463,13 @@ class SoundtrackBrowserWindow(ba.Window):
sel_name = 'Back'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
ba.app.ui.window_states[self.__class__.__name__] = sel_name
ba.app.ui.window_states[type(self)] = sel_name
except Exception:
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(self.__class__.__name__)
sel_name = ba.app.ui.window_states.get(type(self))
if sel_name == 'Scroll':
sel = self._scrollwidget
elif sel_name == 'New':

View File

@ -1011,7 +1011,7 @@ class StoreBrowserWindow(ba.Window):
sel_name = f'Tab:{selected_tab_ids[0].value}'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
ba.app.ui.window_states[self.__class__.__name__] = {
ba.app.ui.window_states[type(self)] = {
'sel_name': sel_name,
}
except Exception:
@ -1021,7 +1021,7 @@ class StoreBrowserWindow(ba.Window):
from efro.util import enum_by_value
try:
sel: Optional[ba.Widget]
sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
sel_name = ba.app.ui.window_states.get(type(self),
{}).get('sel_name')
assert isinstance(sel_name, (str, type(None)))

View File

@ -35,6 +35,7 @@ class TeamNamesColorsWindow(popup.PopupWindow):
appconfig = ba.app.config
self._names = list(
appconfig.get('Custom Team Names', DEFAULT_TEAM_NAMES))
# We need to flatten the translation since it will be an
# editable string.
self._names = [
@ -46,7 +47,7 @@ class TeamNamesColorsWindow(popup.PopupWindow):
self._color_buttons: List[ba.Widget] = []
self._color_text_fields: List[ba.Widget] = []
ba.buttonwidget(
resetbtn = ba.buttonwidget(
parent=self.root_widget,
label=ba.Lstr(resource='settingsWindowAdvanced.resetText'),
autoselect=True,
@ -77,20 +78,27 @@ class TeamNamesColorsWindow(popup.PopupWindow):
description=ba.Lstr(resource='nameText'),
editable=True,
padding=4))
ba.buttonwidget(parent=self.root_widget,
label=ba.Lstr(resource='cancelText'),
autoselect=True,
on_activate_call=self._on_cancel_press,
size=(150, 50),
position=(self._width * 0.5 - 200, 20))
ba.buttonwidget(parent=self.root_widget,
label=ba.Lstr(resource='saveText'),
autoselect=True,
on_activate_call=self._save,
size=(150, 50),
position=(self._width * 0.5 + 50, 20))
ba.widget(edit=self._color_text_fields[0],
down_widget=self._color_text_fields[1])
ba.widget(edit=self._color_text_fields[1],
up_widget=self._color_text_fields[0])
ba.widget(edit=self._color_text_fields[0], up_widget=resetbtn)
cancelbtn = ba.buttonwidget(parent=self.root_widget,
label=ba.Lstr(resource='cancelText'),
autoselect=True,
on_activate_call=self._on_cancel_press,
size=(150, 50),
position=(self._width * 0.5 - 200, 20))
savebtn = ba.buttonwidget(parent=self.root_widget,
label=ba.Lstr(resource='saveText'),
autoselect=True,
on_activate_call=self._save,
size=(150, 50),
position=(self._width * 0.5 + 50, 20))
ba.containerwidget(edit=self.root_widget,
selected_child=self._color_buttons[0])
ba.widget(edit=savebtn, left_widget=cancelbtn)
self._update()
def _color_click(self, i: int) -> None:

View File

@ -496,9 +496,7 @@ class WatchWindow(ba.Window):
sel_name = 'TabContainer'
else:
raise ValueError(f'unrecognized selection {sel}')
ba.app.ui.window_states[self.__class__.__name__] = {
'sel_name': sel_name
}
ba.app.ui.window_states[type(self)] = {'sel_name': sel_name}
except Exception:
ba.print_exception(f'Error saving state for {self}.')
@ -506,7 +504,7 @@ class WatchWindow(ba.Window):
from efro.util import enum_by_value
try:
sel: Optional[ba.Widget]
sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
sel_name = ba.app.ui.window_states.get(type(self),
{}).get('sel_name')
assert isinstance(sel_name, (str, type(None)))
try:

View File

@ -17,6 +17,7 @@
<w>aclass's</w>
<w>activityplayer</w>
<w>addcall</w>
<w>addchars</w>
<w>addrs</w>
<w>adjoint</w>
<w>adminset</w>
@ -47,6 +48,7 @@
<w>appname</w>
<w>appnameupper</w>
<w>appstate</w>
<w>argsjoined</w>
<w>asci</w>
<w>assigninput</w>
<w>athome</w>
@ -54,6 +56,7 @@
<w>audiocache</w>
<w>automagically</w>
<w>autoselect</w>
<w>availmins</w>
<w>avel</w>
<w>avels</w>
<w>axismotion</w>
@ -114,6 +117,7 @@
<w>bsstd</w>
<w>bstat</w>
<w>bsuuid</w>
<w>btnlabel</w>
<w>bufs</w>
<w>buildconfig</w>
<w>buildnumber</w>
@ -131,8 +135,10 @@
<w>camalign</w>
<w>camelback</w>
<w>camerashake</w>
<w>cancelbtn</w>
<w>capitan</w>
<w>cargs</w>
<w>cbtnoffs</w>
<w>ccdd</w>
<w>ccontext</w>
<w>ccylinder</w>
@ -177,6 +183,7 @@
<w>cpuid</w>
<w>crashenv</w>
<w>crashlytics</w>
<w>cresult</w>
<w>crom</w>
<w>crosswire</w>
<w>crvel</w>
@ -329,6 +336,7 @@
<w>fread</w>
<w>freeform</w>
<w>freeifaddrs</w>
<w>freemins</w>
<w>freqs</w>
<w>froemling</w>
<w>fromini</w>
@ -411,6 +419,7 @@
<w>hostactivity</w>
<w>hostcmd</w>
<w>hostinfo</w>
<w>hostingstate</w>
<w>hotkeys</w>
<w>hotplug</w>
<w>hscrollwidget</w>
@ -491,6 +500,7 @@
<w>linearsize</w>
<w>listobj</w>
<w>llock</w>
<w>lockpath</w>
<w>lockstr</w>
<w>locktype</w>
<w>logmsg</w>
@ -596,6 +606,7 @@
<w>nonlint</w>
<w>noone</w>
<w>nothin</w>
<w>nowtickets</w>
<w>nptr</w>
<w>nsize</w>
<w>ntoa</w>
@ -613,6 +624,8 @@
<w>obvs</w>
<w>oculus</w>
<w>oenval</w>
<w>offsx</w>
<w>offsy</w>
<w>oiffsss</w>
<w>oldname</w>
<w>oooo</w>
@ -668,6 +681,7 @@
<w>positivez</w>
<w>postinit</w>
<w>powerup</w>
<w>pptabcom</w>
<w>precalc</w>
<w>predeclare</w>
<w>prefs</w>
@ -679,9 +693,11 @@
<w>printnodes</w>
<w>printobjects</w>
<w>priv</w>
<w>privatetab</w>
<w>profilers</w>
<w>prog</w>
<w>proj</w>
<w>projdir</w>
<w>prolly</w>
<w>psmx</w>
<w>pspec</w>
@ -739,6 +755,7 @@
<w>reprfunc</w>
<w>rerase</w>
<w>resends</w>
<w>resetbtn</w>
<w>resetinput</w>
<w>resync</w>
<w>retrysecs</w>
@ -759,6 +776,7 @@
<w>safecolor</w>
<w>samsung</w>
<w>sapspace</w>
<w>savebtn</w>
<w>savebutton</w>
<w>scancode</w>
<w>scenetime</w>
@ -767,6 +785,10 @@
<w>sdkcheck</w>
<w>sdl's</w>
<w>sdlk</w>
<w>selchild</w>
<w>selindex</w>
<w>selwidget</w>
<w>selwidgets</w>
<w>seqlen</w>
<w>seqtype</w>
<w>seqtypestr</w>
@ -884,6 +906,7 @@
<w>theres</w>
<w>threadname</w>
<w>threadtype</w>
<w>ticon</w>
<w>tiltage</w>
<w>timedisplay</w>
<w>timeformat</w>

View File

@ -30,7 +30,8 @@
"requests",
"typing_extensions",
"cpplint",
"ansiwrap"
"ansiwrap",
"filelock"
],
"python_paths": [
"assets/src/ba_data/python",

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2021-01-15 for Ballistica version 1.5.30 build 20267</em></h4>
<h4><em>last updated on 2021-01-26 for Ballistica version 1.6.0 build 20278</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr>
@ -85,6 +85,10 @@
<h4><a name="function_category_General_Utility_Functions">General Utility Functions</a></h4>
<ul>
<li><a href="#function_ba_charstr">ba.charstr()</a></li>
<li><a href="#function_ba_clipboard_get_text">ba.clipboard_get_text()</a></li>
<li><a href="#function_ba_clipboard_has_text">ba.clipboard_has_text()</a></li>
<li><a href="#function_ba_clipboard_is_supported">ba.clipboard_is_supported()</a></li>
<li><a href="#function_ba_clipboard_set_text">ba.clipboard_set_text()</a></li>
<li><a href="#function_ba_do_once">ba.do_once()</a></li>
<li><a href="#function_ba_garbage_collect">ba.garbage_collect()</a></li>
<li><a href="#function_ba_getclass">ba.getclass()</a></li>
@ -395,7 +399,7 @@ actually award achievements.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_Activity">ba.Activity</a></strong></h3>
<p>Inherits from: <a href="#class_ba_DependencyComponent">ba.DependencyComponent</a>, <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Inherits from: <a href="#class_ba_DependencyComponent">ba.DependencyComponent</a>, <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>Units of execution wrangled by a <a href="#class_ba_Session">ba.Session</a>.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
@ -659,7 +663,7 @@ is a convenient way to access this same functionality.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_ActivityNotFoundError">ba.ActivityNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected <a href="#class_ba_Activity">ba.Activity</a> does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -784,7 +788,7 @@ even if myactor is set to None.</p>
<p>Return the <a href="#class_ba_Activity">ba.Activity</a> this Actor is associated with.</p>
<p>If the Activity no longer exists, raises a <a href="#class_ba_ActivityNotFoundError">ba.ActivityNotFoundError</a>
or returns None depending on whether 'doraise' is set.</p>
or returns None depending on whether 'doraise' is True.</p>
</dd>
<dt><h4><a name="method_ba_Actor__handlemessage">handlemessage()</a></dt></h4><dd>
@ -823,7 +827,7 @@ likely result in errors.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_ActorNotFoundError">ba.ActorNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected <a href="#class_ba_Actor">ba.Actor</a> does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -1010,7 +1014,7 @@ to resume.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_AppConfig">ba.AppConfig</a></strong></h3>
<p>Inherits from: dict</p>
<p>Inherits from: builtins.dict</p>
<p>A special dict that holds the game's persistent configuration values.</p>
<p>Category: <a href="#class_category_App_Classes">App Classes</a></p>
@ -1023,7 +1027,7 @@ to resume.</p>
<p> AppConfig data is stored as json on disk on so make sure to only place
json-friendly values in it (dict, list, str, float, int, bool).
Be aware that tuples will be quietly converted to lists.
Be aware that tuples will be quietly converted to lists when stored.
</p>
<h3>Methods Defined or Overridden:</h3>
@ -1596,7 +1600,7 @@ start_long_action(callback_when_done=<a href="#class_ba_ContextCall">ba.ContextC
<hr>
<h2><strong><a name="class_ba_ContextError">ba.ContextError</a></strong></h3>
<p>Inherits from: Exception, BaseException</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when a call is made in an invalid context.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a></p>
@ -1606,10 +1610,10 @@ start_long_action(callback_when_done=<a href="#class_ba_ContextCall">ba.ContextC
</p>
<h3>Methods:</h3>
<p>&lt;all methods inherited from <a href="#class_builtins_Exception">builtins.Exception</a>&gt;</p>
<p>&lt;all methods inherited from <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>&gt;</p>
<hr>
<h2><strong><a name="class_ba_CoopGameActivity">ba.CoopGameActivity</a></strong></h3>
<p>Inherits from: <a href="#class_ba_GameActivity">ba.GameActivity</a>, <a href="#class_ba_Activity">ba.Activity</a>, <a href="#class_ba_DependencyComponent">ba.DependencyComponent</a>, <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Inherits from: <a href="#class_ba_GameActivity">ba.GameActivity</a>, <a href="#class_ba_Activity">ba.Activity</a>, <a href="#class_ba_DependencyComponent">ba.DependencyComponent</a>, <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>Base class for cooperative-mode games.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a>
@ -1841,7 +1845,7 @@ the data object is requested and when it's value is accessed.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_DeathType">ba.DeathType</a></strong></h3>
<p>Inherits from: enum.Enum</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></p>
<p>A reason for a death.</p>
<p>Category: <a href="#class_category_Enums">Enums</a>
@ -1858,7 +1862,7 @@ the data object is requested and when it's value is accessed.</p>
</ul>
<hr>
<h2><strong><a name="class_ba_DelegateNotFoundError">ba.DelegateNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected delegate object does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -1868,7 +1872,7 @@ the data object is requested and when it's value is accessed.</p>
<p>&lt;all methods inherited from <a href="#class_ba_NotFoundError">ba.NotFoundError</a>&gt;</p>
<hr>
<h2><strong><a name="class_ba_Dependency">ba.Dependency</a></strong></h3>
<p>Inherits from: <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Inherits from: <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>A dependency on a DependencyComponent (with an optional config).</p>
<p>Category: <a href="#class_category_Dependency_Classes">Dependency Classes</a></p>
@ -1941,7 +1945,7 @@ on the dep config value. (for instance a map required by a game type)</p>
</dl>
<hr>
<h2><strong><a name="class_ba_DependencyError">ba.DependencyError</a></strong></h3>
<p>Inherits from: Exception, BaseException</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when one or more <a href="#class_ba_Dependency">ba.Dependency</a> items are missing.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a></p>
@ -1968,7 +1972,7 @@ on the dep config value. (for instance a map required by a game type)</p>
</dl>
<hr>
<h2><strong><a name="class_ba_DependencySet">ba.DependencySet</a></strong></h3>
<p>Inherits from: <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Inherits from: <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>Set of resolved dependencies and their associated data.</p>
<p>Category: <a href="#class_category_Dependency_Classes">Dependency Classes</a></p>
@ -2129,13 +2133,13 @@ its time with lingering corpses, sound effects, etc.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_EmptyPlayer">ba.EmptyPlayer</a></strong></h3>
<p>Inherits from: <a href="#class_ba_Player">ba.Player</a>, <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Inherits from: <a href="#class_ba_Player">ba.Player</a>, <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>An empty player for use by Activities that don't need to define one.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
<p> <a href="#class_ba_Player">ba.Player</a> and <a href="#class_ba_Team">ba.Team</a> are 'Generic' types, and so passing them as
type arguments when defining a <a href="#class_ba_Activity">ba.Activity</a> reduces type safety.
<p> <a href="#class_ba_Player">ba.Player</a> and <a href="#class_ba_Team">ba.Team</a> are 'Generic' types, and so passing those top level
classes as type arguments when defining a <a href="#class_ba_Activity">ba.Activity</a> reduces type safety.
For example, activity.teams[0].player will have type 'Any' in that case.
For that reason, it is better to pass EmptyPlayer and EmptyTeam when
defining a <a href="#class_ba_Activity">ba.Activity</a> that does not need custom types of its own.</p>
@ -2192,13 +2196,13 @@ its time with lingering corpses, sound effects, etc.</p>
<p>&lt;all methods inherited from <a href="#class_ba_Player">ba.Player</a>&gt;</p>
<hr>
<h2><strong><a name="class_ba_EmptyTeam">ba.EmptyTeam</a></strong></h3>
<p>Inherits from: <a href="#class_ba_Team">ba.Team</a>, <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Inherits from: <a href="#class_ba_Team">ba.Team</a>, <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>An empty player for use by Activities that don't need to define one.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
<p> <a href="#class_ba_Player">ba.Player</a> and <a href="#class_ba_Team">ba.Team</a> are 'Generic' types, and so passing them as
type arguments when defining a <a href="#class_ba_Activity">ba.Activity</a> reduces type safety.
<p> <a href="#class_ba_Player">ba.Player</a> and <a href="#class_ba_Team">ba.Team</a> are 'Generic' types, and so passing those top level
classes as type arguments when defining a <a href="#class_ba_Activity">ba.Activity</a> reduces type safety.
For example, activity.teams[0].player will have type 'Any' in that case.
For that reason, it is better to pass EmptyPlayer and EmptyTeam when
defining a <a href="#class_ba_Activity">ba.Activity</a> that does not need custom types of its own.</p>
@ -2234,7 +2238,7 @@ its time with lingering corpses, sound effects, etc.</p>
<p>&lt;all methods inherited from <a href="#class_ba_Team">ba.Team</a>&gt;</p>
<hr>
<h2><strong><a name="class_ba_Existable">ba.Existable</a></strong></h3>
<p>Inherits from: <a href="#class_typing_Protocol">typing.Protocol</a>, <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Inherits from: <a href="https://docs.python.org/3/library/typing.html#typing.Protocol">typing.Protocol</a>, <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>A Protocol for objects supporting an exists() method.</p>
<p>Category: <a href="#class_category_Protocols">Protocols</a>
@ -2337,8 +2341,8 @@ its time with lingering corpses, sound effects, etc.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_GameActivity">ba.GameActivity</a></strong></h3>
<p>Inherits from: <a href="#class_ba_Activity">ba.Activity</a>, <a href="#class_ba_DependencyComponent">ba.DependencyComponent</a>, <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Common base class for all game ba.Activities.</p>
<p>Inherits from: <a href="#class_ba_Activity">ba.Activity</a>, <a href="#class_ba_DependencyComponent">ba.DependencyComponent</a>, <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>Common base class for all game <a href="#class_ba_Activity">ba.Activities</a>.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a>
</p>
@ -2990,7 +2994,7 @@ prefs, etc.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_InputDeviceNotFoundError">ba.InputDeviceNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected <a href="#class_ba_InputDevice">ba.InputDevice</a> does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -3000,7 +3004,7 @@ prefs, etc.</p>
<p>&lt;all methods inherited from <a href="#class_ba_NotFoundError">ba.NotFoundError</a>&gt;</p>
<hr>
<h2><strong><a name="class_ba_InputType">ba.InputType</a></strong></h3>
<p>Inherits from: enum.Enum</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></p>
<p>Types of input a controller can send to the game.</p>
<p>Category: <a href="#class_category_Enums">Enums</a></p>
@ -4053,7 +4057,7 @@ signify that the default soundtrack should be used..</p>
</dl>
<hr>
<h2><strong><a name="class_ba_MusicPlayMode">ba.MusicPlayMode</a></strong></h3>
<p>Inherits from: enum.Enum</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></p>
<p>Influences behavior when playing music.</p>
<p>Category: <a href="#class_category_Enums">Enums</a>
@ -4156,7 +4160,7 @@ account what is supported locally.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_MusicType">ba.MusicType</a></strong></h3>
<p>Inherits from: enum.Enum</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></p>
<p>Types of music available to play in-game.</p>
<p>Category: <a href="#class_category_Enums">Enums</a></p>
@ -4368,7 +4372,7 @@ even if myactor is set to None.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_NodeNotFoundError">ba.NodeNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected <a href="#class_ba_Node">ba.Node</a> does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -4378,14 +4382,14 @@ even if myactor is set to None.</p>
<p>&lt;all methods inherited from <a href="#class_ba_NotFoundError">ba.NotFoundError</a>&gt;</p>
<hr>
<h2><strong><a name="class_ba_NotFoundError">ba.NotFoundError</a></strong></h3>
<p>Inherits from: Exception, BaseException</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when a referenced object does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
</p>
<h3>Methods:</h3>
<p>&lt;all methods inherited from <a href="#class_builtins_Exception">builtins.Exception</a>&gt;</p>
<p>&lt;all methods inherited from <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>&gt;</p>
<hr>
<h2><strong><a name="class_ba_OutOfBoundsMessage">ba.OutOfBoundsMessage</a></strong></h3>
<p><em>&lt;top level class&gt;</em>
@ -4404,7 +4408,7 @@ even if myactor is set to None.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_Permission">ba.Permission</a></strong></h3>
<p>Inherits from: enum.Enum</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></p>
<p>Permissions that can be requested from the OS.</p>
<p>Category: <a href="#class_category_Enums">Enums</a>
@ -4462,7 +4466,7 @@ even if myactor is set to None.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_Player">ba.Player</a></strong></h3>
<p>Inherits from: <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Inherits from: <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>A player in a specific <a href="#class_ba_Activity">ba.Activity</a>.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
@ -4657,7 +4661,7 @@ the type-checker properly identifies the returned value as one.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_PlayerNotFoundError">ba.PlayerNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected <a href="#class_ba_Player">ba.Player</a> does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -4944,7 +4948,7 @@ change this. Defaults to an empty string.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_ScoreType">ba.ScoreType</a></strong></h3>
<p>Inherits from: enum.Enum</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></p>
<p>Type of scores.</p>
<p>Category: <a href="#class_category_Enums">Enums</a>
@ -5010,7 +5014,7 @@ Pass 0 or a negative number for no ban time.</p>
<h2><strong><a name="class_ba_Session">ba.Session</a></strong></h3>
<p><em>&lt;top level class&gt;</em>
</p>
<p>Defines a high level series of activities with a common purpose.</p>
<p>Defines a high level series of <a href="#class_ba_Activity">ba.Activities</a> with a common purpose.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
@ -5201,7 +5205,7 @@ session.setactivity(foo) and then <a href="#function_ba_newnode">ba.newnode</a>(
</dl>
<hr>
<h2><strong><a name="class_ba_SessionNotFoundError">ba.SessionNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected <a href="#class_ba_Session">ba.Session</a> does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -5345,7 +5349,7 @@ other players.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_SessionPlayerNotFoundError">ba.SessionPlayerNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected <a href="#class_ba_SessionPlayer">ba.SessionPlayer</a> does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -5411,7 +5415,7 @@ of the session.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_SessionTeamNotFoundError">ba.SessionTeamNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected <a href="#class_ba_SessionTeam">ba.SessionTeam</a> does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -5463,7 +5467,7 @@ of the session.</p>
<hr>
<h2><strong><a name="class_ba_SpecialChar">ba.SpecialChar</a></strong></h3>
<p>Inherits from: enum.Enum</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></p>
<p>Special characters the game can print.</p>
<p>Category: <a href="#class_category_Enums">Enums</a>
@ -5680,7 +5684,7 @@ of the session.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_Team">ba.Team</a></strong></h3>
<p>Inherits from: <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Inherits from: <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>A team in a specific <a href="#class_ba_Activity">ba.Activity</a>.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
@ -5729,7 +5733,7 @@ of the session.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_TeamGameActivity">ba.TeamGameActivity</a></strong></h3>
<p>Inherits from: <a href="#class_ba_GameActivity">ba.GameActivity</a>, <a href="#class_ba_Activity">ba.Activity</a>, <a href="#class_ba_DependencyComponent">ba.DependencyComponent</a>, <a href="#class_typing_Generic">typing.Generic</a></p>
<p>Inherits from: <a href="#class_ba_GameActivity">ba.GameActivity</a>, <a href="#class_ba_Activity">ba.Activity</a>, <a href="#class_ba_DependencyComponent">ba.DependencyComponent</a>, <a href="https://docs.python.org/3/library/typing.html#typing.Generic">typing.Generic</a></p>
<p>Base class for teams and free-for-all mode games.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
@ -5859,7 +5863,7 @@ False otherwise.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_TeamNotFoundError">ba.TeamNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected <a href="#class_ba_Team">ba.Team</a> does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -5895,7 +5899,7 @@ False otherwise.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_TimeFormat">ba.TimeFormat</a></strong></h3>
<p>Inherits from: enum.Enum</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></p>
<p>Specifies the format time values are provided in.</p>
<p>Category: <a href="#class_category_Enums">Enums</a>
@ -5927,7 +5931,7 @@ you should use the <a href="#function_ba_timer">ba.timer</a>() function instead.
<p>time: length of time (in seconds by default) that the timer will wait
before firing. Note that the actual delay experienced may vary
depending on the timetype. (see below)</p>
depending on the timetype. (see below)</p>
<p>call: A callable Python object. Note that the timer will retain a
strong reference to the callable for as long as it exists, so you
@ -5937,28 +5941,11 @@ desired.</p>
<p>repeat: if True, the timer will fire repeatedly, with each successive
firing having the same delay as the first.</p>
<p>timetype can be either 'sim', 'base', or 'real'. It defaults to
'sim'. Types are explained below:</p>
<p>timetype: A <a href="#class_ba_TimeType">ba.TimeType</a> value determining which timeline the timer is
placed onto.</p>
<p>'sim' time maps to local simulation time in <a href="#class_ba_Activity">ba.Activity</a> or <a href="#class_ba_Session">ba.Session</a>
Contexts. This means that it may progress slower in slow-motion play
modes, stop when the game is paused, etc. This time type is not
available in UI contexts.</p>
<p>'base' time is also linked to gameplay in <a href="#class_ba_Activity">ba.Activity</a> or <a href="#class_ba_Session">ba.Session</a>
Contexts, but it progresses at a constant rate regardless of
slow-motion states or pausing. It can, however, slow down or stop
in certain cases such as network outages or game slowdowns due to
cpu load. Like 'sim' time, this is unavailable in UI contexts.</p>
<p>'real' time always maps to actual clock time with a bit of filtering
added, regardless of Context. (the filtering prevents it from going
backwards or jumping forward by large amounts due to the app being
backgrounded, system time changing, etc.)
Real time timers are currently only available in the UI context.</p>
<p>the 'timeformat' arg defaults to SECONDS but can also be MILLISECONDS
if you want to pass time as milliseconds.</p>
<p>timeformat: A <a href="#class_ba_TimeFormat">ba.TimeFormat</a> value determining how the passed time is
interpreted.</p>
<pre><span><em><small># Example: use a Timer object to print repeatedly for a few seconds:</small></em></span>
def say_it():
@ -5966,14 +5953,14 @@ def say_it():
def stop_saying_it():
self.t = None
<a href="#function_ba_screenmessage">ba.screenmessage</a>('MUSHROOM MUSHROOM!')
<span><em><small># create our timer; it will run as long as we hold self.t</small></em></span>
<span><em><small># Create our timer; it will run as long as we have the self.t ref.</small></em></span>
self.t = <a href="#class_ba_Timer">ba.Timer</a>(0.3, say_it, repeat=True)
<span><em><small># now fire off a one-shot timer to kill it</small></em></span>
<span><em><small># Now fire off a one-shot timer to kill it.</small></em></span>
<a href="#function_ba_timer">ba.timer</a>(3.89, stop_saying_it)</pre>
<hr>
<h2><strong><a name="class_ba_TimeType">ba.TimeType</a></strong></h3>
<p>Inherits from: enum.Enum</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></p>
<p>Specifies the type of time for various operations to target/use.</p>
<p>Category: <a href="#class_category_Enums">Enums</a></p>
@ -6001,7 +5988,7 @@ self.t = <a href="#class_ba_Timer">ba.Timer</a>(0.3, say_it, repeat=True)
<h2><strong><a name="class_ba_UIController">ba.UIController</a></strong></h3>
<p><em>&lt;top level class&gt;</em>
</p>
<p>Wrangles UILocations.</p>
<p>Wrangles ba.UILocations.</p>
<p>Category: <a href="#class_category_User_Interface_Classes">User Interface Classes</a>
</p>
@ -6022,7 +6009,7 @@ self.t = <a href="#class_ba_Timer">ba.Timer</a>(0.3, say_it, repeat=True)
</dl>
<hr>
<h2><strong><a name="class_ba_UIScale">ba.UIScale</a></strong></h3>
<p>Inherits from: enum.Enum</p>
<p>Inherits from: <a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></p>
<p>The overall scale the UI is being rendered for. Note that this is
independent of pixel resolution. For example, a phone and a desktop PC
might render the game at similar pixel resolutions but the size they
@ -6306,7 +6293,7 @@ widgets.</p>
</dl>
<hr>
<h2><strong><a name="class_ba_WidgetNotFoundError">ba.WidgetNotFoundError</a></strong></h3>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, Exception, BaseException</p>
<p>Inherits from: <a href="#class_ba_NotFoundError">ba.NotFoundError</a>, <a href="https://docs.python.org/3/library/exceptions.html#Exception">Exception</a>, <a href="https://docs.python.org/3/library/exceptions.html#BaseException">BaseException</a></p>
<p>Exception raised when an expected <a href="#class_ba_Widget">ba.Widget</a> does not exist.</p>
<p>Category: <a href="#class_category_Exception_Classes">Exception Classes</a>
@ -6473,6 +6460,50 @@ them elsewhere will be meaningless.</p>
a new one is created and returned. Arguments that are not set to None
are applied to the Widget.</p>
<hr>
<h2><strong><a name="function_ba_clipboard_get_text">ba.clipboard_get_text()</a></strong></h3>
<p><span>clipboard_get_text() -&gt; str</span></p>
<p>Return text currently on the system clipboard.</p>
<p>Category: <a href="#function_category_General_Utility_Functions">General Utility Functions</a></p>
<p>Ensure that <a href="#function_ba_clipboard_has_text">ba.clipboard_has_text</a>() returns True before calling
this function.</p>
<hr>
<h2><strong><a name="function_ba_clipboard_has_text">ba.clipboard_has_text()</a></strong></h3>
<p><span>clipboard_has_text() -&gt; bool</span></p>
<p>Return whether there is currently text on the clipboard.</p>
<p>Category: <a href="#function_category_General_Utility_Functions">General Utility Functions</a></p>
<p>This will return False if no system clipboard is available; no need
to call ba.clipboard_available() separately.</p>
<hr>
<h2><strong><a name="function_ba_clipboard_is_supported">ba.clipboard_is_supported()</a></strong></h3>
<p><span>clipboard_is_supported() -&gt; bool</span></p>
<p>Return whether this platform supports clipboard operations at all.</p>
<p>Category: <a href="#function_category_General_Utility_Functions">General Utility Functions</a></p>
<p>If this returns False, UIs should not show 'copy to clipboard'
buttons, etc.</p>
<hr>
<h2><strong><a name="function_ba_clipboard_set_text">ba.clipboard_set_text()</a></strong></h3>
<p><span>clipboard_set_text(value: str) -&gt; None</span></p>
<p>Copy a string to the system clipboard.</p>
<p>Category: <a href="#function_category_General_Utility_Functions">General Utility Functions</a></p>
<p>Ensure that ba.clipboard_available() returns True before adding
buttons/etc. that make use of this functionality.</p>
<hr>
<h2><strong><a name="function_ba_columnwidget">ba.columnwidget()</a></strong></h3>
<p><span>columnwidget(edit: <a href="#class_ba_Widget">ba.Widget</a> = None,
@ -6582,17 +6613,19 @@ settings, exiting element counts, or other factors.</p>
<h2><strong><a name="function_ba_existing">ba.existing()</a></strong></h3>
<p><span>existing(obj: Optional[ExistableType]) -&gt; Optional[ExistableType]</span></p>
<p>Convert invalid references to None for any <a href="#class_ba_Existable">ba.Existable</a> type.</p>
<p>Convert invalid references to None for any <a href="#class_ba_Existable">ba.Existable</a> object.</p>
<p>Category: <a href="#function_category_Gameplay_Functions">Gameplay Functions</a></p>
<p>To best support type checking, it is important that invalid references
not be passed around and instead get converted to values of None.
That way the type checker can properly flag attempts to pass dead
objects into functions expecting only live ones, etc.
This call can be used on any 'existable' object (one with an exists()
method) and will convert it to a None value if it does not exist.
For more info, see notes on 'existables' here:
objects (Optional[FooType]) into functions expecting only live ones
(FooType), etc. This call can be used on any 'existable' object
(one with an exists() method) and will convert it to a None value
if it does not exist.</p>
<p>For more info, see notes on 'existables' here:
https://ballistica.net/wiki/Coding-Style-Guide</p>
<hr>
@ -6989,14 +7022,14 @@ app running.</p>
<hr>
<h2><strong><a name="function_ba_rowwidget">ba.rowwidget()</a></strong></h3>
<p><span>rowwidget(edit: Widget = None, parent: Widget = None,
<p><span>rowwidget(edit: <a href="#class_ba_Widget">ba.Widget</a> = None, parent: <a href="#class_ba_Widget">ba.Widget</a> = None,
size: Sequence[float] = None,
position: Sequence[float] = None,
background: bool = None, selected_child: Widget = None,
visible_child: Widget = None,
background: bool = None, selected_child: <a href="#class_ba_Widget">ba.Widget</a> = None,
visible_child: <a href="#class_ba_Widget">ba.Widget</a> = None,
claims_left_right: bool = None,
claims_tab: bool = None,
selection_loops_to_parent: bool = None) -&gt; Widget</span></p>
selection_loops_to_parent: bool = None) -&gt; <a href="#class_ba_Widget">ba.Widget</a></span></p>
<p>Create or edit a row widget.</p>
@ -7076,9 +7109,9 @@ are applied to the Widget.</p>
<hr>
<h2><strong><a name="function_ba_setmusic">ba.setmusic()</a></strong></h3>
<p><span>setmusic(musictype: Optional[MusicType], continuous: bool = False) -&gt; None</span></p>
<p><span>setmusic(musictype: Optional[<a href="#class_ba_MusicType">ba.MusicType</a>], continuous: bool = False) -&gt; None</span></p>
<p>Tell the game to play (or stop playing) a certain type of music.</p>
<p>Set the app to play (or stop playing) a certain type of music.</p>
<p>Category: <a href="#function_category_Gameplay_Functions">Gameplay Functions</a></p>
@ -7103,15 +7136,17 @@ playing, the playing track will not be restarted.</p>
<h2><strong><a name="function_ba_storagename">ba.storagename()</a></strong></h3>
<p><span>storagename(suffix: str = None) -&gt; str</span></p>
<p>Generate a (hopefully) unique name for storing things in public places.</p>
<p>Generate a unique name for storing class data in shared places.</p>
<p>Category: <a href="#function_category_General_Utility_Functions">General Utility Functions</a></p>
<p>This consists of a leading underscore, the module path at the
call site with dots replaced by underscores, the class name, and
the provided suffix. When storing data in public places such as
'customdata' dicts, this minimizes the chance of collisions if a
module or class is duplicated or renamed.</p>
call site with dots replaced by underscores, the containing class's
qualified name, and the provided suffix. When storing data in public
places such as 'customdata' dicts, this minimizes the chance of
collisions with other similarly named classes.</p>
<p>Note that this will function even if called in the class definition.</p>
<pre><span><em><small># Example: generate a unique name for storage purposes:</small></em></span>
class MyThingie:</pre>
@ -7119,22 +7154,22 @@ class MyThingie:</pre>
<pre><span><em><small> # This will give something like '_mymodule_submodule_mythingie_data'.</small></em></span>
_STORENAME = <a href="#function_ba_storagename">ba.storagename</a>('data')</pre>
<p> def __init__(self, activity):
# Store some data in the Activity we were passed
activity.customdata[self._STORENAME] = {}</p>
<pre><span><em><small> # Use that name to store some data in the Activity we were passed.</small></em></span>
def __init__(self, activity):
activity.customdata[self._STORENAME] = {}</pre>
<hr>
<h2><strong><a name="function_ba_textwidget">ba.textwidget()</a></strong></h3>
<p><span>textwidget(edit: Widget = None, parent: Widget = None,
<p><span>textwidget(edit: <a href="#class_ba_Widget">ba.Widget</a> = None, parent: <a href="#class_ba_Widget">ba.Widget</a> = None,
size: Sequence[float] = None, position: Sequence[float] = None,
text: Union[str, <a href="#class_ba_Lstr">ba.Lstr</a>] = None, v_align: str = None,
h_align: str = None, editable: bool = None, padding: float = None,
on_return_press_call: Callable[[], None] = None,
on_activate_call: Callable[[], None] = None,
selectable: bool = None, query: Widget = None, max_chars: int = None,
selectable: bool = None, query: <a href="#class_ba_Widget">ba.Widget</a> = None, max_chars: int = None,
color: Sequence[float] = None, click_activate: bool = None,
on_select_call: Callable[[], None] = None,
always_highlight: bool = None, draw_controller: Widget = None,
always_highlight: bool = None, draw_controller: <a href="#class_ba_Widget">ba.Widget</a> = None,
scale: float = None, corner_scale: float = None,
description: Union[str, <a href="#class_ba_Lstr">ba.Lstr</a>] = None,
transition_delay: float = None, maxwidth: float = None,

View File

@ -21,8 +21,8 @@
namespace ballistica {
// These are set automatically via script; don't change here.
const int kAppBuildNumber = 20268;
const char* kAppVersion = "1.5.30";
const int kAppBuildNumber = 20279;
const char* kAppVersion = "1.6.0";
// Our standalone globals.
// These are separated out for easy access.

View File

@ -263,9 +263,6 @@ class Graphics {
internal_components_inited_ = val;
}
auto set_gyro_vals(const Vector3f& vals) -> void { gyro_vals_ = vals; }
// auto draw_overlay_bounds() const -> bool { return draw_overlay_bounds_; }
// auto set_draw_overlay_bounds(bool val) -> void { draw_overlay_bounds_ =
// val; }
auto show_net_info() const -> bool { return show_net_info_; }
auto set_show_net_info(bool val) -> void { show_net_info_ = val; }
auto debug_graph_1() const -> NetGraph* { return debug_graph_1_.get(); }

View File

@ -286,8 +286,8 @@ void GraphicsServer::SetScreen(bool fullscreen, int width, int height,
// we request fullscreen-windows for full-screen situations and that's it.
// (otherwise we may wind up with huge windows due to passing in desktop
// resolutions and retina wonkiness)
width = 800;
height = 600;
width = static_cast<int>(kBaseVirtualResX * 0.8f);
height = static_cast<int>(kBaseVirtualResY * 0.8f);
// We should never have to recreate the context after the initial time on
// our modern builds.

View File

@ -1209,13 +1209,24 @@ void Input::HandleKeyPress(const SDL_Keysym* keysym) {
if (!repeat_press && keysym->sym == SDLK_q
&& ((keysym->mod & KMOD_CTRL) || (keysym->mod & KMOD_GUI))) { // NOLINT
g_game->PushConfirmQuitCall();
return;
}
}
// Let the console intercept stuff if it wants at this point.
if (g_app_globals->console != nullptr
&& g_app_globals->console->HandleKeyPress(keysym)) {
return;
}
// Ctrl-V or Cmd-V sends paste commands to any interested text fields.
// Command-Q or Control-Q quits.
if (!repeat_press && keysym->sym == SDLK_v
&& ((keysym->mod & KMOD_CTRL) || (keysym->mod & KMOD_GUI))) { // NOLINT
g_ui->SendWidgetMessage(WidgetMessage(WidgetMessage::Type::kPaste));
return;
}
bool handled = false;
// None of the following stuff accepts key repeats.

View File

@ -1361,4 +1361,88 @@ auto Platform::GetCurrentSeconds() -> int64_t {
.count();
}
auto Platform::ClipboardIsSupported() -> bool {
// We only call our actual virtual function once.
if (!have_clipboard_is_supported_) {
clipboard_is_supported_ = DoClipboardIsSupported();
have_clipboard_is_supported_ = true;
}
return clipboard_is_supported_;
}
auto Platform::ClipboardHasText() -> bool {
// If subplatform says they don't support clipboards, don't even ask.
if (!ClipboardIsSupported()) {
return false;
}
return DoClipboardHasText();
}
auto Platform::ClipboardSetText(const std::string& text) -> void {
// If subplatform says they don't support clipboards, this is an error.
if (!ClipboardIsSupported()) {
throw Exception("ClipboardSetText called with no clipboard support.",
PyExcType::kRuntime);
}
DoClipboardSetText(text);
}
auto Platform::ClipboardGetText() -> std::string {
// If subplatform says they don't support clipboards, this is an error.
if (!ClipboardIsSupported()) {
throw Exception("ClipboardGetText called with no clipboard support.",
PyExcType::kRuntime);
}
return DoClipboardGetText();
}
auto Platform::DoClipboardIsSupported() -> bool {
// Go through SDL functionality on SDL based platforms;
// otherwise default to no clipboard.
#if BA_SDL2_BUILD && !BA_OSTYPE_IOS_TVOS
return true;
#else
return false;
#endif
}
auto Platform::DoClipboardHasText() -> bool {
// Go through SDL functionality on SDL based platforms;
// otherwise default to no clipboard.
#if BA_SDL2_BUILD && !BA_OSTYPE_IOS_TVOS
return SDL_HasClipboardText();
#else
// Shouldn't get here since we default to no clipboard support.
FatalError("Shouldn't get here.");
return false;
#endif
}
auto Platform::DoClipboardSetText(const std::string& text) -> void {
// Go through SDL functionality on SDL based platforms;
// otherwise default to no clipboard.
#if BA_SDL2_BUILD && !BA_OSTYPE_IOS_TVOS
SDL_SetClipboardText(text.c_str());
#else
// Shouldn't get here since we default to no clipboard support.
FatalError("Shouldn't get here.");
#endif
}
auto Platform::DoClipboardGetText() -> std::string {
// Go through SDL functionality on SDL based platforms;
// otherwise default to no clipboard.
#if BA_SDL2_BUILD && !BA_OSTYPE_IOS_TVOS
char* out = SDL_GetClipboardText();
if (out == nullptr) {
throw Exception("Error fetching clipboard contents.", PyExcType::kRuntime);
}
return out;
#else
// Shouldn't get here since we default to no clipboard support.
FatalError("Shouldn't get here.");
return "";
#endif
}
} // namespace ballistica

View File

@ -103,7 +103,25 @@ class Platform {
virtual auto GetCWD() -> std::string;
// Unlink a file.
virtual void Unlink(const char* path);
virtual auto Unlink(const char* path) -> void;
#pragma mark CLIPBOARD ---------------------------------------------------------
/// Return whether clipboard operations are supported at all.
/// This gets called when determining whether to display clipboard related
/// UI elements/etc.
auto ClipboardIsSupported() -> bool;
/// Return whether there is currently text on the clipboard.
auto ClipboardHasText() -> bool;
/// Set current clipboard text. Raises an Exception if clipboard is
/// unsupported.
auto ClipboardSetText(const std::string& text) -> void;
/// Return current text from the clipboard. Raises an Exception if
/// clipboard is unsupported or if there's no text on the clipboard.
auto ClipboardGetText() -> std::string;
#pragma mark PRINTING/LOGGING --------------------------------------------------
@ -493,6 +511,11 @@ class Platform {
// Generate a random UUID string.
virtual auto GenerateUUID() -> std::string;
virtual auto DoClipboardIsSupported() -> bool;
virtual auto DoClipboardHasText() -> bool;
virtual auto DoClipboardSetText(const std::string& text) -> void;
virtual auto DoClipboardGetText() -> std::string;
private:
int py_call_num_{};
bool using_custom_app_python_dir_{};
@ -500,6 +523,8 @@ class Platform {
bool have_has_touchscreen_value_{};
bool have_touchscreen_{};
bool is_tegra_k1_{};
bool have_clipboard_is_supported_{};
bool clipboard_is_supported_{};
millisecs_t starttime_{};
std::string device_uuid_;
bool have_device_uuid_{};

View File

@ -32,7 +32,7 @@ void PythonClassTimer::SetupType(PyTypeObject* obj) {
"you should use the ba.timer() function instead.\n"
"\n"
"time: length of time (in seconds by default) that the timer will wait\n"
"before firing. Note that the actual delay experienced may vary\n "
"before firing. Note that the actual delay experienced may vary\n"
"depending on the timetype. (see below)\n"
"\n"
"call: A callable Python object. Note that the timer will retain a\n"
@ -43,28 +43,11 @@ void PythonClassTimer::SetupType(PyTypeObject* obj) {
"repeat: if True, the timer will fire repeatedly, with each successive\n"
"firing having the same delay as the first.\n"
"\n"
"timetype can be either 'sim', 'base', or 'real'. It defaults to\n"
"'sim'. Types are explained below:\n"
"timetype: A ba.TimeType value determining which timeline the timer is\n"
"placed onto.\n"
"\n"
"'sim' time maps to local simulation time in ba.Activity or ba.Session\n"
"Contexts. This means that it may progress slower in slow-motion play\n"
"modes, stop when the game is paused, etc. This time type is not\n"
"available in UI contexts.\n"
"\n"
"'base' time is also linked to gameplay in ba.Activity or ba.Session\n"
"Contexts, but it progresses at a constant rate regardless of\n "
"slow-motion states or pausing. It can, however, slow down or stop\n"
"in certain cases such as network outages or game slowdowns due to\n"
"cpu load. Like 'sim' time, this is unavailable in UI contexts.\n"
"\n"
"'real' time always maps to actual clock time with a bit of filtering\n"
"added, regardless of Context. (the filtering prevents it from going\n"
"backwards or jumping forward by large amounts due to the app being\n"
"backgrounded, system time changing, etc.)\n"
"Real time timers are currently only available in the UI context.\n"
"\n"
"the 'timeformat' arg defaults to SECONDS but can also be MILLISECONDS\n"
"if you want to pass time as milliseconds.\n"
"timeformat: A ba.TimeFormat value determining how the passed time is\n"
"interpreted.\n"
"\n"
"# Example: use a Timer object to print repeatedly for a few seconds:\n"
"def say_it():\n"
@ -72,9 +55,9 @@ void PythonClassTimer::SetupType(PyTypeObject* obj) {
"def stop_saying_it():\n"
" self.t = None\n"
" ba.screenmessage('MUSHROOM MUSHROOM!')\n"
"# create our timer; it will run as long as we hold self.t\n"
"# Create our timer; it will run as long as we have the self.t ref.\n"
"self.t = ba.Timer(0.3, say_it, repeat=True)\n"
"# now fire off a one-shot timer to kill it\n"
"# Now fire off a one-shot timer to kill it.\n"
"ba.timer(3.89, stop_saying_it)";
obj->tp_new = tp_new;
obj->tp_dealloc = (destructor)tp_dealloc;

View File

@ -16,4 +16,5 @@ class PythonMethodsApp {
};
} // namespace ballistica
#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_APP_H_

View File

@ -16,4 +16,5 @@ class PythonMethodsGameplay {
};
} // namespace ballistica
#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GAMEPLAY_H_

View File

@ -16,4 +16,5 @@ class PythonMethodsGraphics {
};
} // namespace ballistica
#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_

View File

@ -32,6 +32,46 @@ namespace ballistica {
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
#pragma ide diagnostic ignored "RedundantCast"
auto PyClipboardIsSupported(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
if (g_platform->ClipboardIsSupported()) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
BA_PYTHON_CATCH;
}
auto PyClipboardHasText(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
if (g_platform->ClipboardHasText()) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
BA_PYTHON_CATCH;
}
auto PyClipboardSetText(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("clipboard_set_text");
const char* value;
static const char* kwlist[] = {"value", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "s",
const_cast<char**>(kwlist), &value)) {
return nullptr;
}
g_platform->ClipboardSetText(value);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PyClipboardGetText(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
return PyUnicode_FromString(g_platform->ClipboardGetText().c_str());
Py_RETURN_FALSE;
BA_PYTHON_CATCH;
}
auto PyIsRunningOnOuya(PyObject* self, PyObject* args) -> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("is_running_on_ouya");
@ -743,6 +783,44 @@ auto PyApp(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* {
auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
return {
{"clipboard_is_supported", (PyCFunction)PyClipboardIsSupported,
METH_NOARGS,
"clipboard_is_supported() -> bool\n"
"\n"
"Return whether this platform supports clipboard operations at all.\n"
"\n"
"Category: General Utility Functions\n"
"\n"
"If this returns False, UIs should not show 'copy to clipboard'\n"
"buttons, etc."},
{"clipboard_has_text", (PyCFunction)PyClipboardHasText, METH_NOARGS,
"clipboard_has_text() -> bool\n"
"\n"
"Return whether there is currently text on the clipboard.\n"
"\n"
"Category: General Utility Functions\n"
"\n"
"This will return False if no system clipboard is available; no need\n"
" to call ba.clipboard_available() separately."},
{"clipboard_set_text", (PyCFunction)PyClipboardSetText,
METH_VARARGS | METH_KEYWORDS,
"clipboard_set_text(value: str) -> None\n"
"\n"
"Copy a string to the system clipboard.\n"
"\n"
"Category: General Utility Functions\n"
"\n"
"Ensure that ba.clipboard_available() returns True before adding\n"
" buttons/etc. that make use of this functionality."},
{"clipboard_get_text", (PyCFunction)PyClipboardGetText, METH_NOARGS,
"clipboard_get_text() -> str\n"
"\n"
"Return text currently on the system clipboard.\n"
"\n"
"Category: General Utility Functions\n"
"\n"
"Ensure that ba.clipboard_has_text() returns True before calling\n"
" this function."},
{"printobjects", (PyCFunction)PyPrintObjects,
METH_VARARGS | METH_KEYWORDS,
"printobjects() -> None\n"

View File

@ -2636,14 +2636,14 @@ auto PythonMethodsUI::GetMethods() -> std::vector<PyMethodDef> {
"are applied to the Widget."},
{"rowwidget", (PyCFunction)PyRowWidget, METH_VARARGS | METH_KEYWORDS,
"rowwidget(edit: Widget = None, parent: Widget = None,\n"
"rowwidget(edit: ba.Widget = None, parent: ba.Widget = None,\n"
" size: Sequence[float] = None,\n"
" position: Sequence[float] = None,\n"
" background: bool = None, selected_child: Widget = None,\n"
" visible_child: Widget = None,\n"
" background: bool = None, selected_child: ba.Widget = None,\n"
" visible_child: ba.Widget = None,\n"
" claims_left_right: bool = None,\n"
" claims_tab: bool = None,\n"
" selection_loops_to_parent: bool = None) -> Widget\n"
" selection_loops_to_parent: bool = None) -> ba.Widget\n"
"\n"
"Create or edit a row widget.\n"
"\n"
@ -2699,17 +2699,17 @@ auto PythonMethodsUI::GetMethods() -> std::vector<PyMethodDef> {
"are applied to the Widget."},
{"textwidget", (PyCFunction)PyTextWidget, METH_VARARGS | METH_KEYWORDS,
"textwidget(edit: Widget = None, parent: Widget = None,\n"
"textwidget(edit: ba.Widget = None, parent: ba.Widget = None,\n"
" size: Sequence[float] = None, position: Sequence[float] = None,\n"
" text: Union[str, ba.Lstr] = None, v_align: str = None,\n"
" h_align: str = None, editable: bool = None, padding: float = None,\n"
" on_return_press_call: Callable[[], None] = None,\n"
" on_activate_call: Callable[[], None] = None,\n"
" selectable: bool = None, query: Widget = None, max_chars: int = "
" selectable: bool = None, query: ba.Widget = None, max_chars: int = "
"None,\n"
" color: Sequence[float] = None, click_activate: bool = None,\n"
" on_select_call: Callable[[], None] = None,\n"
" always_highlight: bool = None, draw_controller: Widget = None,\n"
" always_highlight: bool = None, draw_controller: ba.Widget = None,\n"
" scale: float = None, corner_scale: float = None,\n"
" description: Union[str, ba.Lstr] = None,\n"
" transition_delay: float = None, maxwidth: float = None,\n"

View File

@ -29,7 +29,7 @@ UI::UI() {
assert(g_platform);
// Allow overriding via an environment variable.
auto* ui_override = getenv("BA_FORCE_UI_SCALE");
auto* ui_override = getenv("BA_UI_SCALE");
bool force_test_small{};
bool force_test_medium{};
bool force_test_large{};

View File

@ -296,6 +296,7 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool {
switch (m.type) {
case WidgetMessage::Type::kTextInput:
case WidgetMessage::Type::kKey:
case WidgetMessage::Type::kPaste:
if (selected_widget_) {
bool val = selected_widget_->HandleMessage(m);
if (val != 0) {
@ -1218,8 +1219,7 @@ void ContainerWidget::SetTransition(TransitionType t) {
// stack, update the toolbar for the new topmost input-accepting window
// *immediately* (otherwise we'd have to wait for our transition to complete
// before the toolbar switches).
if (transitioning_ && transitioning_out_ && parent != nullptr
&& parent->is_main_window_stack_) {
if (transitioning_ && transitioning_out_ && parent->is_main_window_stack_) {
g_ui->root_widget()->UpdateForFocusedWindow();
}
}

View File

@ -116,8 +116,8 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) {
// Center-scale.
{
// We should really be scaling our bounds and things, but for now lets just
// do a hacky overall scale.
// We should really be scaling our bounds and things,
// but for now lets just do a hacky overall scale.
EmptyComponent c(pass);
c.SetTransparent(true);
c.PushTransform();
@ -178,6 +178,7 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) {
highlight_center_y_ = b2 - b_border + highlight_height_ * 0.5f;
highlight_dirty_ = false;
}
SimpleComponent c(pass);
c.SetTransparent(true);
c.SetPremultiplied(true);
@ -596,6 +597,16 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool {
bottom_overlap = 3.0f * extra_touch_border_scale_;
}
// If we're doing inline editing, handle clipboard paste.
if (editable() && !ShouldUseStringEditDialog()
&& m.type == WidgetMessage::Type::kPaste) {
if (g_platform->ClipboardIsSupported()) {
if (g_platform->ClipboardHasText()) {
// Just enter it char by char as if we had typed it...
AddCharsToText(g_platform->ClipboardGetText());
}
}
}
// If we're doing inline editing, handle some key events.
if (m.has_keysym && !ShouldUseStringEditDialog()) {
last_carat_change_time_ = g_game->master_time();
@ -769,22 +780,7 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool {
} else {
// Otherwise apply the text directly.
if (editable() && m.sval != nullptr) {
std::vector<uint32_t> unichars =
Utils::UnicodeFromUTF8(text_raw_, "jcjwf8f");
int len = static_cast<int>(unichars.size());
std::vector<uint32_t> sval =
Utils::UnicodeFromUTF8(*m.sval, "j4958fbv");
for (unsigned int i : sval) {
if (len < max_chars_) {
text_group_dirty_ = true;
if (carat_position_ > len) carat_position_ = len;
unichars.insert(unichars.begin() + carat_position_, i);
len++;
carat_position_++;
}
}
text_raw_ = Utils::UTF8FromUnicode(unichars);
text_translation_dirty_ = true;
AddCharsToText(*m.sval);
return true;
}
}
@ -794,8 +790,8 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool {
if (!IsSelectable()) {
return false;
}
float x = m.fval1;
float y = m.fval2;
float x{ScaleAdjustedX(m.fval1)};
float y{ScaleAdjustedY(m.fval2)};
bool claimed = (m.fval3 > 0.0f);
if (claimed) {
mouse_over_ = clear_mouse_over_ = false;
@ -813,8 +809,9 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool {
if (!IsSelectable()) {
return false;
}
float x = m.fval1;
float y = m.fval2;
float x{ScaleAdjustedX(m.fval1)};
float y{ScaleAdjustedY(m.fval2)};
auto click_count = static_cast<int>(m.fval3);
// See if a click is in our clear button.
@ -856,8 +853,8 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool {
}
}
case WidgetMessage::Type::kMouseUp: {
float x = m.fval1;
float y = m.fval2;
float x{ScaleAdjustedX(m.fval1)};
float y{ScaleAdjustedY(m.fval2)};
bool claimed = (m.fval3 > 0.0f);
if (clear_pressed_ && !claimed && editable()
@ -903,6 +900,38 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool {
return false;
}
auto TextWidget::ScaleAdjustedX(float x) -> float {
// Account for our center_scale_ value.
float offsx = x - width_ * 0.5f;
return width_ * 0.5f + offsx / center_scale_;
}
auto TextWidget::ScaleAdjustedY(float y) -> float {
// Account for our center_scale_ value.
float offsy = y - height_ * 0.5f;
return height_ * 0.5f + offsy / center_scale_;
}
auto TextWidget::AddCharsToText(const std::string& addchars) -> void {
assert(editable());
std::vector<uint32_t> unichars = Utils::UnicodeFromUTF8(text_raw_, "jcjwf8f");
int len = static_cast<int>(unichars.size());
std::vector<uint32_t> sval = Utils::UnicodeFromUTF8(addchars, "j4958fbv");
for (unsigned int i : sval) {
if (len < max_chars_) {
text_group_dirty_ = true;
if (carat_position_ > len) {
carat_position_ = len;
}
unichars.insert(unichars.begin() + carat_position_, i);
len++;
carat_position_++;
}
}
text_raw_ = Utils::UTF8FromUnicode(unichars);
text_translation_dirty_ = true;
}
void TextWidget::UpdateTranslation() {
// Apply subs/resources to get our actual text if need be.
if (text_translation_dirty_) {

View File

@ -89,6 +89,9 @@ class TextWidget : public Widget {
}
private:
auto ScaleAdjustedX(float x) -> float;
auto ScaleAdjustedY(float y) -> float;
auto AddCharsToText(const std::string& addchars) -> void;
auto ShouldUseStringEditDialog() const -> bool;
void BringUpEditDialog();
void UpdateTranslation();

View File

@ -37,7 +37,8 @@ struct WidgetMessage {
kMouseWheelVelocityH,
kMouseMove,
kScrollMouseDown,
kTextInput
kTextInput,
kPaste
};
Type type{};

View File

@ -30,10 +30,10 @@ class PipRequirement:
PIP_REQUIREMENTS = [
PipRequirement(modulename='pylint', minversion=[2, 6, 0]),
PipRequirement(modulename='mypy', minversion=[0, 790]),
PipRequirement(modulename='mypy', minversion=[0, 800]),
PipRequirement(modulename='yapf', minversion=[0, 30, 0]),
PipRequirement(modulename='cpplint', minversion=[1, 5, 4]),
PipRequirement(modulename='pytest', minversion=[6, 1, 2]),
PipRequirement(modulename='pytest', minversion=[6, 2, 1]),
PipRequirement(modulename='typing_extensions'),
PipRequirement(modulename='pytz'),
PipRequirement(modulename='ansiwrap'),

View File

@ -112,7 +112,7 @@ def _windows_enable_color() -> bool:
# open CONOUT$ instead
fdout = os.open('CONOUT$', os.O_RDWR)
try:
hout = msvcrt.get_osfhandle(fdout)
hout = msvcrt.get_osfhandle(fdout) # type: ignore
old_mode = wintypes.DWORD()
kernel32.GetConsoleMode(hout, ctypes.byref(old_mode))
mode = (new_mode & mask) | (old_mode.value & ~mask)

View File

@ -703,12 +703,10 @@ def _run_idea_inspections(projroot: Path,
if result.returncode != 0:
# In verbose mode this stuff got printed already.
if not verbose:
stdout = (
result.stdout.decode() if isinstance( # type: ignore
result.stdout, bytes) else str(result.stdout))
stderr = (
result.stderr.decode() if isinstance( # type: ignore
result.stdout, bytes) else str(result.stdout))
stdout = (result.stdout.decode() if isinstance(
result.stdout, bytes) else str(result.stdout))
stderr = (result.stderr.decode() if isinstance(
result.stdout, bytes) else str(result.stdout))
print(f'{displayname} inspection failure stdout:\n{stdout}' +
f'{displayname} inspection failure stderr:\n{stderr}')
raise RuntimeError(f'{displayname} inspection failed.')