diff --git a/.efrocachemap b/.efrocachemap index 0ac35110..0b882640 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -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" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index fd2e442c..f9fc5ba6 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -45,6 +45,7 @@ adbcfaca adbpath addcall + addchars addgame addlevel addr @@ -98,6 +99,7 @@ archs argh argparse + argsjoined argtypes argval armeabi @@ -146,6 +148,7 @@ autoretain autoselect autotools + availmins availplug aval axismotion @@ -244,6 +247,7 @@ bsuffix bsui btnh + btnlabel btnv btnx btype @@ -279,6 +283,7 @@ cameraflash camerashake campaignname + cancelbtn capb capturetheflag carentity @@ -286,6 +291,7 @@ cbits cbot cbtn + cbtnoffs ccfgs ccode ccompiler @@ -433,6 +439,7 @@ crashlytics creationflags creditslist + cresult cryptmodule cspbd cspnf @@ -782,6 +789,7 @@ freeforallendscreen freeforallsession freeforallvictory + freemins freepik freesound froemling @@ -966,6 +974,7 @@ homebrew hometest hostconfig + hostingstate hostuser hout howtoplay @@ -1201,6 +1210,7 @@ locationlist locationsingles locationval + lockpath lockstr locktype locs @@ -1417,6 +1427,7 @@ nosynctool nosynctools notdir + nowtickets npos nprocessors ntpath @@ -1439,6 +1450,8 @@ oculus oenval offsanchor + offsx + offsy ofval oggenc oghash @@ -1591,6 +1604,7 @@ powervr ppos pproxy + pptabcom pragmas prch prec @@ -1619,6 +1633,7 @@ printobjects printpaths priv + privatetab proactor proc procs @@ -1628,6 +1643,7 @@ profilenames proj projconfig + projdir projectpath projectroot projroot @@ -1751,6 +1767,7 @@ reqtype reqtypes resample + resetbtn resetinput resourcetypeinfo respawn @@ -1802,6 +1819,7 @@ samsung sandboxing sandyrb + savebtn savebutton saxutils sbblk @@ -1844,7 +1862,11 @@ sdkcheck sdkutils sdtk + selchild selectmodule + selindex + selwidget + selwidgets senze seqtype seqtypestr @@ -2145,6 +2167,7 @@ this'll threadtype throwiness + ticon timedisplay timeformat timemax diff --git a/CHANGELOG.md b/CHANGELOG.md index 859bc4dc..b8e1cb68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json index 3243d27e..542c6d54 100644 --- a/assets/.asset_manifest_public.json +++ b/assets/.asset_manifest_public.json @@ -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", diff --git a/assets/Makefile b/assets/Makefile index 7b8f4292..9abf9460 100644 --- a/assets/Makefile +++ b/assets/Makefile @@ -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 \ diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py index 9d614a65..363d5302 100644 --- a/assets/src/ba_data/python/_ba.py +++ b/assets/src/ba_data/python/_ba.py @@ -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, diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index 8e3a6e9f..72792c71 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -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 diff --git a/assets/src/ba_data/python/ba/_actor.py b/assets/src/ba_data/python/ba/_actor.py index 2751eb3c..5c8f7aaf 100644 --- a/assets/src/ba_data/python/ba/_actor.py +++ b/assets/src/ba_data/python/ba/_actor.py @@ -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: diff --git a/assets/src/ba_data/python/ba/_appconfig.py b/assets/src/ba_data/python/ba/_appconfig.py index 3e9828a7..ceb7c753 100644 --- a/assets/src/ba_data/python/ba/_appconfig.py +++ b/assets/src/ba_data/python/ba/_appconfig.py @@ -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: diff --git a/assets/src/ba_data/python/ba/_general.py b/assets/src/ba_data/python/ba/_general.py index a617fac3..57ad9a88 100644 --- a/assets/src/ba_data/python/ba/_general.py +++ b/assets/src/ba_data/python/ba/_general.py @@ -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__') diff --git a/assets/src/ba_data/python/ba/_music.py b/assets/src/ba_data/python/ba/_music.py index 0bab82de..c2f92d99 100644 --- a/assets/src/ba_data/python/ba/_music.py +++ b/assets/src/ba_data/python/ba/_music.py @@ -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 diff --git a/assets/src/ba_data/python/ba/_player.py b/assets/src/ba_data/python/ba/_player.py index f6ed2f1f..f0592a6f 100644 --- a/assets/src/ba_data/python/ba/_player.py +++ b/assets/src/ba_data/python/ba/_player.py @@ -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. diff --git a/assets/src/ba_data/python/ba/_session.py b/assets/src/ba_data/python/ba/_session.py index fbed5f85..9405ec48 100644 --- a/assets/src/ba_data/python/ba/_session.py +++ b/assets/src/ba_data/python/ba/_session.py @@ -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 diff --git a/assets/src/ba_data/python/ba/_team.py b/assets/src/ba_data/python/ba/_team.py index 6fa91d50..67e254b2 100644 --- a/assets/src/ba_data/python/ba/_team.py +++ b/assets/src/ba_data/python/ba/_team.py @@ -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. diff --git a/assets/src/ba_data/python/ba/_ui.py b/assets/src/ba_data/python/ba/_ui.py index 2e3350a3..842b1f0a 100644 --- a/assets/src/ba_data/python/ba/_ui.py +++ b/assets/src/ba_data/python/ba/_ui.py @@ -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 diff --git a/assets/src/ba_data/python/ba/ui/__init__.py b/assets/src/ba_data/python/ba/ui/__init__.py index 830af4e9..e65453f1 100644 --- a/assets/src/ba_data/python/ba/ui/__init__.py +++ b/assets/src/ba_data/python/ba/ui/__init__.py @@ -118,7 +118,7 @@ class UIEntry: class UIController: - """Wrangles UILocations. + """Wrangles ba.UILocations. Category: User Interface Classes """ diff --git a/assets/src/ba_data/python/bastd/ui/account/settings.py b/assets/src/ba_data/python/bastd/ui/account/settings.py index a87e7a9a..785f186e 100644 --- a/assets/src/ba_data/python/bastd/ui/account/settings.py +++ b/assets/src/ba_data/python/bastd/ui/account/settings.py @@ -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': diff --git a/assets/src/ba_data/python/bastd/ui/coop/browser.py b/assets/src/ba_data/python/bastd/ui/coop/browser.py index c9867e80..43ba6458 100644 --- a/assets/src/ba_data/python/bastd/ui/coop/browser.py +++ b/assets/src/ba_data/python/bastd/ui/coop/browser.py @@ -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}.') diff --git a/assets/src/ba_data/python/bastd/ui/gather/__init__.py b/assets/src/ba_data/python/bastd/ui/gather/__init__.py index 8501a6d7..472cef0a 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/__init__.py +++ b/assets/src/ba_data/python/bastd/ui/gather/__init__.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/ui/gather/abouttab.py b/assets/src/ba_data/python/bastd/ui/gather/abouttab.py index de0a16b6..a76eb11b 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/abouttab.py +++ b/assets/src/ba_data/python/bastd/ui/gather/abouttab.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/ui/gather/bases.py b/assets/src/ba_data/python/bastd/ui/gather/bases.py deleted file mode 100644 index 4510e11b..00000000 --- a/assets/src/ba_data/python/bastd/ui/gather/bases.py +++ /dev/null @@ -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.""" diff --git a/assets/src/ba_data/python/bastd/ui/gather/googleplaytab.py b/assets/src/ba_data/python/bastd/ui/gather/googleplaytab.py deleted file mode 100644 index de714058..00000000 --- a/assets/src/ba_data/python/bastd/ui/gather/googleplaytab.py +++ /dev/null @@ -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) diff --git a/assets/src/ba_data/python/bastd/ui/gather/manualtab.py b/assets/src/ba_data/python/bastd/ui/gather/manualtab.py index e5de8fac..7ae3b1c8 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/manualtab.py +++ b/assets/src/ba_data/python/bastd/ui/gather/manualtab.py @@ -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) diff --git a/assets/src/ba_data/python/bastd/ui/gather/nearbytab.py b/assets/src/ba_data/python/bastd/ui/gather/nearbytab.py index e0db519e..19fe1c53 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/nearbytab.py +++ b/assets/src/ba_data/python/bastd/ui/gather/nearbytab.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/ui/gather/privatetab.py b/assets/src/ba_data/python/bastd/ui/gather/privatetab.py new file mode 100644 index 00000000..331e9d4e --- /dev/null +++ b/assets/src/ba_data/python/bastd/ui/gather/privatetab.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/ui/gather/publictab.py b/assets/src/ba_data/python/bastd/ui/gather/publictab.py index 26282a67..b1a326ba 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/publictab.py +++ b/assets/src/ba_data/python/bastd/ui/gather/publictab.py @@ -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: diff --git a/assets/src/ba_data/python/bastd/ui/kiosk.py b/assets/src/ba_data/python/bastd/ui/kiosk.py index d5db0f97..c75caeca 100644 --- a/assets/src/ba_data/python/bastd/ui/kiosk.py +++ b/assets/src/ba_data/python/bastd/ui/kiosk.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/ui/play.py b/assets/src/ba_data/python/bastd/ui/play.py index deb9b8bf..16bfddab 100644 --- a/assets/src/ba_data/python/bastd/ui/play.py +++ b/assets/src/ba_data/python/bastd/ui/play.py @@ -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': diff --git a/assets/src/ba_data/python/bastd/ui/playlist/browser.py b/assets/src/ba_data/python/bastd/ui/playlist/browser.py index 284ff31f..f478b3c6 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/browser.py +++ b/assets/src/ba_data/python/bastd/ui/playlist/browser.py @@ -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': diff --git a/assets/src/ba_data/python/bastd/ui/profile/browser.py b/assets/src/ba_data/python/bastd/ui/profile/browser.py index 833c546d..e82fd29d 100644 --- a/assets/src/ba_data/python/bastd/ui/profile/browser.py +++ b/assets/src/ba_data/python/bastd/ui/profile/browser.py @@ -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': diff --git a/assets/src/ba_data/python/bastd/ui/settings/advanced.py b/assets/src/ba_data/python/bastd/ui/settings/advanced.py index dbac1553..1d786905 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/advanced.py +++ b/assets/src/ba_data/python/bastd/ui/settings/advanced.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/ui/settings/allsettings.py b/assets/src/ba_data/python/bastd/ui/settings/allsettings.py index 32c3e3e8..ede3823d 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/allsettings.py +++ b/assets/src/ba_data/python/bastd/ui/settings/allsettings.py @@ -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': diff --git a/assets/src/ba_data/python/bastd/ui/settings/audio.py b/assets/src/ba_data/python/bastd/ui/settings/audio.py index b84f0fd8..99f98d93 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/audio.py +++ b/assets/src/ba_data/python/bastd/ui/settings/audio.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/ui/settings/controls.py b/assets/src/ba_data/python/bastd/ui/settings/controls.py index fd81b5e1..ee06d279 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/controls.py +++ b/assets/src/ba_data/python/bastd/ui/settings/controls.py @@ -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': diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py b/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py index c1a719f0..7989d329 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py +++ b/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py @@ -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': diff --git a/assets/src/ba_data/python/bastd/ui/store/browser.py b/assets/src/ba_data/python/bastd/ui/store/browser.py index fe53e28a..990f4067 100644 --- a/assets/src/ba_data/python/bastd/ui/store/browser.py +++ b/assets/src/ba_data/python/bastd/ui/store/browser.py @@ -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))) diff --git a/assets/src/ba_data/python/bastd/ui/teamnamescolors.py b/assets/src/ba_data/python/bastd/ui/teamnamescolors.py index eecfc951..adfaad66 100644 --- a/assets/src/ba_data/python/bastd/ui/teamnamescolors.py +++ b/assets/src/ba_data/python/bastd/ui/teamnamescolors.py @@ -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: diff --git a/assets/src/ba_data/python/bastd/ui/watch.py b/assets/src/ba_data/python/bastd/ui/watch.py index 09d7e26e..b27a381a 100644 --- a/assets/src/ba_data/python/bastd/ui/watch.py +++ b/assets/src/ba_data/python/bastd/ui/watch.py @@ -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: diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index e0e5dd97..736e7a3b 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -17,6 +17,7 @@ aclass's activityplayer addcall + addchars addrs adjoint adminset @@ -47,6 +48,7 @@ appname appnameupper appstate + argsjoined asci assigninput athome @@ -54,6 +56,7 @@ audiocache automagically autoselect + availmins avel avels axismotion @@ -114,6 +117,7 @@ bsstd bstat bsuuid + btnlabel bufs buildconfig buildnumber @@ -131,8 +135,10 @@ camalign camelback camerashake + cancelbtn capitan cargs + cbtnoffs ccdd ccontext ccylinder @@ -177,6 +183,7 @@ cpuid crashenv crashlytics + cresult crom crosswire crvel @@ -329,6 +336,7 @@ fread freeform freeifaddrs + freemins freqs froemling fromini @@ -411,6 +419,7 @@ hostactivity hostcmd hostinfo + hostingstate hotkeys hotplug hscrollwidget @@ -491,6 +500,7 @@ linearsize listobj llock + lockpath lockstr locktype logmsg @@ -596,6 +606,7 @@ nonlint noone nothin + nowtickets nptr nsize ntoa @@ -613,6 +624,8 @@ obvs oculus oenval + offsx + offsy oiffsss oldname oooo @@ -668,6 +681,7 @@ positivez postinit powerup + pptabcom precalc predeclare prefs @@ -679,9 +693,11 @@ printnodes printobjects priv + privatetab profilers prog proj + projdir prolly psmx pspec @@ -739,6 +755,7 @@ reprfunc rerase resends + resetbtn resetinput resync retrysecs @@ -759,6 +776,7 @@ safecolor samsung sapspace + savebtn savebutton scancode scenetime @@ -767,6 +785,10 @@ sdkcheck sdl's sdlk + selchild + selindex + selwidget + selwidgets seqlen seqtype seqtypestr @@ -884,6 +906,7 @@ theres threadname threadtype + ticon tiltage timedisplay timeformat diff --git a/config/config.json b/config/config.json index 624a6822..0ccbddfd 100644 --- a/config/config.json +++ b/config/config.json @@ -30,7 +30,8 @@ "requests", "typing_extensions", "cpplint", - "ansiwrap" + "ansiwrap", + "filelock" ], "python_paths": [ "assets/src/ba_data/python", diff --git a/docs/ba_module.md b/docs/ba_module.md index 90db98a0..65a29ba0 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2021-01-15 for Ballistica version 1.5.30 build 20267

+

last updated on 2021-01-26 for Ballistica version 1.6.0 build 20278

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 let me know. Happy modding!


@@ -85,6 +85,10 @@

General Utility Functions


ba.DelegateNotFoundError

-

Inherits from: ba.NotFoundError, Exception, BaseException

+

Inherits from: ba.NotFoundError, Exception, BaseException

Exception raised when an expected delegate object does not exist.

Category: Exception Classes @@ -1868,7 +1872,7 @@ the data object is requested and when it's value is accessed.

<all methods inherited from ba.NotFoundError>


ba.Dependency

-

Inherits from: typing.Generic

+

Inherits from: typing.Generic

A dependency on a DependencyComponent (with an optional config).

Category: Dependency Classes

@@ -1941,7 +1945,7 @@ on the dep config value. (for instance a map required by a game type)


ba.DependencyError

-

Inherits from: Exception, BaseException

+

Inherits from: Exception, BaseException

Exception raised when one or more ba.Dependency items are missing.

Category: Exception Classes

@@ -1968,7 +1972,7 @@ on the dep config value. (for instance a map required by a game type)


ba.DependencySet

-

Inherits from: typing.Generic

+

Inherits from: typing.Generic

Set of resolved dependencies and their associated data.

Category: Dependency Classes

@@ -2129,13 +2133,13 @@ its time with lingering corpses, sound effects, etc.


ba.EmptyPlayer

-

Inherits from: ba.Player, typing.Generic

+

Inherits from: ba.Player, typing.Generic

An empty player for use by Activities that don't need to define one.

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.

@@ -2192,13 +2196,13 @@ its time with lingering corpses, sound effects, etc.

<all methods inherited from ba.Player>


ba.EmptyTeam

-

Inherits from: ba.Team, typing.Generic

+

Inherits from: ba.Team, typing.Generic

An empty player for use by Activities that don't need to define one.

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.

@@ -2234,7 +2238,7 @@ its time with lingering corpses, sound effects, etc.

<all methods inherited from ba.Team>


ba.Existable

-

Inherits from: typing.Protocol, typing.Generic

+

Inherits from: typing.Protocol, typing.Generic

A Protocol for objects supporting an exists() method.

Category: Protocols @@ -2337,8 +2341,8 @@ its time with lingering corpses, sound effects, etc.


ba.GameActivity

-

Inherits from: ba.Activity, ba.DependencyComponent, typing.Generic

-

Common base class for all game ba.Activities.

+

Inherits from: ba.Activity, ba.DependencyComponent, typing.Generic

+

Common base class for all game ba.Activities.

Category: Gameplay Classes

@@ -2990,7 +2994,7 @@ prefs, etc.


ba.InputDeviceNotFoundError

-

Inherits from: ba.NotFoundError, Exception, BaseException

+

Inherits from: ba.NotFoundError, Exception, BaseException

Exception raised when an expected ba.InputDevice does not exist.

Category: Exception Classes @@ -3000,7 +3004,7 @@ prefs, etc.

<all methods inherited from ba.NotFoundError>


ba.InputType

-

Inherits from: enum.Enum

+

Inherits from: enum.Enum

Types of input a controller can send to the game.

Category: Enums

@@ -4053,7 +4057,7 @@ signify that the default soundtrack should be used..


ba.MusicPlayMode

-

Inherits from: enum.Enum

+

Inherits from: enum.Enum

Influences behavior when playing music.

Category: Enums @@ -4156,7 +4160,7 @@ account what is supported locally.


ba.MusicType

-

Inherits from: enum.Enum

+

Inherits from: enum.Enum

Types of music available to play in-game.

Category: Enums

@@ -4368,7 +4372,7 @@ even if myactor is set to None.


ba.NodeNotFoundError

-

Inherits from: ba.NotFoundError, Exception, BaseException

+

Inherits from: ba.NotFoundError, Exception, BaseException

Exception raised when an expected ba.Node does not exist.

Category: Exception Classes @@ -4378,14 +4382,14 @@ even if myactor is set to None.

<all methods inherited from ba.NotFoundError>


ba.NotFoundError

-

Inherits from: Exception, BaseException

+

Inherits from: Exception, BaseException

Exception raised when a referenced object does not exist.

Category: Exception Classes

Methods:

-

<all methods inherited from builtins.Exception>

+

<all methods inherited from Exception>


ba.OutOfBoundsMessage

<top level class> @@ -4404,7 +4408,7 @@ even if myactor is set to None.


ba.Permission

-

Inherits from: enum.Enum

+

Inherits from: enum.Enum

Permissions that can be requested from the OS.

Category: Enums @@ -4462,7 +4466,7 @@ even if myactor is set to None.


ba.Player

-

Inherits from: typing.Generic

+

Inherits from: typing.Generic

A player in a specific ba.Activity.

Category: Gameplay Classes

@@ -4657,7 +4661,7 @@ the type-checker properly identifies the returned value as one.


ba.PlayerNotFoundError

-

Inherits from: ba.NotFoundError, Exception, BaseException

+

Inherits from: ba.NotFoundError, Exception, BaseException

Exception raised when an expected ba.Player does not exist.

Category: Exception Classes @@ -4944,7 +4948,7 @@ change this. Defaults to an empty string.


ba.ScoreType

-

Inherits from: enum.Enum

+

Inherits from: enum.Enum

Type of scores.

Category: Enums @@ -5010,7 +5014,7 @@ Pass 0 or a negative number for no ban time.

ba.Session

<top level class>

-

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

@@ -5201,7 +5205,7 @@ session.setactivity(foo) and then ba.newnode(

ba.SessionNotFoundError

-

Inherits from: ba.NotFoundError, Exception, BaseException

+

Inherits from: ba.NotFoundError, Exception, BaseException

Exception raised when an expected ba.Session does not exist.

Category: Exception Classes @@ -5345,7 +5349,7 @@ other players.


ba.SessionPlayerNotFoundError

-

Inherits from: ba.NotFoundError, Exception, BaseException

+

Inherits from: ba.NotFoundError, Exception, BaseException

Exception raised when an expected ba.SessionPlayer does not exist.

Category: Exception Classes @@ -5411,7 +5415,7 @@ of the session.


ba.SessionTeamNotFoundError

-

Inherits from: ba.NotFoundError, Exception, BaseException

+

Inherits from: ba.NotFoundError, Exception, BaseException

Exception raised when an expected ba.SessionTeam does not exist.

Category: Exception Classes @@ -5463,7 +5467,7 @@ of the session.


ba.SpecialChar

-

Inherits from: enum.Enum

+

Inherits from: enum.Enum

Special characters the game can print.

Category: Enums @@ -5680,7 +5684,7 @@ of the session.


ba.Team

-

Inherits from: typing.Generic

+

Inherits from: typing.Generic

A team in a specific ba.Activity.

Category: Gameplay Classes

@@ -5729,7 +5733,7 @@ of the session.


ba.TeamGameActivity

-

Inherits from: ba.GameActivity, ba.Activity, ba.DependencyComponent, typing.Generic

+

Inherits from: ba.GameActivity, ba.Activity, ba.DependencyComponent, typing.Generic

Base class for teams and free-for-all mode games.

Category: Gameplay Classes

@@ -5859,7 +5863,7 @@ False otherwise.


ba.TeamNotFoundError

-

Inherits from: ba.NotFoundError, Exception, BaseException

+

Inherits from: ba.NotFoundError, Exception, BaseException

Exception raised when an expected ba.Team does not exist.

Category: Exception Classes @@ -5895,7 +5899,7 @@ False otherwise.


ba.TimeFormat

-

Inherits from: enum.Enum

+

Inherits from: enum.Enum

Specifies the format time values are provided in.

Category: Enums @@ -5927,7 +5931,7 @@ you should use the ba.timer() function instead.

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 @@ -5937,28 +5941,11 @@ desired.

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():
@@ -5966,14 +5953,14 @@ def say_it():
 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)

ba.TimeType

-

Inherits from: enum.Enum

+

Inherits from: enum.Enum

Specifies the type of time for various operations to target/use.

Category: Enums

@@ -6001,7 +5988,7 @@ self.t = ba.Timer(0.3, say_it, repeat=True)

ba.UIController

<top level class>

-

Wrangles UILocations.

+

Wrangles ba.UILocations.

Category: User Interface Classes

@@ -6022,7 +6009,7 @@ self.t = ba.Timer(0.3, say_it, repeat=True)

ba.UIScale

-

Inherits from: enum.Enum

+

Inherits from: enum.Enum

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.


ba.WidgetNotFoundError

-

Inherits from: ba.NotFoundError, Exception, BaseException

+

Inherits from: ba.NotFoundError, Exception, BaseException

Exception raised when an expected ba.Widget does not exist.

Category: Exception Classes @@ -6473,6 +6460,50 @@ them elsewhere will be meaningless.

a new one is created and returned. Arguments that are not set to None are applied to the Widget.

+
+

ba.clipboard_get_text()

+

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.

+ +
+

ba.clipboard_has_text()

+

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.

+ +
+

ba.clipboard_is_supported()

+

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.

+ +
+

ba.clipboard_set_text()

+

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.

+

ba.columnwidget()

columnwidget(edit: ba.Widget = None, @@ -6582,17 +6613,19 @@ settings, exiting element counts, or other factors.

ba.existing()

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. -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.

+ +

For more info, see notes on 'existables' here: https://ballistica.net/wiki/Coding-Style-Guide


@@ -6989,14 +7022,14 @@ app running.


ba.rowwidget()

-

rowwidget(edit: Widget = None, parent: Widget = None, +

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.

@@ -7076,9 +7109,9 @@ are applied to the Widget.


ba.setmusic()

-

setmusic(musictype: Optional[MusicType], continuous: bool = False) -> None

+

setmusic(musictype: Optional[ba.MusicType], continuous: bool = False) -> None

-

Tell the game to play (or stop playing) a certain type of music.

+

Set the app to play (or stop playing) a certain type of music.

Category: Gameplay Functions

@@ -7103,15 +7136,17 @@ playing, the playing track will not be restarted.

ba.storagename()

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:
@@ -7119,22 +7154,22 @@ class MyThingie:
    # This will give something like '_mymodule_submodule_mythingie_data'.
     _STORENAME = ba.storagename('data')
-

def __init__(self, activity): - # Store some data in the Activity we were passed - activity.customdata[self._STORENAME] = {}

+
    # Use that name to store some data in the Activity we were passed.
+    def __init__(self, activity):
+        activity.customdata[self._STORENAME] = {}

ba.textwidget()

-

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, diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index 476cbae6..064ce01a 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -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. diff --git a/src/ballistica/graphics/graphics.h b/src/ballistica/graphics/graphics.h index 5a842041..57a327e4 100644 --- a/src/ballistica/graphics/graphics.h +++ b/src/ballistica/graphics/graphics.h @@ -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(); } diff --git a/src/ballistica/graphics/graphics_server.cc b/src/ballistica/graphics/graphics_server.cc index 61696137..91ce188f 100644 --- a/src/ballistica/graphics/graphics_server.cc +++ b/src/ballistica/graphics/graphics_server.cc @@ -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(kBaseVirtualResX * 0.8f); + height = static_cast(kBaseVirtualResY * 0.8f); // We should never have to recreate the context after the initial time on // our modern builds. diff --git a/src/ballistica/input/input.cc b/src/ballistica/input/input.cc index 312d44cf..d5468354 100644 --- a/src/ballistica/input/input.cc +++ b/src/ballistica/input/input.cc @@ -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. diff --git a/src/ballistica/platform/platform.cc b/src/ballistica/platform/platform.cc index cac35412..175d7efe 100644 --- a/src/ballistica/platform/platform.cc +++ b/src/ballistica/platform/platform.cc @@ -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 diff --git a/src/ballistica/platform/platform.h b/src/ballistica/platform/platform.h index 96897d39..1eda05b4 100644 --- a/src/ballistica/platform/platform.h +++ b/src/ballistica/platform/platform.h @@ -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_{}; diff --git a/src/ballistica/python/class/python_class_timer.cc b/src/ballistica/python/class/python_class_timer.cc index e432f2a7..9e730975 100644 --- a/src/ballistica/python/class/python_class_timer.cc +++ b/src/ballistica/python/class/python_class_timer.cc @@ -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; diff --git a/src/ballistica/python/methods/python_methods_app.h b/src/ballistica/python/methods/python_methods_app.h index 1bba6607..5ed747b2 100644 --- a/src/ballistica/python/methods/python_methods_app.h +++ b/src/ballistica/python/methods/python_methods_app.h @@ -16,4 +16,5 @@ class PythonMethodsApp { }; } // namespace ballistica + #endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_APP_H_ diff --git a/src/ballistica/python/methods/python_methods_gameplay.h b/src/ballistica/python/methods/python_methods_gameplay.h index 7ee229ce..e0773426 100644 --- a/src/ballistica/python/methods/python_methods_gameplay.h +++ b/src/ballistica/python/methods/python_methods_gameplay.h @@ -16,4 +16,5 @@ class PythonMethodsGameplay { }; } // namespace ballistica + #endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GAMEPLAY_H_ diff --git a/src/ballistica/python/methods/python_methods_graphics.h b/src/ballistica/python/methods/python_methods_graphics.h index 1ae2f90b..1f3577ba 100644 --- a/src/ballistica/python/methods/python_methods_graphics.h +++ b/src/ballistica/python/methods/python_methods_graphics.h @@ -16,4 +16,5 @@ class PythonMethodsGraphics { }; } // namespace ballistica + #endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_ diff --git a/src/ballistica/python/methods/python_methods_system.cc b/src/ballistica/python/methods/python_methods_system.cc index d7604e7a..b3608db4 100644 --- a/src/ballistica/python/methods/python_methods_system.cc +++ b/src/ballistica/python/methods/python_methods_system.cc @@ -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(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 { 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" diff --git a/src/ballistica/python/methods/python_methods_ui.cc b/src/ballistica/python/methods/python_methods_ui.cc index 2d156c44..88c1062c 100644 --- a/src/ballistica/python/methods/python_methods_ui.cc +++ b/src/ballistica/python/methods/python_methods_ui.cc @@ -2636,14 +2636,14 @@ auto PythonMethodsUI::GetMethods() -> std::vector { "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 { "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" diff --git a/src/ballistica/ui/ui.cc b/src/ballistica/ui/ui.cc index 81a95274..5520d981 100644 --- a/src/ballistica/ui/ui.cc +++ b/src/ballistica/ui/ui.cc @@ -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{}; diff --git a/src/ballistica/ui/widget/container_widget.cc b/src/ballistica/ui/widget/container_widget.cc index 2f370c5d..faa4632f 100644 --- a/src/ballistica/ui/widget/container_widget.cc +++ b/src/ballistica/ui/widget/container_widget.cc @@ -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(); } } diff --git a/src/ballistica/ui/widget/text_widget.cc b/src/ballistica/ui/widget/text_widget.cc index b193915b..d54a7c9d 100644 --- a/src/ballistica/ui/widget/text_widget.cc +++ b/src/ballistica/ui/widget/text_widget.cc @@ -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 unichars = - Utils::UnicodeFromUTF8(text_raw_, "jcjwf8f"); - int len = static_cast(unichars.size()); - std::vector 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(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 unichars = Utils::UnicodeFromUTF8(text_raw_, "jcjwf8f"); + int len = static_cast(unichars.size()); + std::vector 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_) { diff --git a/src/ballistica/ui/widget/text_widget.h b/src/ballistica/ui/widget/text_widget.h index eef982ab..8359d89b 100644 --- a/src/ballistica/ui/widget/text_widget.h +++ b/src/ballistica/ui/widget/text_widget.h @@ -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(); diff --git a/src/ballistica/ui/widget/widget.h b/src/ballistica/ui/widget/widget.h index edda2fda..c6e7fdda 100644 --- a/src/ballistica/ui/widget/widget.h +++ b/src/ballistica/ui/widget/widget.h @@ -37,7 +37,8 @@ struct WidgetMessage { kMouseWheelVelocityH, kMouseMove, kScrollMouseDown, - kTextInput + kTextInput, + kPaste }; Type type{}; diff --git a/tools/batools/build.py b/tools/batools/build.py index 77e2727f..ac437641 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -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'), diff --git a/tools/efro/terminal.py b/tools/efro/terminal.py index 0b904a54..aafa34fd 100644 --- a/tools/efro/terminal.py +++ b/tools/efro/terminal.py @@ -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) diff --git a/tools/efrotools/code.py b/tools/efrotools/code.py index fddf53c4..ce0c1339 100644 --- a/tools/efrotools/code.py +++ b/tools/efrotools/code.py @@ -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.')